Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
70 changes: 70 additions & 0 deletions cases/translation/expressions/session-variables.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
category = "translation"
construct = "sf.expressions.session-variables"
session_profile = "utc-v1"

[[case]]
id = "t001_between"
setup = ["SET (min, max) = (40, 70)"]
description = "The page's example: two variables set in one statement bound a BETWEEN, so 40, 55 and 70 are kept. Catches: the variables read as text so the comparison is lexical, or only the first of the pair set"
columns = [{ name = "a", type = "NUMBER(38,0)" }]
rows = [
["30"],
["40"],
["55"],
["70"],
["80"],
]
query = "SELECT ordinal, a FROM input WHERE a BETWEEN $min AND $max ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t002_types"
setup = ["SET my_variable1 = 10", "SET my_variable2 = 'example'"]
description = "The page's SET my_variable1 = 10 and SET my_variable2 = 'example': a number adds to a column and the text concatenates; the type comes from the expression. Catches: the number variable read as text (concatenated), or the text quoted twice"
columns = [{ name = "a", type = "NUMBER(38,0)" }]
rows = [
["1"],
["2"],
]
query = "SELECT ordinal, a + $my_variable1 AS n, $my_variable2 || '-' || a AS s FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t003_case_insensitive"
setup = ["SET MyVar = 5"]
description = "The page: SQL variables are identified by case-insensitive names. Catches: the name matched case-sensitively, so $myvar is unknown"
columns = [{ name = "a", type = "NUMBER(38,0)" }]
rows = [
["1"],
]
query = "SELECT ordinal, $myvar + a AS r1, $MYVAR AS r2 FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t004_from_query"
setup = ["CREATE TABLE sv_src (k NUMBER(38,0))", "INSERT INTO sv_src VALUES (3), (7)", "SET top = (SELECT MAX(k) FROM sv_src)"]
description = "The page: a variable can be set from a query that returns a single result; here MAX over a table the setup creates, which is 7. Catches: the subquery stored as text, or evaluated per row"
columns = [{ name = "a", type = "NUMBER(38,0)" }]
rows = [
["1"],
["2"],
]
query = "SELECT ordinal, a * $top AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["type-combination"]

[[case]]
id = "t005_null"
setup = ["SET n = 10"]
description = "A NULL column added to a variable; the page gives no special rule, so standard NULL handling gives NULL. Catches: the NULL replaced by the variable"
columns = [{ name = "a", type = "NUMBER(38,0)" }]
rows = [
["NULL"],
["2"],
]
query = "SELECT ordinal, a + $n AS r FROM input ORDER BY ordinal"
order = "ordered"
tags = ["null"]
55 changes: 55 additions & 0 deletions coverage/translation/obligations/expressions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2851,3 +2851,58 @@ catches = "NULL matched to NULL"
reading = "operators-subquery: expr op ALL/ANY (query) and [NOT] IN (query) compare against every value the subquery returns, in a WHERE clause; IN is = ANY and NOT IN is != ALL; [NOT] EXISTS is TRUE when the subquery returns rows (or none)"
cases = ["cases/translation/expressions/operators-subquery.in.toml#t004_null_expr"]
required = true

[[obligation]]
id = "sf.expressions.session-variables.t001"
form = "sf.expressions.session-variables"
kind = "type_combination"
target = "SET (min, max) = (40, 70) and BETWEEN $min AND $max"
note = "The page's example: two variables set in one statement bound a BETWEEN, so 40, 55 and 70 are kept."
catches = "the variables read as text so the comparison is lexical, or only the first of the pair set"
reading = "the SQL variables page: SET <name> = <expr> or SET (<n1>, <n2>) = (<e1>, <e2>) sets session variables, read as $<name> wherever a literal constant is allowed; the type comes from the expression; names are case-insensitive"
cases = ["cases/translation/expressions/session-variables.toml#t001_between"]
required = true

[[obligation]]
id = "sf.expressions.session-variables.t002"
form = "sf.expressions.session-variables"
kind = "type_combination"
target = "a number and a text variable"
note = "The page's SET my_variable1 = 10 and SET my_variable2 = 'example': a number adds to a column and the text concatenates; the type comes from the expression."
catches = "the number variable read as text (concatenated), or the text quoted twice"
reading = "the SQL variables page: SET <name> = <expr> or SET (<n1>, <n2>) = (<e1>, <e2>) sets session variables, read as $<name> wherever a literal constant is allowed; the type comes from the expression; names are case-insensitive"
cases = ["cases/translation/expressions/session-variables.toml#t002_types"]
required = true

