diff --git a/lib/sql-types.scm b/lib/sql-types.scm index 91019aa8d..5bc7510d2 100644 --- a/lib/sql-types.scm +++ b/lib/sql-types.scm @@ -18,9 +18,9 @@ the query block's result-types fact for the wire/PDO result contract. Nothing type-related survives into the executable plan. A descriptor is (FORMULA TYPE COLLATION): - - TYPE: uppercase SQL type name, or "any". - - COLLATION: nil, or a (NAME COERCIBILITY) pair. Lower coercibility wins: - 0 = explicit COLLATE, 2 = column, 4 = literal, 5 = numeric/ignorable. */ +- TYPE: uppercase SQL type name, or "any". +- COLLATION: nil, or a (NAME COERCIBILITY) pair. Lower coercibility wins: +0 = explicit COLLATE, 2 = column, 4 = literal, 5 = numeric/ignorable. */ (define sql_info (lambda (formula type collation) (list formula type collation))) (define sql_info_formula car) @@ -61,13 +61,13 @@ unicode alias. Returns nil or a (NAME COERCIBILITY) pair. */ (define sql_merge_type (lambda (ta tb) (if (equal? ta "NULL") tb - (if (equal? tb "NULL") ta - (if (equal? ta "any") "any" - (if (equal? tb "any") "any" - (if (equal? ta tb) ta - (if (or (sql_text_type? ta) (sql_text_type? tb)) "VARCHAR" - (if (or (equal? ta "DOUBLE") (equal? tb "DOUBLE")) "DOUBLE" - (if (and (sql_numeric_type? ta) (sql_numeric_type? tb)) "DECIMAL" "VARCHAR")))))))))) + (if (equal? tb "NULL") ta + (if (equal? ta "any") "any" + (if (equal? tb "any") "any" + (if (equal? ta tb) ta + (if (or (sql_text_type? ta) (sql_text_type? tb)) "VARCHAR" + (if (or (equal? ta "DOUBLE") (equal? tb "DOUBLE")) "DOUBLE" + (if (and (sql_numeric_type? ta) (sql_numeric_type? tb)) "DECIMAL" "VARCHAR")))))))))) (define sql_merge_info (lambda (a b) (sql_info nil @@ -112,11 +112,11 @@ cache guard by the caller; no captured value enters the result contract. */ /* ---- function return contracts ---- Each entry is (HEAD TYPE MODE): - - "fixed" : result TYPE regardless of arguments - - "first" : result TYPE, collation inherited from the first argument - - "text" : result TYPE, collation merged from all arguments - - "merge" : result type and collation merged from all arguments (COALESCE, …) - - "case" : like "merge" but over the value arms only (IF/searched CASE) */ +- "fixed" : result TYPE regardless of arguments +- "first" : result TYPE, collation inherited from the first argument +- "text" : result TYPE, collation merged from all arguments +- "merge" : result type and collation merged from all arguments (COALESCE, …) +- "case" : like "merge" but over the value arms only (IF/searched CASE) */ (define sql_core_function_rules (list (list (quote sql_compare) "BOOLEAN" "fixed") @@ -338,31 +338,35 @@ the conditions are booleans and would otherwise pollute the merge. */ (reduce (cdr infos) (lambda (t i) (sql_type_fuse_arith (sql_arith_op_name head) t (sql_info_type i))) (sql_info_type (car infos))) nil) - (if (and (sql_comparison_head? head) (equal? (count infos) 2)) - (sql_comparison_info head (car infos) (cadr infos)) - (begin - (define rule (sql_function_rule head)) - (define rtype (cadr rule)) - (define mode (nth rule 2)) - (define value_infos (if (equal? mode "case") (sql_case_value_infos infos) infos)) - (define merged (sql_merge_infos value_infos)) - (define type (if (has? (list "merge" "case") mode) (sql_info_type merged) rtype)) - /* fixed: the function owns its result, no operand collation inherited. - first: inherit from the first argument. text/merge/case: merge the - value operands. */ - (define collation (if (not (sql_text_type? type)) nil - (if (equal? mode "first") - (if (empty_list? infos) nil (sql_info_collation (car infos))) - (if (has? (list "text" "merge" "case") mode) - (sql_info_collation merged) - nil)))) - (sql_info (cons head forms) type collation))))))) + (if (and (sql_comparison_head? head) (equal? (count infos) 2)) + (sql_comparison_info head (car infos) (cadr infos)) + (begin + (define rule (sql_function_rule head)) + (define rtype (cadr rule)) + (define mode (nth rule 2)) + (define value_infos (if (equal? mode "case") (sql_case_value_infos infos) infos)) + (define merged (sql_merge_infos value_infos)) + (define type (if (has? (list "merge" "case") mode) (sql_info_type merged) rtype)) + /* fixed: the function owns its result, no operand collation inherited. + first: inherit from the first argument. text/merge/case: merge the + value operands. */ + (define collation (if (not (sql_text_type? type)) nil + (if (equal? mode "first") + (if (empty_list? infos) nil (sql_info_collation (car infos))) + (if (has? (list "text" "merge" "case") mode) + (sql_info_collation merged) + nil)))) + (sql_info (cons head forms) type collation))))))) (define sql_expr_info (lambda (sources expr) (match expr ((symbol get_column) tblvar tbl_ic col col_ic) (sql_get_column_info sources tblvar tbl_ic col col_ic) ((quote get_column) tblvar tbl_ic col col_ic) (sql_get_column_info sources tblvar tbl_ic col col_ic) ((symbol quote) _datum) (sql_info expr "any" nil) + /* Frontends attach the declared contract to their conversion formula. + Resolve the operand normally; only its executable formula survives. */ + ((symbol sql_type_expr) formula type collation) + (sql_info (sql_info_formula (sql_expr_info sources formula)) type collation) ((symbol session) _key) (sql_info expr "any" nil) ((symbol session_globalvar) _key) (sql_info expr "any" nil) ((symbol lambda) _params _body) (sql_info expr "any" nil) @@ -375,12 +379,12 @@ relation and operator string are resolved at plan time. */ (define sql_compare (lambda (left right less operator collation) (if (or (nil? left) (nil? right)) nil (if (equal? operator "equal??") (and (not (less left right)) (not (less right left))) - (if (equal? operator "equal?") (and (not (less left right)) (not (less right left))) - (if (equal? operator "<") (less left right) - (if (equal? operator ">") (less right left) - (if (equal? operator "<=") (not (less right left)) - (if (equal? operator ">=") (not (less left right)) - (error "unsupported SQL comparison")))))))))) + (if (equal? operator "equal?") (and (not (less left right)) (not (less right left))) + (if (equal? operator "<") (less left right) + (if (equal? operator ">") (less right left) + (if (equal? operator "<=") (not (less right left)) + (if (equal? operator ">=") (not (less left right)) + (error "unsupported SQL comparison")))))))))) /* ---- query-block level type resolution ---- @@ -421,9 +425,9 @@ always treated as "bin" — get the coercing relation. */ (define info (sql_expr_info sources order_expr)) (define coll (sql_info_collation info)) (if (and (or (equal? order_dir <) (equal? order_dir >)) - (not (sql_column_ref? (sql_info_formula info))) - (sql_text_type? (sql_info_type info)) - (not (nil? coll))) + (not (sql_column_ref? (sql_info_formula info))) + (sql_text_type? (sql_info_type info)) + (not (nil? coll))) (list (sql_info_formula info) (collate (car coll) (equal? order_dir >))) (list (sql_info_formula info) order_dir))) _ item))) @@ -432,7 +436,7 @@ always treated as "bin" — get the coercing relation. */ (if (not (query_block? block)) block (begin - (define sources (qb_sources block)) + (define sources (sql_type_lower_annotations (qb_sources block))) (define outer (coalesceNil outer_sources '())) (define all_sources (if (empty_list? outer) sources (merge (list sources outer)))) (make_query_block @@ -446,15 +450,37 @@ always treated as "bin" — get the coercing relation. */ (if (nil? (qb_having block)) nil (sql_type_formula all_sources (qb_having block))) (map (coalesceNil (qb_order block) '()) (lambda (order_item) (sql_type_order_item all_sources order_item))) - (qb_limit block) (qb_offset block) (qb_hidden block) (qb_stages block) + (qb_limit block) (qb_offset block) (sql_type_lower_annotations (qb_hidden block)) (sql_type_lower_annotations (qb_stages block)) (qassoc_set (qb_facts block) (quote result-types) (map_assoc (expand_query_block_fields sources (qb_fields block)) (lambda (rt_title rt_expr) (sql_type_result_descriptor all_sources rt_expr))))))))) -/* Entry point: annotate the root query block of a compiled IR. Group/orc/window -stages and non-query-block roots pass through untouched in this slice. */ +/* Rewrite only paths containing frontend expression contracts. nil means +unchanged; a singleton holds the replacement, including a legitimate NULL. +Retain annotation-free sources/stage graphs instead of cloning every node +for frontends that have emitted no contracts. Quoted syntax stays opaque. */ +(define sql_type_annotation_replacement (lambda (node) + (match node + ((symbol sql_type_expr) _formula _type _collation) (list (sql_type_formula '() node)) + ((symbol quote) _datum) nil + (cons head tail) (begin + (define rewritten_head (sql_type_annotation_replacement head)) + (define rewritten_tail (sql_type_annotation_replacement tail)) + (if (and (nil? rewritten_head) (nil? rewritten_tail)) nil + (list (cons + (if (nil? rewritten_head) head (car rewritten_head)) + (if (nil? rewritten_tail) tail (car rewritten_tail)))))) + _ nil))) + +(define sql_type_lower_annotations (lambda (node) + (match (sql_type_annotation_replacement node) + '(formula) formula + _ node))) + +/* Entry point: annotate root expressions, then remove remaining expression contracts +from nested logical inputs without inventing storage or planner operations. */ (define sql_type_annotate_ir (lambda (ir) - (if (query_block? (ir_root ir)) - (make_ir (ir_kind ir) (sql_type_query_block (ir_root ir) '()) - (ir_stages ir) (ir_context_of ir) (ir_return ir)) - ir))) + (make_ir (ir_kind ir) + (if (query_block? (ir_root ir)) + (sql_type_query_block (ir_root ir) '()) (sql_type_lower_annotations (ir_root ir))) + (sql_type_lower_annotations (ir_stages ir)) (ir_context_of ir) (ir_return ir)))) diff --git a/lib/tsql-parser.scm b/lib/tsql-parser.scm index b2e6e498a..dc1b7f443 100644 --- a/lib/tsql-parser.scm +++ b/lib/tsql-parser.scm @@ -127,33 +127,53 @@ runtime and storage hot paths; do not substitute float64 for exact casts. */ (if (int? value) value (intdiv (tsql_numeric_input value) 1)))) (if (or (nil? integer) (< integer minimum) (> integer maximum)) (error "integer conversion overflow") integer))))) -(define tsql_cast_float (lambda (value) (if (nil? value) nil (tsql_numeric_input value)))) +(define tsql_cast_float (lambda (value) (if (nil? value) nil (simplify (tsql_numeric_input value))))) (define tsql_cast_bit (lambda (value) (if (nil? value) nil (if (and (string? value) (equal? (toUpper (sql_trim value)) "TRUE")) true (if (and (string? value) (equal? (toUpper (sql_trim value)) "FALSE")) false (not (equal? (tsql_numeric_input value) 0))))))) +/* A cast keeps its syntax-declared SQL type until the shared expression +compiler consumes the (expr, type, collation) contract. Runtime values do not +select that type, and no arithmetic operation is needed merely to mark it. */ (define tsql_cast_expr (lambda (value type dimensions) (begin (define name (toUpper type)) (tsql_decl_dimensions name dimensions) - (match name - "INT" (list (quote intdiv) (list tsql_cast_integer value -2147483648 2147483647) 1) - "INTEGER" (list (quote intdiv) (list tsql_cast_integer value -2147483648 2147483647) 1) - "SMALLINT" (list (quote intdiv) (list tsql_cast_integer value -32768 32767) 1) - "TINYINT" (list (quote intdiv) (list tsql_cast_integer value 0 255) 1) - "BIGINT" (list (quote intdiv) (list tsql_cast_integer value (tsql_parse_integer "-9223372036854775808") (tsql_parse_integer "9223372036854775807")) 1) - "FLOAT" (list (quote if) (list (quote nil?) value) nil (list (quote simplify) (list tsql_cast_float value))) + (define formula (match name + "INT" (list tsql_cast_integer value -2147483648 2147483647) + "INTEGER" (list tsql_cast_integer value -2147483648 2147483647) + "SMALLINT" (list tsql_cast_integer value -32768 32767) + "TINYINT" (list tsql_cast_integer value 0 255) + "BIGINT" (list tsql_cast_integer value (tsql_parse_integer "-9223372036854775808") (tsql_parse_integer "9223372036854775807")) + "FLOAT" (list tsql_cast_float value) /* Approximate types use native floating precision. Binary32 rounding and dialect precision boundaries remain noncritical compatibility work. */ - "REAL" (list (quote if) (list (quote nil?) value) nil (list (quote simplify) (list tsql_cast_float value))) - "BIT" (list (quote sql_not) (list (quote sql_not) (list tsql_cast_bit value))) - _ (error "unsupported conversion to " name))))) + "REAL" (list tsql_cast_float value) + "BIT" (list tsql_cast_bit value) + _ (error "unsupported conversion to " name))) + (list (quote sql_type_expr) formula name nil)))) +/* Window inputs must be plain formulas before the common planner derives +aggregate identities. Consume their expression triples here and keep the +window result contract separately until shared expression compilation. */ +(define tsql_window_expr (lambda (name args over) (begin + (define infos (map args (lambda (arg) (sql_expr_info (list) arg)))) + (define input_type (if (empty_list? infos) "any" (sql_info_type (car infos)))) + (define type (match name + "COUNT" "BIGINT" + "SUM" (match input_type + "TINYINT" "INT" "SMALLINT" "INT" "INTEGER" "INT" + "REAL" "FLOAT" _ input_type) + "MIN" input_type "MAX" input_type + _ "any")) + (define formula (list (quote window_func) name (map infos sql_info_formula) (sql_type_lower_annotations over))) + (if (equal? type "any") formula + (list (quote sql_type_expr) formula type + (if (empty_list? infos) nil (sql_info_collation (car infos)))))))) (define tsql_negate_expr (lambda (value) (begin - /* A constant cast already gives us its value and declared range at compile - time. Diagnose negation overflow here without introducing runtime type tags - or checks into ordinary arithmetic. Dynamic result typing is a follow-up. */ + /* Constant operands and the syntax-declared range suffice to diagnose + negation overflow without runtime SQL tags or ordinary arithmetic checks. */ (match value - ((symbol intdiv) '(conversion operand minimum maximum) _divisor) + ((symbol sql_type_expr) '(conversion operand minimum maximum) _type _collation) (if (and (equal? conversion tsql_cast_integer) (plain_literal_expr? operand)) (if (and (< minimum 0) (equal? (tsql_cast_integer operand minimum maximum) minimum)) (error "unary integer conversion overflow") true) true) @@ -194,15 +214,13 @@ never from the first row. Unknown expression types stay text in this first slice (define tsql_result_columns (lambda (query) (match query ((symbol query-block) _schema sources fields _where _group _having _order _limit _offset _hidden _stages _facts) (extract_assoc (expand_query_block_fields sources fields) (lambda (name expression) (begin - /* Native integer division guarantees an integer even when a conversion's - input has no catalog type. Describe that wire result before optimization. */ - (define type (match expression - ((symbol intdiv) _left _right) "BIGINT" - _ (sql_info_type (sql_expr_info sources expression)))) + (define type (sql_info_type (sql_expr_info sources expression))) (define kind (if (has? '("INT" "INTEGER" "BIGINT" "SMALLINT" "TINYINT") type) 38 (if (has? '("FLOAT" "DOUBLE" "REAL" "DECIMAL" "NUMERIC") type) 109 (if (has? '("BOOLEAN" "BOOL" "BIT") type) 104 231)))) - (list "name" name "kind" kind "size" (if (equal? kind 231) 8000 (if (equal? kind 104) 1 8)) "flags" 1)))) + (define size (if (equal? kind 231) 8000 (if (equal? kind 104) 1 + (match type "TINYINT" 1 "SMALLINT" 2 "INT" 4 "INTEGER" 4 _ 8)))) + (list "name" name "kind" kind "size" size "flags" 1)))) ((symbol union-block) _mode branches _order _limit _offset _facts) (tsql_result_columns (car branches)) _ (error "unsupported T-SQL result metadata shape")))) @@ -332,12 +350,12 @@ never from the first row. Unknown expression types stay text in this first slice (parser '((atom "CASE" true) (define expr tsql_expression) (define conditions (+ (parser '((atom "WHEN" true) (define a tsql_expression) (atom "THEN" true) (define b tsql_expression)) '(a b)))) (? (atom "ELSE" true) (define elsebranch tsql_expression)) (atom "END" true)) (merge '((quote if)) (merge (extract_assoc (merge conditions) (lambda (a b) '('('equal?? expr a) b)))) '(elsebranch))) - (parser '((atom "COUNT" true) "(" "*" ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") '('window_func "COUNT" '() _over)) - (parser '((atom "COUNT" true) "(" (define e tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") '('window_func "COUNT" (list e) _over)) - (parser '((atom "SUM" true) "(" (define s tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") '('window_func "SUM" (list s) _over)) - (parser '((atom "AVG" true) "(" (define s tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") '('window_func "AVG" (list s) _over)) - (parser '((atom "MIN" true) "(" (define s tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") '('window_func "MIN" (list s) _over)) - (parser '((atom "MAX" true) "(" (define s tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") '('window_func "MAX" (list s) _over)) + (parser '((atom "COUNT" true) "(" "*" ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") (tsql_window_expr "COUNT" (list) _over)) + (parser '((atom "COUNT" true) "(" (define e tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") (tsql_window_expr "COUNT" (list e) _over)) + (parser '((atom "SUM" true) "(" (define s tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") (tsql_window_expr "SUM" (list s) _over)) + (parser '((atom "AVG" true) "(" (define s tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") (tsql_window_expr "AVG" (list s) _over)) + (parser '((atom "MIN" true) "(" (define s tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") (tsql_window_expr "MIN" (list s) _over)) + (parser '((atom "MAX" true) "(" (define s tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") (tsql_window_expr "MAX" (list s) _over)) (parser '((atom "COUNT" true) "(" (atom "DISTINCT" true) (define e tsql_expression) ")") '('count_distinct e)) (parser '((atom "COUNT" true) "(" "*" ")") '((quote aggregate) 1 (quote +) 0)) @@ -349,7 +367,7 @@ never from the first row. Unknown expression types stay text in this first slice (parser '((atom "MAX" true) "(" (define s tsql_expression) ")") (begin (define d (sql_aggregates "MAX")) '('aggregate s (car d) (cadr d)))) (parser '((define fn tsql_identifier_unquoted) "(" (define arg tsql_expression) ")" (atom "OVER" true) "(" (define _over tsql_window_spec) ")") - '('window_func (toUpper fn) (list arg) _over)) + (tsql_window_expr (toUpper fn) (list arg) _over)) (parser '((atom "SUBSTRING" true) "(" (define s tsql_expression) "," (define start tsql_expression) "," (define len tsql_expression) ")") '((quote sql_substr) s start len)) @@ -795,7 +813,7 @@ never from the first row. Unknown expression types stay text in this first slice true (error "INSERT column count does not match value count")) (if (reduce rows (lambda (found row) (or found (tsql_dataset_contains_inner_select row))) false) (tsql_insert_select_plan database tbl columns (tsql_values_to_select_query database columns rows)) - '('insert '('table database tbl) (cons list columns) (sql_insert_values_expr rows) + '('insert '('table database tbl) (cons list columns) (sql_insert_values_expr (map rows (lambda (row) (map row (lambda (value) (sql_type_formula (list) value)))))) '(list) nil false '('lambda '('id) '('session "last_insert_id" 'id)) (quote tx))) ))) (define tsql_insert_select (parser '( diff --git a/tests/performance/range-first-aggregate.yaml b/tests/performance/range-first-aggregate.yaml index 40b46f512..ab8d3d85a 100644 --- a/tests/performance/range-first-aggregate.yaml +++ b/tests/performance/range-first-aggregate.yaml @@ -3,7 +3,9 @@ # Workload: one cold request plus ten reuses, with no discarded warmup. metadata: - performance_shard_weight: 2 + # Measured JIT verification: about 268 seconds for all seven pairs, + # including process startup; scheduling weights use 15-second units. + performance_shard_weight: 18 version: "1.0" description: "Range-first scalar values inside a period aggregate" isolated: true diff --git a/tests/sql/compatibility/tsql-native-frontend.yaml b/tests/sql/compatibility/tsql-native-frontend.yaml index 7c75ea75c..f30c741b6 100644 --- a/tests/sql/compatibility/tsql-native-frontend.yaml +++ b/tests/sql/compatibility/tsql-native-frontend.yaml @@ -285,6 +285,162 @@ test_cases: - name: invalid alter dimensions are rejected before schema mutation sql: ALTER TABLE [tsql_native_copy] ADD [invalid] DECIMAL(40,2) expect: {error: true} + - name: cast result types come from declarations including null operands + scm: | + (begin + (define types '("INT" "INTEGER" "SMALLINT" "TINYINT" "BIGINT" "FLOAT" "REAL" "BIT")) + (reduce (map types (lambda (type) (begin + (define info (sql_expr_info (list) (tsql_cast_expr nil type (list)))) + (and (equal? (sql_info_type info) type) + (nil? (sql_info_collation info)) + (nil? (eval (sql_info_formula info))))))) + (lambda (ok item) (and ok item)) true)) + expect: {result: true} + - name: integer cast emits only its conversion without a division marker + scm: | + (begin + (define info (sql_expr_info (list) (tsql_cast_expr "42" "SMALLINT" (list)))) + (and (equal? (sql_info_type info) "SMALLINT") + (expression_equal? (sql_info_formula info) (list tsql_cast_integer "42" -32768 32767)) + (equal? (eval (sql_info_formula info)) 42))) + expect: {result: true} + - name: bit cast emits only its conversion without boolean markers + scm: | + (begin + (define info (sql_expr_info (list) (tsql_cast_expr "1" "BIT" (list)))) + (and (equal? (sql_info_type info) "BIT") + (expression_equal? (sql_info_formula info) (list tsql_cast_bit "1")) + (equal? (eval (sql_info_formula info)) true))) + expect: {result: true} + - name: float cast emits only its conversion without type markers + scm: | + (begin + (define info (sql_expr_info (list) (tsql_cast_expr 1 "FLOAT" (list)))) + (and (equal? (sql_info_type info) "FLOAT") + (expression_equal? (sql_info_formula info) (list tsql_cast_float 1)) + (equal? (eval (sql_info_formula info)) 1))) + expect: {result: true} + - name: outer cast declaration determines nested conversion type + scm: | + (begin + (define info (sql_expr_info (list) + (tsql_cast_expr (tsql_cast_expr "42" "SMALLINT" (list)) "FLOAT" (list)))) + (and (equal? (sql_info_type info) "FLOAT") + (expression_equal? (sql_info_formula info) + (list tsql_cast_float (list tsql_cast_integer "42" -32768 32767))) + (equal? (eval (sql_info_formula info)) 42))) + expect: {result: true} + - name: case and coalesce merge declared cast types rather than helper contracts + scm: | + (begin + (define small (tsql_cast_expr "42" "SMALLINT" (list))) + (define absent (tsql_cast_expr nil "SMALLINT" (list))) + (and (equal? (sql_info_type (sql_expr_info (list) (list (quote if) false absent small))) "SMALLINT") + (equal? (sql_info_type (sql_expr_info (list) (list coalesceNil absent small))) "SMALLINT") + (equal? (sql_info_type (sql_expr_info (list) (list (quote +) small small))) "BIGINT"))) + expect: {result: true} + - name: conversions inside case and isnull execute through the shared compiler + sql: SELECT CASE WHEN 1=1 THEN CAST('12' AS SMALLINT) ELSE CAST(NULL AS SMALLINT) END AS choice, ISNULL(CAST(NULL AS INT),CAST('42' AS INT)) AS fallback + expect: {rows: 1, data: [{choice: 12, fallback: 42}]} + - name: conversions inside a derived source retain executable formulas + sql: SELECT [d].[value] FROM (SELECT CAST('42' AS INT) AS [value]) AS [d] + expect: {rows: 1, data: [{value: 42}]} + - name: conversions inside union branches retain executable formulas + sql: SELECT CAST('12' AS INT) AS value UNION ALL SELECT CONVERT(INT,'42') AS value + expect: {rows: 2, data: [{value: 12}, {value: 42}]} + - name: conversion inside aggregate input retains executable formula + sql: SELECT SUM(CAST([amount] AS INT)) AS total FROM [tsql_native] + expect: {rows: 1, data: [{total: 14}]} + - name: conversion inside scalar subquery retains executable formula + sql: SELECT (SELECT CAST('42' AS INT)) AS value + expect: {rows: 1, data: [{value: 42}]} + - name: conversion inside joined condition retains executable formula + sql: SELECT a.[id] FROM [tsql_native] AS a JOIN [tsql_native] AS b ON a.[id]=CAST(b.[id] AS INT) ORDER BY a.[id] + expect: {rows: 2, data: [{id: 1}, {id: 2}]} + - name: conversions in insert values use the normal native insert + sql: INSERT INTO [tsql_native_copy] ([id],[name]) VALUES (CAST('4' AS INT),N'cast value') + expect: {rows: 1} + - name: inserted converted value remains readable + sql: SELECT [id] FROM [tsql_native_copy] WHERE [name]=N'cast value' + expect: {rows: 1, data: [{id: 4}]} + - name: cast still checks unknown column references + sql: SELECT CAST([missing_column] AS INT) FROM [tsql_native] + expect: {error: true} + - name: unsupported cast target is an explicit frontend error + sql: SELECT CAST('42' AS VARBINARY(2)) + expect: {error: true} + - name: result metadata describes null casts and nested casts before any row + scm: | + (begin + (define session (newsession)) + (session "username" "root") + (session "schema" "memcp-tests") + (session "wire_metadata" true) + (define output (newsession)) + (tds_frontend "execute" session + (list "database" "memcp-tests" "parameters" (list) "declarations" "" + "text" "SELECT CAST(NULL AS SMALLINT) AS absent, CAST(CAST(1 AS INT) AS FLOAT) AS numeric_value, CAST(NULL AS BIT) AS flag WHERE 1=0") + (lambda (values) (output "row" true)) + (lambda (fields) (output "fields" fields)) nil nil) + (and (nil? (output "row")) + (equal? (map (output "fields") (lambda (field) (field "kind"))) '(38 109 104)))) + expect: {result: true} + - name: wire widths follow integer declarations while values stay native int64 + scm: | + (begin + (define session (newsession)) + (session "username" "root") + (session "schema" "memcp-tests") + (session "wire_metadata" true) + (define output (newsession)) + (tds_frontend "execute" session + (list "database" "memcp-tests" "parameters" (list) "declarations" "" + "text" "SELECT CAST('255' AS TINYINT) AS tiny_value, CAST('-32768' AS SMALLINT) AS small_value, CAST('2147483647' AS INT) AS int_value, CAST('9223372036854775807' AS BIGINT) AS big_value") + (lambda (values) (output "values" values)) + (lambda (fields) (output "fields" fields)) nil nil) + (and (equal? (map (output "fields") (lambda (field) (field "size"))) '(1 2 4 8)) + (equal? (map (output "fields") (lambda (field) (field "kind"))) '(38 38 38 38)) + (reduce (map (output "values") int?) (lambda (ok value) (and ok value)) true) + (expression_equal? (nth (output "values") 3) (tsql_parse_integer "9223372036854775807")))) + expect: {result: true} + - name: compiled query block records declared cast types with plain formulas + scm: | + (begin + (define expr (tsql_cast_expr nil "INT" (list))) + (define block (make_query_block "memcp-tests" (list) (list "value" expr) true nil nil nil nil nil nil nil nil)) + (define typed (sql_type_query_block block (list))) + (define result ((qassoc_get (qb_facts typed) (quote result-types) nil) "value")) + (and (equal? (sql_info_type result) "INT") + (expression_equal? ((qb_fields typed) "value") (list tsql_cast_integer nil -2147483648 2147483647)))) + expect: {result: true} + - name: conversion inside window input uses the existing window planner + sql: SELECT [id], SUM(CAST([amount] AS INT)) OVER () AS total FROM [tsql_native] ORDER BY [id] + expect: {rows: 2, data: [{id: 1, total: 14}, {id: 2, total: 14}]} + - name: conversion inside group keys and having uses the shared grouping planner + sql: SELECT CAST([amount] AS INT) AS value, COUNT(*) AS entries FROM [tsql_native] GROUP BY CAST([amount] AS INT) HAVING CAST([amount] AS INT)=7 + expect: {rows: 1, data: [{value: 7, entries: 2}]} + - name: conversions inside ordered expressions use the common order lowering + sql: SELECT [id] FROM [tsql_native] ORDER BY CAST([id] AS INT) DESC + expect: {rows: 2, data: [{id: 2}, {id: 1}]} + - name: valid conversion inside update uses the native dml plan + sql: UPDATE [tsql_native] SET [amount]=CAST('7' AS INT) WHERE [id]=CONVERT(INT,'1') + expect: {rows: 1} + - name: window cast result carries the frontend integer contract + scm: | + (begin + (define info (sql_expr_info (list) + (tsql_window_expr "SUM" (list (tsql_cast_expr "42" "SMALLINT" (list))) (list)))) + (and (equal? (sql_info_type info) "INT") + (expression_equal? (sql_info_formula info) + (list (quote window_func) "SUM" (list (list tsql_cast_integer "42" -32768 32767)) (list))))) + expect: {result: true} + - name: executable window plan contains no frontend type annotations + scm: | + (not (regexp_test + (string (parse_tsql "memcp-tests" + "SELECT id,SUM(CAST(amount AS INT)) OVER () AS total FROM tsql_native ORDER BY id" + nil nil nil)) "sql_type_expr")) + expect: {result: true} cleanup: - sql: DROP VIEW IF EXISTS [tsql_native_view] - sql: DROP TABLE IF EXISTS [tsql_native_copy] diff --git a/tests/sql/expressions/sql-type-fusion.yaml b/tests/sql/expressions/sql-type-fusion.yaml index 4f84d8c97..76c76ca19 100644 --- a/tests/sql/expressions/sql-type-fusion.yaml +++ b/tests/sql/expressions/sql-type-fusion.yaml @@ -61,6 +61,47 @@ test_cases: (if (reduce checks (lambda (acc c) (and acc c)) true) true (error (concat "type predicate mismatch: " (string checks))))) + - name: "declared expression contracts override the operand representation" + scm: | + (begin + (define integral (sql_expr_info (list) (list (quote sql_type_expr) 1 "FLOAT" nil))) + (define nullable (sql_expr_info (list) (list (quote sql_type_expr) nil "INT" nil))) + (if (and (equal? (sql_info_type integral) "FLOAT") + (equal? (sql_info_formula integral) 1) + (equal? (sql_info_type nullable) "INT") + (nil? (sql_info_formula nullable))) true + (error "SQL declaration must control the type independently of the formula value"))) + + - name: "declared text collation is compile-time information only" + scm: | + (begin + (define info (sql_expr_info (list) + (list (quote sql_type_expr) "x" "VARCHAR" (list "utf8mb4_bin" 0)))) + (if (and (equal? (sql_info_formula info) "x") + (equal? (sql_info_type info) "VARCHAR") + (equal? (sql_info_collation info) (list "utf8mb4_bin" 0))) true + (error "declared expression contract leaked into the executable formula"))) + + - name: "annotation-free logical structure has no replacement" + scm: | + (begin + (define expression (list (quote +) (list (quote get_column) "d" false "weight" false) 1)) + (if (and (nil? (sql_type_annotation_replacement expression)) + (expression_equal? (sql_type_lower_annotations expression) expression)) true + (error "annotation-free expressions must retain their original structure"))) + + - name: "annotation replacement distinguishes NULL from an unchanged node" + scm: | + (begin + (define contract (list (quote sql_type_expr) nil "INT" nil)) + (define quoted (list (quote quote) contract)) + (if (and (expression_equal? (sql_type_annotation_replacement contract) (list nil)) + (nil? (sql_type_lower_annotations contract)) + (expression_equal? (sql_type_lower_annotations (list (quote coalesceNil) contract 42)) + (list (quote coalesceNil) nil 42)) + (nil? (sql_type_annotation_replacement quoted))) true + (error "NULL replacement or quoted expression handling is incorrect"))) + - name: "literal typing" scm: | (begin