Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/sqlquack-core/src/diagnostics/codes.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ SQ0005 not_expressible
SQ0006 conversion_budget
SQ0007 session_parameters
SQ0008 divergence_refused
SQ0009 unaliased_output_name
SQ0101 input_too_large
SQ0102 token_budget_exceeded
SQ0103 nesting_depth_exceeded
Expand Down
18 changes: 18 additions & 0 deletions crates/sqlquack-core/src/diagnostics/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ pub const CONVERSION_BUDGET: &str = "SQ0006";
pub const SESSION_PARAMETERS: &str = "SQ0007";
/// The conversion needs a known divergence the policy refuses in this mode.
pub const DIVERGENCE_REFUSED: &str = "SQ0008";
/// An unaliased select item's Snowflake-generated column name is not verified.
pub const UNALIASED_OUTPUT_NAME: &str = "SQ0009";
/// The construct name of that refusal, which its code is registered under.
pub const UNALIASED_OUTPUT_NAME_CONSTRUCT: &str = "unaliased_output_name";

/// Every diagnostic code, in the order issued.
pub const CODES: &[CodeEntry] = &[
Expand Down Expand Up @@ -156,6 +160,20 @@ pub const CODES: &[CodeEntry] = &[
construct.",
example: r#"echo "SELECT HASH(1) AS h" | sqlquack convert --builtin-policy"#,
},
CodeEntry {
code: UNALIASED_OUTPUT_NAME,
name: UNALIASED_OUTPUT_NAME_CONSTRUCT,
alias: None,
title: "this column has no alias, and SQLQuack has not verified the name Snowflake gives it",
help: Some("add an alias, e.g. `AS one`"),
explanation: "Snowflake names an unaliased select item after the expression's text, and \
a converted query must return the same column names. SQLQuack emits Snowflake's name \
only for the expression shapes whose names are verified against recorded results; \
the expression at the span is not one of them, so the statement is refused rather \
than converted with a different column name. An alias fixes the name in both \
engines.",
example: r#"echo "SELECT 'a' || 'b'" | sqlquack convert --builtin-policy"#,
},
CodeEntry {
code: "SQ0101",
name: "input_too_large",
Expand Down
9 changes: 8 additions & 1 deletion crates/sqlquack-core/src/diagnostics/main/diagnose.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! `diagnose`: refusals as coded diagnostics pointing into their source.

use crate::classification::models::ConstructFamily;
use crate::diagnostics::_helpers::code_entry::code_entry;
use crate::diagnostics::_helpers::coded::coded;
use crate::diagnostics::_helpers::line_index::LineIndex;
Expand Down Expand Up @@ -34,6 +35,7 @@ pub fn diagnose(refusals: &[Refusal], source: &str) -> Vec<Diagnostic> {
.construct
.as_ref()
.filter(|_| refusal.category != RefusalCategory::SnowflakeRejects)
.filter(|_| code == MISSING_RULE || refusal.category != RefusalCategory::MissingRule)
{
diagnostic.message = format!("{}: `{construct}`", diagnostic.message);
}
Expand All @@ -46,7 +48,12 @@ pub fn diagnose(refusals: &[Refusal], source: &str) -> Vec<Diagnostic> {
fn code_and_label(refusal: &Refusal) -> (&'static str, String) {
let code = match refusal.category {
RefusalCategory::ParseFidelity => PARSE_FIDELITY,
RefusalCategory::MissingRule => MISSING_RULE,
RefusalCategory::MissingRule => refusal
.construct
.as_ref()
.filter(|construct| construct.family == ConstructFamily::Syntax)
.and_then(|construct| code_entry(&construct.name))
.map_or(MISSING_RULE, |entry| entry.code),
RefusalCategory::TypeCondition => TYPE_CONDITION,
RefusalCategory::UnresolvedName => UNRESOLVED_NAME,
RefusalCategory::NotExpressible => NOT_EXPRESSIBLE,
Expand Down
12 changes: 7 additions & 5 deletions crates/sqlquack-rules/src/conversion/_helpers/queries/query.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Queries: SELECT with its projection and clauses, and WITH.

use serde_json::{Map, Value, json};
use sqlquack_core::classification::models::Construct;
use sqlquack_core::classification::models::{Construct, ConstructFamily};
use sqlquack_core::diagnostics::constants::UNALIASED_OUTPUT_NAME_CONSTRUCT;
use sqlquack_core::provenance::constants::QUERY_CONSTRUCT;
use sqlquack_core::provenance::models::ShapeArgument;
use sqlquack_core::refusals::main::missing::missing;
Expand Down Expand Up @@ -622,10 +623,11 @@ fn convert_projection(
Some((kind, _)) => {
let emitted = convert_held(item, site)?;
if named {
return Err(outside_conditions(
node_construct(kind),
"an unaliased expression gets a Snowflake-generated column name".to_owned(),
));
let refusal = missing(
Construct::new(ConstructFamily::Syntax, UNALIASED_OUTPUT_NAME_CONSTRUCT),
format!("Snowflake names this `{kind}` column after its text"),
);
return Err(site.context.locator.placed(refusal, item));
}
Ok(vec![(None, emitted)])
}
Expand Down
13 changes: 13 additions & 0 deletions crates/sqlquack/tests/cli/test_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,19 @@ fn given_refused_sql_when_converting_then_text_and_json_carry_the_same_coded_dia
under the built-in default\n",
expected_json: ("SQ0203", "qualify_without_window", 1, 10),
},
ConvertDiagnosticTestCase {
description: "an unaliased item has its own code, its own span and an alias as help",
sql: "SELECT 'a' AS a, UPPER('b')",
flags: &[],
expected_text: concat!(
"error[SQ0009]: this column has no alias, and SQLQuack has not verified the name ",
"Snowflake gives it\n --> <stdin>:1:18\n |\n1 | SELECT 'a' AS a, UPPER('b')\n",
" | ^^^^^^^^^^ Snowflake names this `function` column after its text\n",
" |\n = help: add an alias, e.g. `AS one`\n",
"sqlquack convert: refused, 1 refusal under the built-in default\n",
),
expected_json: ("SQ0009", "unaliased_output_name", 1, 18),
},
ConvertDiagnosticTestCase {
description: "a divergence the policy refuses has its own code and help naming the policy",
sql: "SELECT HASH(1) AS h",
Expand Down
1 change: 1 addition & 0 deletions website/src/content/docs/docs/cli/convert.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Each refusal is one of these:
| [`SQ0006`](/docs/diagnostics/reference/#sq0006) | Conversion exceeded its time or size budget. |
| [`SQ0007`](/docs/diagnostics/reference/#sq0007) | The session parameters match no recorded profile. |
| [`SQ0008`](/docs/diagnostics/reference/#sq0008) | The conversion needs a known divergence the policy refuses. |
| [`SQ0009`](/docs/diagnostics/reference/#sq0009) | A select item has no alias, and SQLQuack has not verified the column name Snowflake gives it. |

SQL that does not parse, or that Snowflake rejects, is refused with the same codes as
[`check`](/docs/cli/check/). A `VALUES` list of more than 1,000 rows is refused.
Expand Down
15 changes: 15 additions & 0 deletions website/src/content/docs/docs/diagnostics/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ renumbered. See [Reading diagnostics](/docs/diagnostics/overview/) for the outpu
| [`SQ0006`](#sq0006) | `conversion_budget` | conversion exceeded its budget |
| [`SQ0007`](#sq0007) | `session_parameters` | the session parameters match no recorded session profile |
| [`SQ0008`](#sq0008) | `divergence_refused` | the conversion needs a known divergence the policy refuses |
| [`SQ0009`](#sq0009) | `unaliased_output_name` | this column has no alias, and SQLQuack has not verified the name Snowflake gives it |
| [`SQ0101`](#sq0101) | `input_too_large` | the input is larger than the parser accepts |
| [`SQ0102`](#sq0102) | `token_budget_exceeded` | the input has more tokens than the parser accepts |
| [`SQ0103`](#sq0103) | `nesting_depth_exceeded` | brackets nest deeper than the parser accepts |
Expand Down Expand Up @@ -135,6 +136,20 @@ The construct at the span converts only by relying on the known divergence the l
echo "SELECT HASH(1) AS h" | sqlquack convert --builtin-policy
```

### SQ0009

**this column has no alias, and SQLQuack has not verified the name Snowflake gives it** (`unaliased_output_name`)

Snowflake names an unaliased select item after the expression's text, and a converted query must return the same column names. SQLQuack emits Snowflake's name only for the expression shapes whose names are verified against recorded results; the expression at the span is not one of them, so the statement is refused rather than converted with a different column name. An alias fixes the name in both engines.

**Help:** add an alias, e.g. `AS one`

**Example:**

```sh
echo "SELECT 'a' || 'b'" | sqlquack convert --builtin-policy
```

## Input limits

Limits that keep parsing and conversion bounded.
Expand Down
Loading