[[obligation]]
id = "sf.expressions.session-variables.t003"
form = "sf.expressions.session-variables"
kind = "type_combination"
target = "SET MyVar, read as $myvar and $MYVAR"
note = "The page: SQL variables are identified by case-insensitive names."
catches = "the name matched case-sensitively, so $myvar is unknown"
reading = "the SQL variables page: SET <name> = <expr> or SET (<n1>, <n2>) = (<e1>, <e2>) sets session variables, read as $<name> wherever a literal constant is allowed; the type comes from the expression; names are case-insensitive"
cases = ["cases/translation/expressions/session-variables.toml#t003_case_insensitive"]
required = true

[[obligation]]
id = "sf.expressions.session-variables.t004"
form = "sf.expressions.session-variables"
kind = "type_combination"
target = "SET v = (SELECT ...)"
note = "The page: a variable can be set from a query that returns a single result; here MAX over a table the setup creates, which is 7."
catches = "the subquery stored as text, or evaluated per row"
reading = "the SQL variables page: SET <name> = <expr> or SET (<n1>, <n2>) = (<e1>, <e2>) sets session variables, read as $<name> wherever a literal constant is allowed; the type comes from the expression; names are case-insensitive"
cases = ["cases/translation/expressions/session-variables.toml#t004_from_query"]
required = true

[[obligation]]
id = "sf.expressions.session-variables.t005"
form = "sf.expressions.session-variables"
kind = "null_argument"
target = "NULL column beside a variable"
note = "A NULL column added to a variable; the page gives no special rule, so standard NULL handling gives NULL."
catches = "the NULL replaced by the variable"
reading = "the SQL variables page: SET <name> = <expr> or SET (<n1>, <n2>) = (<e1>, <e2>) sets session variables, read as $<name> wherever a literal constant is allowed; the type comes from the expression; names are case-insensitive"
cases = ["cases/translation/expressions/session-variables.toml#t005_null"]
required = true
12 changes: 11 additions & 1 deletion coverage/translation/results-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -8659,6 +8659,11 @@
"translation/expressions/operators-subquery.in.toml#t002_not_in": "matching",
"translation/expressions/operators-subquery.in.toml#t003_null_in_subquery": "matching",
"translation/expressions/operators-subquery.in.toml#t004_null_expr": "matching",
"translation/expressions/session-variables.toml#t001_between": "matching",
"translation/expressions/session-variables.toml#t002_types": "matching",
"translation/expressions/session-variables.toml#t003_case_insensitive": "matching",
"translation/expressions/session-variables.toml#t004_from_query": "matching",
"translation/expressions/session-variables.toml#t005_null": "matching",
"translation/functions/abs.toml#t001_int": "matching",
"translation/functions/abs.toml#t002_dec": "matching",
"translation/functions/abs.toml#t003_float": "matching",
Expand Down Expand Up @@ -13200,7 +13205,7 @@
"sf.expressions.operators-subquery.exists": "matching",
"sf.expressions.operators-subquery.in": "refused",
"sf.expressions.schema-variables": "not_translatable",
"sf.expressions.session-variables": "unmeasured",
"sf.expressions.session-variables": "matching",
"sf.expressions.snowflake-db-classes": "not_translatable",
"sf.functions.abs": "matching",
"sf.functions.accumulate": "refused",
Expand Down Expand Up @@ -15620,6 +15625,11 @@
"sf.expressions.operators-subquery.in.t002": "met",
"sf.expressions.operators-subquery.in.t003": "met",
"sf.expressions.operators-subquery.in.t004": "met",
"sf.expressions.session-variables.t001": "met",
"sf.expressions.session-variables.t002": "met",
"sf.expressions.session-variables.t003": "met",
"sf.expressions.session-variables.t004": "met",
"sf.expressions.session-variables.t005": "met",
"sf.functions.abs.t001": "met",
"sf.functions.abs.t002": "met",
"sf.functions.abs.t003": "met",
Expand Down
33 changes: 20 additions & 13 deletions crates/sqlquack-rules/src/catalogue/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,20 @@ use crate::conversion::constants::{
PARENTHESES, PARSE_JSON_RULE, PARSE_JSON_VARIANT, PARSE_URL_RULE, PERCENTILE_RULE, PIVOT_RULE,
POSITIONAL_COLUMN, POWER_RULE, PROJECT_FUNCTION_CALL, PROJECT_TABLE_FUNCTION, QUALIFY,
REDUCE_RULE, REGEXP, REGEXP_COLUMN_PATTERN, REGEXP_COUNTING, REGEXP_PARAMETERS,
REGEXP_POSIX_SPACE, ROUNDING, SCALAR_SUBQUERY, SELECT, SET_OPERATION, SIGN_RULE, SPLIT_RULE,
SPLIT_TO_TABLE_RULE, STAR, SUM, SUM_FLOAT, SYSTEM_TYPE_NAME, TEMPORAL_TEXT_FORMAT, TEXT_AFFIX,
TEXT_BRANCHES, TEXT_CHR, TEXT_CONTAINS, TEXT_HEX, TEXT_HEX_DECODE, TEXT_INITCAP, TEXT_MD5,
TEXT_NUMBER_COMPARISON, TEXT_PAD, TEXT_POSITION, TEXT_REPEAT, TEXT_REPLACE,
TEXT_SEARCH_POSITION, TEXT_SHA2, TEXT_TO_TIME_RULE, TEXT_TO_TIMESTAMP_RULE, TEXT_TRANSLATE,
TIME_ARITHMETIC, TIME_CAST, TIME_ZONE_CONVERSION, TIMESTAMP_FORMAT_RULE, TIMESTAMP_TIME_RULE,
TO_ARRAY_RULE, TO_BOOLEAN_RULE, TO_DATE_TEMPORAL, TO_TIMESTAMP_TEMPORAL, TO_VARIANT_RULE, TRIM,
UNION_BY_NAME, USER_OVERRIDE_RULE, UTC_OFFSET_ARITHMETIC, VALUE_AT_EXTREME_KEY, VALUES_ROWS,
VARIANT_CAST, VARIANT_COMPUTED_GET, VARIANT_EQUALITY, VARIANT_EQUALITY_BY_TEXT,
VARIANT_INSPECT, VARIANT_KIND_CAST, VARIANT_KIND_RULE, VARIANT_MAX, VARIANT_ORDER_KEY,
VARIANT_PARTITION, VARIANT_PATH, VARIANT_TEXT_BRANCHES, VARIANT_TO_VARCHAR, VARIANT_TYPE_NAME,
VARIANT_TYPED_CHECK, WHERE, WINDOW_AGGREGATE, WINDOW_FIRST_LAST, WINDOW_OFFSET, WINDOW_RANKING,
WINDOW_VALUE_FRAME, ZERO_IF_NULL_RULE, ZONE_COLUMNS, ZONED_PARTS,
REGEXP_POSIX_SPACE, ROUNDING, SCALAR_SUBQUERY, SELECT, SESSION_VARIABLE, SET_OPERATION,
SIGN_RULE, SPLIT_RULE, SPLIT_TO_TABLE_RULE, STAR, SUM, SUM_FLOAT, SYSTEM_TYPE_NAME,
TEMPORAL_TEXT_FORMAT, TEXT_AFFIX, TEXT_BRANCHES, TEXT_CHR, TEXT_CONTAINS, TEXT_HEX,
TEXT_HEX_DECODE, TEXT_INITCAP, TEXT_MD5, TEXT_NUMBER_COMPARISON, TEXT_PAD, TEXT_POSITION,
TEXT_REPEAT, TEXT_REPLACE, TEXT_SEARCH_POSITION, TEXT_SHA2, TEXT_TO_TIME_RULE,
TEXT_TO_TIMESTAMP_RULE, TEXT_TRANSLATE, TIME_ARITHMETIC, TIME_CAST, TIME_ZONE_CONVERSION,
TIMESTAMP_FORMAT_RULE, TIMESTAMP_TIME_RULE, TO_ARRAY_RULE, TO_BOOLEAN_RULE, TO_DATE_TEMPORAL,
TO_TIMESTAMP_TEMPORAL, TO_VARIANT_RULE, TRIM, UNION_BY_NAME, USER_OVERRIDE_RULE,
UTC_OFFSET_ARITHMETIC, VALUE_AT_EXTREME_KEY, VALUES_ROWS, VARIANT_CAST, VARIANT_COMPUTED_GET,
VARIANT_EQUALITY, VARIANT_EQUALITY_BY_TEXT, VARIANT_INSPECT, VARIANT_KIND_CAST,
VARIANT_KIND_RULE, VARIANT_MAX, VARIANT_ORDER_KEY, VARIANT_PARTITION, VARIANT_PATH,
VARIANT_TEXT_BRANCHES, VARIANT_TO_VARCHAR, VARIANT_TYPE_NAME, VARIANT_TYPED_CHECK, WHERE,
WINDOW_AGGREGATE, WINDOW_FIRST_LAST, WINDOW_OFFSET, WINDOW_RANKING, WINDOW_VALUE_FRAME,
ZERO_IF_NULL_RULE, ZONE_COLUMNS, ZONED_PARTS,
};
use crate::conversion::models::RuleSpec;

Expand Down Expand Up @@ -124,6 +125,12 @@ pub const RULES: &[RuleSpec] = &[
conditions: "VALUES rows of one width, as a query or FROM source: the UNION ALL of one SELECT per row, so each column takes the set operation's common type; columns are named COLUMN1, COLUMN2 and so on unless an alias list renames them. More than 1,000 rows refuse for now.",
failures: &[],
},
RuleSpec {
id: SESSION_VARIABLE,
constructs: &["node:parameter"],
conditions: "`$name` of an unquoted variable the case's setup SET, matched without regard to case, read as the literal, NULL or scalar subquery it was set to, as recorded; the subquery may not read variables or call functions whose result can change between the SET and the read. Other variables, and SET of other values, are refused.",
failures: &[],
},
RuleSpec {
id: POSITIONAL_COLUMN,
constructs: &["node:parameter"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::conversion::_helpers::support::tree::{
use crate::conversion::constants::{
COLUMN, COMMENT, CONSTANT_LITERAL, DOLLAR_PARAMETER, DOLLAR_STRING_KIND, NEGATE,
NUMBER_LITERAL, PARENTHESES, POSITIONAL_COLUMN, SCALAR_SUBQUERY, SEQUENCE_COLUMN,
SEQUENCE_PEERS, TYPED_LITERAL_KINDS, TYPED_LITERAL_PAIR,
SEQUENCE_PEERS, SESSION_VARIABLE, TYPED_LITERAL_KINDS, TYPED_LITERAL_PAIR,
};
use sqlquack_semantics::scopes::models::ScopeRelation;
use sqlquack_semantics::types::models::Holding;
Expand Down Expand Up @@ -99,6 +99,31 @@ fn sequence_column(scope: &Scope<'_>, qualifier: Option<&str>, name: &str) -> bo
false
}

/// `$name`, a session variable the case's setup SET, or `$n`, a column position.
pub(crate) fn parameter(payload: &Value, site: Site<'_>) -> Result<Emitted, Refusal> {
let Some(name) = payload.get("name").and_then(Value::as_str) else {
return positional_column(payload, site);
};
let construct = node_construct("parameter");
let refuse = |reason: String| outside_conditions(construct.clone(), reason);
if payload.get("style").and_then(Value::as_str) != Some(DOLLAR_PARAMETER) {
return Err(refuse(format!(
"the parameter {name}, which is not a $name variable"
)));
}
if payload.get("quoted").and_then(Value::as_bool) == Some(true) {
return Err(refuse(format!("the quoted variable name {name}")));
}
let resolved = name.to_ascii_uppercase();
let Some(value) = site.context.options.session_variables.get(&resolved) else {
return Err(refuse(format!(
"the variable ${name}, which the case's setup does not SET"
)));
};
let _rule = site.context.log.rule(SESSION_VARIABLE);
convert_expression(value, site)
}

/// `$n`: the n-th column of the query level's only FROM source, read as that column.
pub(crate) fn positional_column(payload: &Value, site: Site<'_>) -> Result<Emitted, Refusal> {
let construct = node_construct("parameter");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use sqlquack_duckdb::checking::main::bound_outside_lambdas::bound_outside_lambda
use sqlquack_semantics::types::models::Volatility;

use crate::conversion::_helpers::expressions::atoms::{
boolean_literal, column, commented, literal, negate, null_literal, parentheses,
positional_column, scalar_subquery,
boolean_literal, column, commented, literal, negate, null_literal, parameter, parentheses,
scalar_subquery,
};
use crate::conversion::_helpers::expressions::calls::{convert_aggregate, convert_function};
use crate::conversion::_helpers::expressions::casts::convert_cast;
Expand Down Expand Up @@ -277,7 +277,7 @@ fn convert_kind(node: &Value, site: Site<'_>) -> Result<Emitted, Refusal> {
};
match kind {
"column" => column(payload, site),
"parameter" => positional_column(payload, site),
"parameter" => parameter(payload, site),
"literal" => literal(payload, site),
"boolean" => Ok(boolean_literal(payload, site)),
"null" => Ok(null_literal(site)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ use crate::conversion::constants::{
DATE_FORMAT_RULE, DATE_NAME, DATE_PART_RULE, DATE_PARTS, DAY_NAME_RULE, DECIMAL_EPOCH_RULE,
FRACTIONAL_DATE_AMOUNT, FROM_PARTS, LAST_DAY_RULE, MAX_FRACTION_DIGITS_U8, MICROSECOND_DIGITS,
MONTH_NAME_FIELDS, MONTH_NAME_FOLLOWERS, NULL_BRANCHES, NUMBER_TEXT_FORMAT, OFFSET_FREE_UNITS,
RECORDED_TIME_ZONES, SCALE_DIGITS, TEMPORAL_TEXT_FIELDS, TEMPORAL_TEXT_FORMAT,
TEMPORAL_TEXT_SEPARATORS, TEXT_TO_TIME_RULE, TEXT_TO_TIMESTAMP_RULE, TIME_ARITHMETIC,
TIME_ZONE_CONVERSION, TIMESTAMP_FORMAT_RULE, TIMESTAMP_NTZ_NAME, TIMESTAMP_TIME_RULE,
TO_DATE_TEMPORAL, TO_TIMESTAMP_TEMPORAL, UTC_OFFSET_ARITHMETIC, UTC_ZONE, ZONE_COLUMNS,
ZONED_PARTS,
SCALE_DIGITS, TEMPORAL_TEXT_FIELDS, TEMPORAL_TEXT_FORMAT, TEMPORAL_TEXT_SEPARATORS,
TEXT_TO_TIME_RULE, TEXT_TO_TIMESTAMP_RULE, TIME_ARITHMETIC, TIME_ZONE_CONVERSION,
TIMESTAMP_FORMAT_RULE, TIMESTAMP_NTZ_NAME, TIMESTAMP_TIME_RULE, TO_DATE_TEMPORAL,
TO_TIMESTAMP_TEMPORAL, UTC_OFFSET_ARITHMETIC, UTC_ZONE, ZONE_COLUMNS, ZONED_PARTS,
};
use crate::time_zones::constants::RECORDED_TIME_ZONES;
use crate::time_zones::main::recorded_zone::recorded_zone;
use sqlquack_duckdb::raises::main::raised::raised;
use sqlquack_duckdb::temporal::constants::{DUCKDB_CURRENT_DATE, DUCKDB_CURRENT_TIMESTAMP};
use sqlquack_policy::policy::constants::{
Expand Down Expand Up @@ -1110,7 +1111,7 @@ struct Zone {
/// A literal zone name, or a VARCHAR expression naming one per row.
fn zone_operand(node: &Value, construct: &Construct, site: Site<'_>) -> Result<Zone, Refusal> {
if let Some(zone) = zone_literal(node, site) {
if !RECORDED_TIME_ZONES.contains(&zone.as_str()) {
if !recorded_zone(&zone) {
return Err(outside_conditions(
construct.clone(),
format!(
Expand Down
1 change: 1 addition & 0 deletions crates/sqlquack-rules/src/conversion/_helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) mod expressions;
pub(crate) mod queries;
pub(crate) mod setup;
pub(crate) mod support;
3 changes: 3 additions & 0 deletions crates/sqlquack-rules/src/conversion/_helpers/setup/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub(crate) mod tables;
pub(crate) mod user_types;
pub(crate) mod variables;
Loading