1mod compiler;
27pub mod tags;
28
29pub use compiler::column_default_sql;
30pub use compiler::compile;
31pub use compiler::compile_computed_in_type;
32pub use compiler::compile_constraint_expr;
33pub use compiler::compile_expr_in_type;
34pub use compiler::compile_expr_unaliased;
35pub use compiler::compile_inlined_default;
36pub use compiler::compile_scalar_default;
37pub use compiler::compile_scalar_default_typed;
38pub use compiler::compile_trigger_handler;
39pub use compiler::compile_with_config;
40pub use compiler::default_blocker;
41pub(crate) use compiler::infer_ir_type;
42pub use compiler::inlined_pointer_defaults;
43pub use compiler::pg_type_to_pyql;
44pub(crate) use compiler::types_compatible;
45pub use compiler::{GLOBALS_ARG, compile_fn_body, functions_needing_globals};
46pub use compiler::{RewriteAssignment, compile_rewrite_assignments};
47
48use crate::parse::ast::{BinOpKind, UnaryOpKind};
49use std::collections::HashMap;
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
62pub struct SessionConfig {
63 pub allow_user_specified_id: bool,
67}
68
69#[derive(Debug, Clone)]
72pub enum IrStmt {
73 Select(IrSelect),
77 PathSelect(IrPathSelect),
79 Insert(IrInsert),
80 Update(IrUpdate),
81 Delete(IrDelete),
82 For(IrFor),
84 Group(IrGroup),
86 FunctionSelect(IrFunctionSelect),
88 VectorSearch(IrVectorSearch),
90 FtsSearch(IrFtsSearch),
92 ScalarUnion(Vec<IrStmt>),
95}
96
97#[derive(Debug, Clone)]
100pub struct IrFor {
101 pub var_name: String,
102 pub iterator: IrForIterator,
103 pub body: Box<IrStmt>,
104 pub body_ctes: Vec<IrCteDef>,
108}
109
110#[derive(Debug, Clone)]
111pub enum IrForIterator {
112 Values { exprs: Vec<IrExpr>, pg_type: String },
114 Query {
119 stmt: Box<IrStmt>,
120 scalar: bool,
123 },
124 SetReturning { expr: IrExpr, pg_type: String },
128}
129
130#[derive(Debug, Clone)]
133pub struct IrGroup {
134 pub source: IrSource,
135 pub shape: Vec<IrShapePointer>,
137 pub keys: Vec<(String, IrExpr)>,
139 pub filter: Option<IrExpr>,
141 pub order_by: Vec<IrSort>,
143 pub offset: Option<IrExpr>,
147 pub limit: Option<IrExpr>,
148 pub output: IrGroupOutput,
149}
150
151#[derive(Debug, Clone)]
152pub struct IrGroupProjection {
153 pub pointers: Vec<IrShapePointer>,
154 pub order_by: Vec<IrSort>,
156 pub offset: Option<IrExpr>,
157 pub limit: Option<IrExpr>,
158}
159
160#[derive(Debug, Clone)]
164pub enum IrGroupOutput {
165 Groups,
166 Projection(Box<IrGroupProjection>),
170 Elements,
173}
174
175#[derive(Debug, Clone)]
180pub struct IrPathSelect {
181 pub root: IrSource,
182 pub joins: Vec<IrPathJoin>,
183 pub result: IrPathResult,
184 pub filter: Option<IrExpr>,
185 pub order_by: Vec<IrSort>,
186 pub offset: Option<IrExpr>,
187 pub limit: Option<IrExpr>,
188 pub distinct: bool,
189 pub poly_implementors: Vec<IrPolyImplementor>,
192}
193
194#[derive(Debug, Clone)]
195pub enum IrPathJoin {
196 Single {
198 source_alias: String,
199 fk_col: String,
200 target: IrSource,
201 },
202 Multi {
204 source_alias: String,
205 junction_alias: String,
206 join: IrMultiLinkJoin,
207 target: IrSource,
208 },
209 BacklinkSingle {
211 source_alias: String,
212 fk_col: String,
213 target: IrSource,
214 },
215 Function {
220 fn_module: String,
221 fn_name: String,
222 args: Vec<IrExpr>,
223 target: IrSource,
224 },
225 Lateral { inner: Box<IrPathSelect>, target: IrSource },
231 BacklinkMulti {
233 source_alias: String,
234 junction_alias: String,
235 junction_table: String,
236 module: String,
237 owner_col: String,
239 current_col: String,
241 target: IrSource,
242 },
243}
244
245#[derive(Debug, Clone)]
246pub enum IrPathResult {
247 Scalar(IrExpr, Option<TupleCastShape>),
255 Object {
257 alias: String,
258 type_name: String,
259 shape: Vec<IrShapePointer>,
260 },
261}
262
263#[derive(Debug, Clone)]
266pub enum IrFreeExpr {
267 Scalar(IrExpr),
269 FreeObject(Vec<(String, IrExpr)>),
271 NamedTupleRow(Vec<(String, IrExpr)>),
276 Tuple(Vec<IrExpr>),
278 AssertSet {
281 fn_name: String,
282 inner: Box<IrArraySource>,
283 message: Option<IrExpr>,
285 },
286 CtePassthrough(String),
288}
289
290#[derive(Debug, Clone)]
292pub enum IrArraySource {
293 Select(IrSelect),
295 ObjectFunction(Box<IrFunctionSelect>),
299 ObjectSelect(Box<IrSelect>),
303 PathSelect(Box<IrPathSelect>),
304 Group(Box<IrGroup>),
306 StmtColumn {
309 stmt: Box<IrStmt>,
310 column: String,
311 },
312 RawExpr {
314 source: IrSource,
315 poly_implementors: Vec<IrPolyImplementor>,
317 poly_columns: Vec<String>,
318 expr: IrExpr,
319 },
320}
321
322#[derive(Debug, Clone)]
325pub struct IrSelect {
326 pub rows: Vec<IrRowSource>,
330 pub filter: Option<IrExpr>,
333 pub order_by: Vec<IrSort>,
334 pub offset: Option<IrExpr>,
335 pub limit: Option<IrExpr>,
336 pub distinct: bool,
337 pub dml_source: Option<Box<IrStmt>>,
342 pub polymorphic: bool,
346 pub poly_implementors: Vec<IrPolyImplementor>,
348 pub poly_columns: Vec<String>,
350 pub lock: Option<IrLockClause>,
357}
358
359#[derive(Debug, Clone)]
360pub struct IrLockClause {
361 pub strength: IrLockStrength,
362 pub wait: IrLockWait,
363}
364
365#[derive(Debug, Clone)]
366pub enum IrLockStrength {
367 Update,
368 NoKeyUpdate,
369 Share,
370 KeyShare,
371}
372
373#[derive(Debug, Clone)]
374pub enum IrLockWait {
375 Block,
376 NoWait,
377 SkipLocked,
378}
379
380#[derive(Debug, Clone)]
386pub enum IrRowSource {
387 Bound {
388 source: IrSource,
389 shape: Vec<IrShapePointer>,
390 },
391 Free(IrFreeExpr),
392}
393
394impl IrSelect {
395 pub fn schema_bound(source: IrSource, shape: Vec<IrShapePointer>, filter: Option<IrExpr>) -> Self {
398 IrSelect {
399 rows: vec![IrRowSource::Bound { source, shape }],
400 filter,
401 order_by: vec![],
402 offset: None,
403 limit: None,
404 distinct: false,
405 dml_source: None,
406 polymorphic: false,
407 poly_implementors: vec![],
408 poly_columns: vec![],
409 lock: None,
410 }
411 }
412}
413
414#[derive(Debug, Clone)]
416pub struct IrPolyImplementor {
417 pub type_name: String,
419 pub table: String,
421 pub module: String,
423}
424
425#[derive(Debug, Clone)]
428pub struct IrSource {
429 pub type_name: String,
431 pub table: String,
433 pub alias: String,
435 pub poly: Option<IrPolyFanout>,
447}
448
449#[derive(Debug, Clone)]
452pub struct IrPolyFanout {
453 pub implementors: Vec<IrPolyImplementor>,
454 pub columns: Vec<String>,
455}
456
457#[derive(Debug, Clone)]
460pub struct IrScalarSetPointer {
461 pub alias: String,
462 pub source: IrSource,
463 pub poly_implementors: Vec<IrPolyImplementor>,
464 pub poly_columns: Vec<String>,
465 pub bool_expr: IrExpr,
466}
467
468#[derive(Debug, Clone)]
469pub enum IrShapePointer {
470 Scalar(IrScalarPointer),
471 SingleLink(IrSingleLinkPointer),
472 MultiLink(IrMultiLinkPointer),
473 Computed(IrComputedPointer),
474 ScalarSet(IrScalarSetPointer),
475 Asserted(Box<IrAssertedPointer>),
480}
481
482#[derive(Debug, Clone)]
483pub struct IrAssertedPointer {
484 pub fn_name: String,
489 pub inner: IrShapePointer,
490 pub check: Option<IrShapePointer>,
494 pub message: Option<IrExpr>,
496}
497
498impl IrShapePointer {
499 pub fn alias(&self) -> &str {
501 match self {
502 IrShapePointer::Scalar(p) => &p.alias,
503 IrShapePointer::SingleLink(p) => &p.alias,
504 IrShapePointer::MultiLink(p) => &p.alias,
505 IrShapePointer::Computed(p) => &p.alias,
506 IrShapePointer::ScalarSet(p) => &p.alias,
507 IrShapePointer::Asserted(p) => p.inner.alias(),
508 }
509 }
510
511 pub fn is_object_pointer(&self) -> bool {
515 match self {
516 IrShapePointer::SingleLink(_) | IrShapePointer::MultiLink(_) => true,
517 IrShapePointer::Asserted(a) => a.inner.is_object_pointer(),
518 IrShapePointer::Computed(c) => match &c.expr {
519 IrExpr::ObjectSubquery(_) | IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. } => true,
520 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
524 IrArraySource::ObjectFunction(_) | IrArraySource::ObjectSelect(_) => true,
525 IrArraySource::PathSelect(ps) => matches!(ps.result, IrPathResult::Object { .. }),
526 _ => false,
527 },
528 _ => false,
529 },
530 IrShapePointer::Scalar(_) | IrShapePointer::ScalarSet(_) => false,
531 }
532 }
533}
534
535#[derive(Debug, Clone)]
537pub struct IrScalarPointer {
538 pub alias: String,
540 pub column: String,
542 pub pg_type: String,
544 pub tuple_shape: Option<TupleCastShape>,
548 pub marker_offset: Option<usize>,
553 pub implicit_id: bool,
558}
559
560#[derive(Debug, Clone)]
565pub struct IrSingleLinkPointer {
566 pub alias: String,
567 pub correlation: IrSingleLinkCorrelation,
568 pub subquery: IrSelect,
570 pub link_properties: Vec<IrLinkProp>,
574 pub marker_offset: Option<usize>,
576}
577
578#[derive(Debug, Clone)]
579pub enum IrSingleLinkCorrelation {
580 Fk {
582 fk_column: String,
584 target_pk: String,
586 },
587 Junction {
590 join: IrMultiLinkJoin,
591 target_pk: String,
593 },
594}
595
596#[derive(Debug, Clone)]
599pub struct IrMultiLinkPointer {
600 pub alias: String,
601 pub join: IrMultiLinkJoin,
603 pub subquery: IrSelect,
605 pub link_properties: Vec<IrLinkProp>,
607 pub marker_offset: Option<usize>,
609 pub single: bool,
612}
613
614#[derive(Debug, Clone)]
616pub struct IrLinkProp {
617 pub name: String,
619}
620
621#[derive(Debug, Clone)]
622pub enum IrMultiLinkJoin {
623 Standard {
626 junction_table: String,
628 module: String,
630 },
631 Through {
633 junction_table: String,
635 module: String,
637 source_col: String,
639 target_col: String,
641 },
642 BacklinkFk {
648 fk_col: String,
650 },
651 BacklinkJunction {
656 junction_table: String,
657 module: String,
658 owner_col: String,
660 current_col: String,
662 },
663}
664
665#[derive(Debug, Clone)]
667pub struct IrComputedPointer {
668 pub alias: String,
669 pub expr: IrExpr,
670 pub marker_offset: Option<usize>,
672}
673
674#[derive(Debug, Clone)]
679pub struct VectorEnqueueInfo {
680 pub type_name: String,
682 pub index_name: Option<String>,
684}
685
686#[derive(Debug, Clone)]
690pub struct SearchEnqueueInfo {
691 pub type_name: String,
692 pub index_name: Option<String>,
693 pub operation: &'static str,
695 pub backend: crate::schema::SearchBackend,
698}
699
700#[derive(Debug, Clone)]
703pub struct IrInsert {
704 pub guard: Option<IrExpr>,
709 pub target: IrSource,
710 pub assignments: Vec<(String, IrExpr)>,
712 pub unless_conflict: Option<IrConflict>,
713 pub rewrites: Vec<IrRewrite>,
715 pub returning: Vec<IrShapePointer>,
717 pub enqueue_vector: Vec<VectorEnqueueInfo>,
719 pub enqueue_search: Vec<SearchEnqueueInfo>,
721 pub multi_link_appends: Vec<IrMultiLinkMutation>,
726 pub id_default_sql: Option<String>,
731 pub nested_ctes: Vec<IrCteDef>,
740}
741
742#[derive(Debug, Clone)]
743pub struct IrConflict {
744 pub on: Option<IrExpr>,
746 pub do_update: Option<Vec<(String, IrExpr)>>,
748 pub do_update_where: Option<IrExpr>,
754}
755
756#[derive(Debug, Clone)]
759pub struct IrUpdate {
760 pub target: IrSource,
761 pub filter: Option<IrExpr>,
762 pub assignments: Vec<(String, IrExpr)>,
763 pub rewrites: Vec<IrRewrite>,
765 pub returning: Vec<IrShapePointer>,
766 pub enqueue_vector: Vec<VectorEnqueueInfo>,
768 pub enqueue_search: Vec<SearchEnqueueInfo>,
770 pub poly_implementors: Vec<IrPolyImplementor>,
772 pub poly_columns: Vec<String>,
778 pub multi_link_clears: Vec<IrMultiLinkClear>,
780 pub multi_link_replaces: Vec<IrMultiLinkMutation>,
782 pub multi_link_appends: Vec<IrMultiLinkMutation>,
784 pub multi_link_removals: Vec<IrMultiLinkMutation>,
786 pub nested_ctes: Vec<IrCteDef>,
793}
794
795#[derive(Debug, Clone)]
796pub struct IrMultiLinkClear {
797 pub junction_table: String,
798 pub module: String,
799 pub source_col: String,
801}
802
803#[derive(Debug, Clone)]
805pub struct IrMultiLinkMutation {
806 pub junction_table: String,
807 pub module: String,
808 pub source_col: String,
810 pub target_col: String,
812 pub values: IrMultiLinkValues,
815 pub single: bool,
820}
821
822#[derive(Debug, Clone)]
830pub struct IrMultiLinkValues {
831 pub source: IrMultiLinkValueSource,
832 pub link_props: Vec<(String, IrExpr)>,
833}
834
835#[derive(Debug, Clone)]
837pub enum IrMultiLinkValueSource {
838 CteRef(String),
840 Select(Box<IrSelect>),
842 PathSelect(Box<IrPathSelect>),
844 Function(Box<IrFunctionSelect>),
847 Union(Box<IrMultiLinkValues>, Box<IrMultiLinkValues>),
850 Asserted {
855 fn_name: String,
856 inner: Box<IrMultiLinkValues>,
857 message: Option<IrExpr>,
859 },
860}
861
862#[derive(Debug, Clone)]
865pub struct IrDelete {
866 pub target: IrSource,
867 pub filter: Option<IrExpr>,
868 pub returning: Vec<IrShapePointer>,
869 pub poly_implementors: Vec<IrPolyImplementor>,
871 pub poly_columns: Vec<String>,
873 pub enqueue_search: Vec<SearchEnqueueInfo>,
875}
876
877#[derive(Debug, Clone, PartialEq, Eq)]
883pub enum SetOpMode {
884 Array,
885 Exists,
886 Aggregate(String),
887}
888
889#[derive(Debug, Clone, Copy, PartialEq, Eq)]
890pub enum SetOpKind {
891 Intersect,
892 Except,
893}
894
895impl SetOpKind {
896 pub fn sql(self) -> &'static str {
897 match self {
898 Self::Intersect => "INTERSECT",
899 Self::Except => "EXCEPT",
900 }
901 }
902}
903
904#[derive(Debug, Clone)]
905pub enum IrExpr {
906 ColumnRef {
908 alias: String,
909 column: String,
910 pg_type: String,
911 },
912 Param {
914 index: usize,
915 },
916 Literal(IrLiteral),
917 BinOp(Box<IrBinOp>),
918 UnaryOp(Box<IrUnaryOp>),
919 FunctionCall(IrFunctionCall),
920 TypeCast(Box<IrTypeCast>),
921 IfElse(Box<IrIfElse>),
922 Subquery(Box<IrSelect>),
924 Array(Vec<IrExpr>),
926 Null,
928 AggOverSet {
931 fn_name: String,
932 schema: Option<String>,
933 elems: Vec<IrExpr>,
934 },
935 AggOverQuery {
938 fn_name: String,
939 inner: Box<IrSelect>,
940 },
941 AggOverCte {
947 fn_name: String,
948 cte: String,
949 column: Option<String>,
950 },
951 ExistsOverCte {
957 cte: String,
958 column: Option<String>,
959 },
960 SetOp {
964 op: SetOpKind,
965 left: Box<IrExpr>,
966 right: Box<IrExpr>,
967 mode: SetOpMode,
968 },
969 CteRef {
973 name: String,
974 scalar: bool,
975 pg_type: Option<String>,
980 },
981 CteFieldRef {
988 name: String,
989 field: String,
990 pg_type: Option<String>,
993 },
994 ForVar {
997 name: String,
998 pg_type: Option<String>,
1002 },
1003 ArrayFromSelect(Box<IrArraySource>),
1006 ScalarSubquery(Box<IrSelect>),
1011 EnumLiteral {
1013 pg_type: String,
1014 variant: String,
1015 },
1016 NamedTuple {
1024 fields: Vec<(String, IrExpr)>,
1025 is_free_object: bool,
1026 },
1027 ObjectPathUnion {
1032 branches: Vec<IrPathSelect>,
1033 limit: Option<Box<IrExpr>>,
1034 multi: bool,
1038 },
1039 ObjectPathSubquery(Box<IrPathSelect>),
1044 ObjectSubquery(Box<IrSelect>),
1050 Tuple(Vec<IrExpr>),
1052 GlobalParam {
1054 index: usize,
1055 pg_type: String,
1056 },
1057 GlobalRef {
1059 cte_name: String,
1060 },
1061 Subscript {
1063 expr: Box<IrExpr>,
1064 index: Box<IrExpr>,
1065 is_array: bool,
1066 },
1067 JsonbField {
1069 expr: Box<IrExpr>,
1070 field: String,
1071 },
1072 JsonbIndex {
1076 expr: Box<IrExpr>,
1077 index: usize,
1078 },
1079 Slice {
1081 expr: Box<IrExpr>,
1082 lower: Option<Box<IrExpr>>,
1083 upper: Option<Box<IrExpr>>,
1084 is_array: bool,
1085 },
1086 FnSubquery(Box<IrFunctionSelect>),
1092 PathSubquery(Box<IrPathSelect>),
1095 FnParam {
1098 name: String,
1099 pg_type: String,
1100 },
1101 RawSql(String),
1111}
1112
1113#[derive(Debug, Clone)]
1123pub struct IrVectorSearch {
1124 pub source: IrSource,
1126 pub vector_col: String,
1128 pub distance_op: &'static str,
1130 pub query_expr: IrExpr,
1133 pub object_shape: Vec<IrShapePointer>,
1136 pub filter: Option<IrExpr>,
1137 pub order_by_distance: Option<IrSortDir>,
1140 pub offset: Option<IrExpr>,
1141 pub limit: Option<IrExpr>,
1142 pub inference_query_param_name: Option<String>,
1145 pub inference_query_literal: Option<String>,
1147 pub inference_model: Option<String>,
1149 pub inference_type_name: Option<String>,
1151 pub inference_index_name: Option<Option<String>>,
1153}
1154
1155#[derive(Debug, Clone)]
1162pub struct IrFtsSearch {
1163 pub source: IrSource,
1165 pub backend: crate::schema::SearchBackend,
1167 pub search_col: String,
1169 pub tsquery_fn: &'static str,
1171 pub query_expr: IrExpr,
1173 pub object_shape: Vec<IrShapePointer>,
1175 pub filter: Option<IrExpr>,
1176 pub order_by_rank: Option<IrSortDir>,
1177 pub offset: Option<IrExpr>,
1178 pub limit: Option<IrExpr>,
1179 pub deferred_index_name: Option<String>,
1181 pub deferred_query_param_name: Option<String>,
1183 pub deferred_query_literal: Option<String>,
1185 pub deferred_ids_param: Option<usize>,
1187 pub deferred_scores_param: Option<usize>,
1189}
1190
1191#[derive(Debug, Clone)]
1196pub struct IrFunctionSelect {
1197 pub fn_module: String,
1198 pub fn_name: String,
1199 pub fn_args: Vec<IrExpr>,
1200 pub alias: String,
1202 pub type_name: String,
1204 pub polymorphic: bool,
1206 pub poly_implementors: Vec<IrPolyImplementor>,
1208 pub poly_columns: Vec<String>,
1210 pub shape: Vec<IrShapePointer>,
1211 pub filter: Option<IrExpr>,
1212 pub order_by: Vec<IrSort>,
1213 pub offset: Option<IrExpr>,
1214 pub limit: Option<IrExpr>,
1215 pub distinct: bool,
1216}
1217
1218#[derive(Debug, Clone)]
1219pub struct IrBinOp {
1220 pub left: IrExpr,
1221 pub op: BinOpKind,
1222 pub right: IrExpr,
1223}
1224
1225#[derive(Debug, Clone)]
1226pub struct IrUnaryOp {
1227 pub op: UnaryOpKind,
1228 pub operand: IrExpr,
1229}
1230
1231#[derive(Debug, Clone)]
1232pub struct IrFunctionCall {
1233 pub return_pg_type: Option<String>,
1247 pub schema: Option<String>,
1248 pub name: String,
1249 pub args: Vec<IrExpr>,
1250 pub sql_template: Option<String>,
1253}
1254
1255#[derive(Debug, Clone)]
1256pub struct IrTypeCast {
1257 pub expr: IrExpr,
1258 pub pg_type: String,
1260 pub tuple_shape: Option<TupleCastShape>,
1265}
1266
1267#[derive(Debug, Clone)]
1268pub struct TupleCastShape {
1269 pub type_name: Option<String>,
1272 pub members: Vec<crate::query::JsonMember>,
1273}
1274
1275#[derive(Debug, Clone)]
1276pub struct IrIfElse {
1277 pub condition: IrExpr,
1278 pub if_: IrExpr,
1279 pub else_: IrExpr,
1280}
1281
1282#[derive(Debug, Clone)]
1283pub enum IrLiteral {
1284 Str(String),
1285 Int(i64),
1286 Float(f64),
1287 Bool(bool),
1288}
1289
1290#[derive(Debug, Clone)]
1293pub struct IrSort {
1294 pub expr: IrExpr,
1295 pub direction: IrSortDir,
1296 pub nulls: IrNulls,
1297}
1298
1299#[derive(Debug, Clone)]
1300pub enum IrSortDir {
1301 Asc,
1302 Desc,
1303}
1304
1305#[derive(Debug, Clone)]
1306pub enum IrNulls {
1307 First,
1308 Last,
1309}
1310
1311#[derive(Debug, Clone)]
1316pub struct IrRewrite {
1317 pub column: String,
1319 pub expr: IrExpr,
1324}
1325
1326#[derive(Debug, Clone)]
1328pub struct IrCteDef {
1329 pub name: String,
1330 pub stmt: IrStmt,
1331 pub type_name: String,
1334 pub correlated_to: Option<String>,
1339}
1340
1341#[derive(Debug, Clone)]
1343pub struct IrSessionGlobalCte {
1344 pub cte_name: String,
1345 pub qualified_name: String,
1346 pub param_index: usize,
1347 pub pg_type: String,
1348}
1349
1350#[derive(Debug, Clone)]
1352pub struct IrComputedGlobalCte {
1353 pub cte_name: String,
1354 pub qualified_name: String,
1355 pub stmt: IrStmt,
1356}
1357
1358#[derive(Debug, Clone)]
1359pub enum IrGlobalCte {
1360 Session(IrSessionGlobalCte),
1361 Computed(Box<IrComputedGlobalCte>),
1365}
1366
1367impl IrGlobalCte {
1368 pub fn cte_name(&self) -> &str {
1369 match self {
1370 Self::Session(s) => &s.cte_name,
1371 Self::Computed(c) => &c.cte_name,
1372 }
1373 }
1374}
1375
1376pub struct IrOutput {
1380 pub stmt: IrStmt,
1381 pub params: Vec<String>,
1384 pub ctes: Vec<IrCteDef>,
1386 pub global_ctes: Vec<IrGlobalCte>,
1388 pub warnings: Vec<String>,
1390 pub uses_globals_arg: bool,
1394 pub subtype_fanouts: HashMap<(String, String), IrPolyFanout>,
1398}
1399
1400pub type QualifiedTable = (String, String);
1402
1403const INHERITED_JUNCTION: &str = "@inherited:";
1407
1408pub fn inherited_junction(tables: &[QualifiedTable], columns: &[String]) -> String {
1411 let tables = tables
1412 .iter()
1413 .map(|(module, table)| format!("{module}\u{1f}{table}"))
1414 .collect::<Vec<_>>()
1415 .join("\u{1e}");
1416 format!("{INHERITED_JUNCTION}{tables}\u{1d}{}", columns.join("\u{1f}"))
1417}
1418
1419pub fn parse_inherited_junction(name: &str) -> Option<(Vec<QualifiedTable>, Vec<String>)> {
1422 let (tables, columns) = name.strip_prefix(INHERITED_JUNCTION)?.split_once('\u{1d}')?;
1423 let tables = tables
1424 .split('\u{1e}')
1425 .filter_map(|entry| entry.split_once('\u{1f}'))
1426 .map(|(module, table)| (module.to_string(), table.to_string()))
1427 .collect();
1428 Some((tables, columns.split('\u{1f}').map(str::to_string).collect()))
1429}
1430
1431#[cfg(test)]
1432mod tests {
1433 use super::*;
1434 #[allow(unused_imports)]
1435 use super::{IrFreeExpr, IrLiteral};
1436 use crate::parse;
1437 use crate::schema::{
1438 ChannelDescriptor, ChannelPayload, ComputedDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
1439 PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
1440 };
1441
1442 fn make_schema() -> SchemaDescriptor {
1443 SchemaDescriptor {
1444 types: vec![
1445 TypeDescriptor {
1446 name: "Person".into(),
1447 module: "default".into(),
1448 table: "person".into(),
1449 abstract_: false,
1450 materialized: false,
1451 description: None,
1452 parents: vec![],
1453 interfaces: vec![],
1454 bases: vec![],
1455 properties: vec![
1456 PropertyDescriptor {
1457 name: "id".into(),
1458 pg_type: "uuid".into(),
1459 nullable: false,
1460 default_sql: Some("uuidv7()".into()),
1461 default_pyql: None,
1462 description: None,
1463 check_constraints: vec![],
1464 is_exclusive: true,
1465 is_pk: true,
1466 is_readonly: true,
1467 rewrites: vec![],
1468 tuple_members: None,
1469 column_type: None,
1470 },
1471 PropertyDescriptor {
1472 name: "name".into(),
1473 pg_type: "text".into(),
1474 nullable: false,
1475 default_sql: None,
1476 default_pyql: None,
1477 description: None,
1478 check_constraints: vec![],
1479 is_exclusive: false,
1480 is_pk: false,
1481 is_readonly: false,
1482 rewrites: vec![],
1483 tuple_members: None,
1484 column_type: None,
1485 },
1486 PropertyDescriptor {
1487 name: "age".into(),
1488 pg_type: "int8".into(),
1489 nullable: true,
1490 default_sql: None,
1491 default_pyql: None,
1492 description: None,
1493 check_constraints: vec![],
1494 is_exclusive: false,
1495 is_pk: false,
1496 is_readonly: false,
1497 rewrites: vec![],
1498 tuple_members: None,
1499 column_type: None,
1500 },
1501 ],
1502 links: vec![LinkDescriptor {
1503 name: "company".into(),
1504 target: "default::Company".into(),
1505 nullable: true,
1506 through: None,
1507 description: None,
1508 default_pyql: None,
1509 is_exclusive: false,
1510 is_readonly: false,
1511 rewrites: vec![],
1512 on_delete: vec![],
1513 }],
1514 multilinks: vec![MultiLinkDescriptor {
1515 name: "posts".into(),
1516 target: "default::Post".into(),
1517 through: None,
1518 nullable: false,
1519 description: None,
1520 default_pyql: None,
1521 on_delete: vec![],
1522 is_exclusive: false,
1523 }],
1524 computed: vec![],
1525 constraints: vec![],
1526 indexes: vec![],
1527 partition: None,
1528 vector_indexes: vec![],
1529 search_indexes: vec![],
1530 triggers: vec![],
1531 junction: false,
1532 signals: vec![],
1533 },
1534 TypeDescriptor {
1535 name: "Company".into(),
1536 module: "default".into(),
1537 table: "company".into(),
1538 abstract_: false,
1539 materialized: false,
1540 description: None,
1541 parents: vec![],
1542 interfaces: vec![],
1543 bases: vec![],
1544 properties: vec![PropertyDescriptor {
1545 name: "name".into(),
1546 pg_type: "text".into(),
1547 nullable: false,
1548 default_sql: None,
1549 default_pyql: None,
1550 description: None,
1551 check_constraints: vec![],
1552 is_exclusive: false,
1553 is_pk: false,
1554 is_readonly: false,
1555 rewrites: vec![],
1556 tuple_members: None,
1557 column_type: None,
1558 }],
1559 links: vec![],
1560 multilinks: vec![],
1561 computed: vec![],
1562 constraints: vec![],
1563 indexes: vec![],
1564 partition: None,
1565 vector_indexes: vec![],
1566 search_indexes: vec![],
1567 triggers: vec![],
1568 junction: false,
1569 signals: vec![],
1570 },
1571 TypeDescriptor {
1572 name: "Post".into(),
1573 module: "default".into(),
1574 table: "post".into(),
1575 abstract_: false,
1576 materialized: false,
1577 description: None,
1578 parents: vec![],
1579 interfaces: vec![],
1580 bases: vec![],
1581 properties: vec![PropertyDescriptor {
1582 name: "title".into(),
1583 pg_type: "text".into(),
1584 nullable: false,
1585 default_sql: None,
1586 default_pyql: None,
1587 description: None,
1588 check_constraints: vec![],
1589 is_exclusive: false,
1590 is_pk: false,
1591 is_readonly: false,
1592 rewrites: vec![],
1593 tuple_members: None,
1594 column_type: None,
1595 }],
1596 links: vec![],
1597 multilinks: vec![],
1598 computed: vec![],
1599 constraints: vec![],
1600 indexes: vec![],
1601 partition: None,
1602 vector_indexes: vec![],
1603 search_indexes: vec![],
1604 triggers: vec![],
1605 junction: false,
1606 signals: vec![],
1607 },
1608 ],
1609 scalars: vec![],
1610 enums: vec![],
1611 named_tuples: vec![],
1612 globals: vec![],
1613 functions: vec![],
1614 aliases: vec![],
1615 channels: vec![],
1616 ..Default::default()
1617 }
1618 }
1619
1620 fn compile(query: &str) -> IrOutput {
1621 let schema = make_schema();
1622 let ast = parse::parse(query).expect("parse failed");
1623 super::compile(&ast, &schema).expect("IR compile failed")
1624 }
1625
1626 fn bound(sel: &IrSelect) -> (&IrSource, &[IrShapePointer]) {
1630 match sel.rows.as_slice() {
1631 [IrRowSource::Bound { source, shape }] => (source, shape),
1632 _ => panic!("expected a single schema-bound row"),
1633 }
1634 }
1635
1636 fn free_items(sel: &IrSelect) -> Vec<&IrFreeExpr> {
1639 sel.rows
1640 .iter()
1641 .map(|r| match r {
1642 IrRowSource::Free(item) => item,
1643 IrRowSource::Bound { .. } => panic!("expected a free row"),
1644 })
1645 .collect()
1646 }
1647
1648 #[test]
1649 fn test_select_resolves_source() {
1650 let ir = compile("SELECT Person { name, age }");
1651 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1652 let (source, shape) = bound(&sel);
1653 assert_eq!(source.table, "person");
1654 assert_eq!(source.type_name, "default::Person");
1655 assert_eq!(shape.len(), 3);
1657 assert!(matches!(&shape[0], IrShapePointer::Scalar(p) if p.alias == "id" && p.implicit_id));
1658 assert!(matches!(shape[1], IrShapePointer::Scalar(_)));
1659 }
1660
1661 #[test]
1662 fn test_select_filter_param_ordering() {
1663 let ir = compile("SELECT Person { name } FILTER .name = $name AND .age > $min_age");
1664 assert_eq!(ir.params, vec!["name", "min_age"]);
1665 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1666 assert!(sel.filter.is_some());
1667 }
1668
1669 #[test]
1670 fn test_select_single_link() {
1671 let ir = compile("SELECT Person { name, company { name } }");
1672 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1673 let (_, shape) = bound(&sel);
1674 assert_eq!(shape.len(), 3);
1675 let IrShapePointer::SingleLink(link) = &shape[2] else {
1676 panic!("expected SingleLink")
1677 };
1678 assert_eq!(link.alias, "company");
1679 let IrSingleLinkCorrelation::Fk { fk_column, .. } = &link.correlation else {
1680 panic!("expected Fk correlation")
1681 };
1682 assert_eq!(fk_column, "company_id");
1683 assert_eq!(bound(&link.subquery).0.table, "company");
1684 }
1685
1686 #[test]
1687 fn test_select_multi_link() {
1688 let ir = compile("SELECT Person { name, posts { title } }");
1689 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1690 let (_, shape) = bound(&sel);
1691 let IrShapePointer::MultiLink(ml) = &shape[2] else {
1692 panic!("expected MultiLink")
1693 };
1694 assert_eq!(ml.alias, "posts");
1695 assert_eq!(bound(&ml.subquery).0.table, "post");
1696 let IrMultiLinkJoin::Standard { junction_table, .. } = &ml.join else {
1697 panic!()
1698 };
1699 assert_eq!(junction_table, "person.posts");
1700 }
1701
1702 #[test]
1703 fn test_select_no_shape_returns_id_only() {
1704 let ir = compile("SELECT Person");
1705 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1706 let (_, shape) = bound(&sel);
1708 assert_eq!(shape.len(), 1);
1709 let IrShapePointer::Scalar(f) = &shape[0] else { panic!() };
1710 assert_eq!(f.alias, "id");
1711 }
1712
1713 #[test]
1714 fn test_free_select_set_literal() {
1715 let schema = make_schema();
1716 let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
1717 let ir = super::compile(&ast, &schema).unwrap();
1718 let IrStmt::Select(sel) = ir.stmt else {
1719 panic!("expected Select")
1720 };
1721 let items = free_items(&sel);
1722 assert_eq!(items.len(), 3);
1723 assert!(matches!(
1724 items[0],
1725 IrFreeExpr::Scalar(IrExpr::Literal(IrLiteral::Int(1)))
1726 ));
1727 }
1728
1729 #[test]
1730 fn test_free_select_free_object() {
1731 let schema = make_schema();
1732 let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
1733 let ir = super::compile(&ast, &schema).unwrap();
1734 let IrStmt::Select(sel) = ir.stmt else {
1735 panic!("expected Select")
1736 };
1737 let items = free_items(&sel);
1738 assert_eq!(items.len(), 1);
1739 let IrFreeExpr::FreeObject(fields) = &items[0] else {
1740 panic!("expected FreeObject")
1741 };
1742 assert_eq!(fields.len(), 2);
1743 assert_eq!(fields[0].0, "foo");
1744 assert_eq!(fields[1].0, "n");
1745 }
1746
1747 #[test]
1748 fn test_free_select_tuple() {
1749 let schema = make_schema();
1750 let ast = parse::parse("SELECT (1, 'hello')").unwrap();
1751 let ir = super::compile(&ast, &schema).unwrap();
1752 let IrStmt::Select(sel) = ir.stmt else {
1753 panic!("expected Select")
1754 };
1755 let items = free_items(&sel);
1756 assert_eq!(items.len(), 1);
1757 assert!(matches!(items[0], IrFreeExpr::Tuple(_)));
1758 }
1759
1760 #[test]
1761 fn test_free_select_scalar_literal() {
1762 let schema = make_schema();
1763 let ast = parse::parse("SELECT 42").unwrap();
1764 let ir = super::compile(&ast, &schema).unwrap();
1765 let IrStmt::Select(sel) = ir.stmt else {
1766 panic!("expected Select")
1767 };
1768 let items = free_items(&sel);
1769 assert_eq!(items.len(), 1);
1770 assert!(matches!(
1771 items[0],
1772 IrFreeExpr::Scalar(IrExpr::Literal(IrLiteral::Int(42)))
1773 ));
1774 }
1775
1776 #[test]
1777 fn test_free_select_function_call() {
1778 let schema = make_schema();
1779 let ast = parse::parse("SELECT str_lower('HELLO')").unwrap();
1780 let ir = super::compile(&ast, &schema).unwrap();
1781 let IrStmt::Select(sel) = ir.stmt else {
1782 panic!("expected Select")
1783 };
1784 let items = free_items(&sel);
1785 assert!(matches!(items[0], IrFreeExpr::Scalar(IrExpr::FunctionCall(_))));
1786 }
1787
1788 #[test]
1789 fn test_free_select_rejects_dot_path() {
1790 let schema = make_schema();
1791 let ast = parse::parse("SELECT {.name}").unwrap();
1792 assert!(super::compile(&ast, &schema).is_err());
1793 }
1794
1795 #[test]
1796 fn test_type_error_uuid_eq_str() {
1797 let schema = make_schema();
1798 let ast = parse::parse("SELECT Person FILTER .id = 'not-a-uuid'").unwrap();
1799 let err = super::compile(&ast, &schema).err().expect("expected type error");
1800 let msg = err.to_string();
1801 assert!(
1802 msg.contains("std::uuid") && msg.contains("std::str"),
1803 "unexpected: {msg}"
1804 );
1805 }
1806
1807 #[test]
1808 fn test_type_error_str_eq_int() {
1809 let schema = make_schema();
1810 let ast = parse::parse("SELECT Person FILTER .name = 42").unwrap();
1811 let err = super::compile(&ast, &schema).err().expect("expected type error");
1812 let msg = err.to_string();
1813 assert!(
1814 msg.contains("std::str") && msg.contains("std::int64"),
1815 "unexpected: {msg}"
1816 );
1817 }
1818
1819 #[test]
1820 fn test_int_literal_compatible_with_all_int_columns() {
1821 let schema = make_schema();
1823 let ast = parse::parse("SELECT Person FILTER .age = 30").unwrap();
1824 assert!(super::compile(&ast, &schema).is_ok());
1825 }
1826
1827 #[test]
1828 fn test_cast_int16_compatible_with_int8_column() {
1829 let schema = make_schema();
1830 let ast = parse::parse("SELECT Person FILTER .age = <int16>30").unwrap();
1831 assert!(super::compile(&ast, &schema).is_ok());
1832 }
1833
1834 #[test]
1835 fn test_unknown_type_error() {
1836 let schema = make_schema();
1837 let ast = parse::parse("SELECT Ghost { name }").unwrap();
1838 assert!(super::compile(&ast, &schema).is_err());
1839 }
1840
1841 #[test]
1842 fn test_nested_dml_link_value_combines_with_multilink_mutation_in_the_same_update() {
1843 let schema = make_schema();
1853 let ast = parse::parse(
1854 "UPDATE Person FILTER .id = $id SET { \
1855 company := (select (insert Company { name := 'Acme' }) { id }), \
1856 posts += (SELECT Post FILTER .title = $t) \
1857 }",
1858 )
1859 .unwrap();
1860 let ir = super::compile(&ast, &schema).unwrap();
1861 let IrStmt::Update(upd) = ir.stmt else {
1862 panic!("expected Update")
1863 };
1864 assert_eq!(upd.nested_ctes.len(), 1);
1865 assert_eq!(upd.multi_link_appends.len(), 1);
1866 }
1867
1868 #[test]
1869 fn test_unknown_pointer_error() {
1870 let schema = make_schema();
1871 let ast = parse::parse("SELECT Person { nonexistent }").unwrap();
1872 assert!(super::compile(&ast, &schema).is_err());
1873 }
1874
1875 #[test]
1876 fn test_insert_compiles_assignments() {
1877 let ir = compile("INSERT Person { name := 'Alice', age := 30 }");
1878 let IrStmt::Insert(ins) = ir.stmt else { panic!() };
1879 assert_eq!(ins.target.table, "person");
1880 assert_eq!(ins.assignments.len(), 2);
1881 assert_eq!(ins.assignments[0].0, "name");
1882 assert_eq!(ins.assignments[1].0, "age");
1883 }
1884
1885 #[test]
1886 fn test_delete_compiles_filter() {
1887 let ir = compile("DELETE Person FILTER .name = $name");
1888 let IrStmt::Delete(del) = ir.stmt else { panic!() };
1889 assert!(del.filter.is_some());
1890 assert_eq!(ir.params, vec!["name"]);
1891 }
1892
1893 fn make_schema_with_computed() -> SchemaDescriptor {
1894 let mut schema = make_schema();
1895 schema.types[0].computed.push(ComputedDescriptor {
1897 name: "upper_name".into(),
1898 expression: "str_upper(.name)".into(),
1899 return_type: Some("text".into()),
1900 link_target: None,
1901 link_multi: false,
1902 });
1903 schema
1904 }
1905
1906 #[test]
1907 fn test_computed_pointer_in_shape() {
1908 let schema = make_schema_with_computed();
1909 let ast = parse::parse("SELECT Person { upper_name }").unwrap();
1910 let ir = super::compile(&ast, &schema).expect("IR compile failed");
1911 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1912 let (_, shape) = bound(&sel);
1914 assert!(
1915 shape
1916 .iter()
1917 .any(|f| matches!(f, IrShapePointer::Computed(c) if c.alias == "upper_name"))
1918 );
1919 }
1920
1921 #[test]
1922 fn test_computed_pointer_in_expression_context() {
1923 let schema = make_schema_with_computed();
1924 let ast = parse::parse("SELECT Person { x := str_lower(.upper_name) }").unwrap();
1925 let ir = super::compile(&ast, &schema).expect("IR compile failed");
1926 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1927 let (_, shape) = bound(&sel);
1928 assert!(
1929 shape
1930 .iter()
1931 .any(|f| matches!(f, IrShapePointer::Computed(c) if c.alias == "x"))
1932 );
1933 }
1934
1935 #[test]
1936 fn test_count_over_multilink_in_computed_shape_element() {
1937 let ir = compile("SELECT Person { post_count := count(.posts) }");
1942 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1943 let (_, shape) = bound(&sel);
1944 let computed = shape
1945 .iter()
1946 .find_map(|f| match f {
1947 IrShapePointer::Computed(c) if c.alias == "post_count" => Some(c),
1948 _ => None,
1949 })
1950 .expect("expected post_count computed pointer");
1951 assert!(matches!(computed.expr, IrExpr::AggOverQuery { .. }));
1952 }
1953
1954 #[test]
1955 fn test_multi_sort_with_then() {
1956 let ir = compile("SELECT Person { name } ORDER BY .name THEN .age");
1957 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1958 assert_eq!(sel.order_by.len(), 2);
1959 }
1960
1961 #[test]
1962 fn test_multi_link_filter_emits_warning() {
1963 let ir = compile("SELECT Person { name } FILTER .posts.title = 'hello'");
1964 assert!(!ir.warnings.is_empty(), "expected a warning for multi-link in filter");
1965 assert!(ir.warnings[0].contains("posts"));
1966 }
1967
1968 #[test]
1969 fn test_session_global_produces_cte() {
1970 let mut schema = make_schema();
1971 schema.globals.push(GlobalDescriptor {
1972 name: "viewer_id".into(),
1973 module: "default".into(),
1974 scalar_type: "std::uuid".into(),
1975 required: false,
1976 default_expr: None,
1977 computed_expr: None,
1978 });
1979 let ast = parse::parse("SELECT Person FILTER .id = global viewer_id").unwrap();
1980 let ir = super::compile(&ast, &schema).expect("IR compile failed");
1981 assert_eq!(ir.global_ctes.len(), 1);
1982 assert_eq!(ir.global_ctes[0].cte_name(), "__global__default::viewer_id");
1983 assert_eq!(ir.params, vec!["__global__default::viewer_id"]);
1984 }
1985
1986 #[test]
1987 fn test_session_global_pg_type_matches_pyql_type_name() {
1988 let mut schema = make_schema();
1996 schema.globals.push(GlobalDescriptor {
1997 name: "viewer_id".into(),
1998 module: "default".into(),
1999 scalar_type: "std::uuid".into(),
2000 required: false,
2001 default_expr: None,
2002 computed_expr: None,
2003 });
2004 let ast = parse::parse("SELECT Person FILTER .id = global viewer_id").unwrap();
2005 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2006 let IrGlobalCte::Session(session) = &ir.global_ctes[0] else {
2007 panic!("expected a session global CTE");
2008 };
2009 assert_eq!(session.pg_type, "uuid");
2010 }
2011
2012 #[test]
2013 fn test_computed_global_field_access_compiles_as_path_select() {
2014 let mut schema = make_schema();
2019 schema.globals.push(GlobalDescriptor {
2020 name: "current_user".into(),
2021 module: "default".into(),
2022 scalar_type: "Person".into(),
2023 required: false,
2024 default_expr: None,
2025 computed_expr: Some("select default::Person filter .id = <uuid>$session_user_id".into()),
2026 });
2027 let ast = parse::parse("SELECT global current_user.id").unwrap();
2028 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2029 let IrStmt::PathSelect(sel) = ir.stmt else {
2030 panic!("expected a path select, not a free select")
2031 };
2032 assert_eq!(sel.root.type_name, "default::Person");
2033 }
2034
2035 #[test]
2036 fn test_subquery_field_access_compiles_as_path_select() {
2037 let ast = parse::parse("SELECT (SELECT default::Person FILTER .age > 20).name").unwrap();
2042 let schema = make_schema();
2043 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2044 let IrStmt::PathSelect(sel) = ir.stmt else {
2045 panic!("expected a path select, not a free select")
2046 };
2047 assert_eq!(sel.root.type_name, "default::Person");
2048 }
2049
2050 #[test]
2051 fn test_string_index_compiles() {
2052 let ast = parse::parse("SELECT 'hello'[1]").unwrap();
2053 let schema = make_schema();
2054 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2055 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2056 let items = free_items(&sel);
2057 assert!(matches!(
2058 items[0],
2059 IrFreeExpr::Scalar(IrExpr::Subscript { is_array: false, .. })
2060 ));
2061 }
2062
2063 #[test]
2064 fn test_array_index_compiles() {
2065 let ast = parse::parse("SELECT [1, 2, 3][0]").unwrap();
2066 let schema = make_schema();
2067 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2068 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2069 let items = free_items(&sel);
2070 assert!(matches!(
2071 items[0],
2072 IrFreeExpr::Scalar(IrExpr::Subscript { is_array: true, .. })
2073 ));
2074 }
2075
2076 #[test]
2077 fn test_string_slice_compiles() {
2078 let ast = parse::parse("SELECT 'hello'[1:3]").unwrap();
2079 let schema = make_schema();
2080 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2081 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2082 let items = free_items(&sel);
2083 assert!(matches!(
2084 items[0],
2085 IrFreeExpr::Scalar(IrExpr::Slice { is_array: false, .. })
2086 ));
2087 }
2088
2089 #[test]
2090 fn test_array_slice_compiles() {
2091 let ast = parse::parse("SELECT [1, 2, 3][0:2]").unwrap();
2092 let schema = make_schema();
2093 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2094 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2095 let items = free_items(&sel);
2096 assert!(matches!(
2097 items[0],
2098 IrFreeExpr::Scalar(IrExpr::Slice { is_array: true, .. })
2099 ));
2100 }
2101
2102 fn make_schema_with_alias() -> SchemaDescriptor {
2103 use crate::schema::AliasDescriptor;
2104 let mut schema = make_schema();
2105 schema.aliases.push(AliasDescriptor {
2106 name: "ActivePersons".into(),
2107 module: "default".into(),
2108 expr: "select Person filter .age >= 18".into(),
2109 });
2110 schema
2111 }
2112
2113 fn make_schema_with_sequence() -> crate::schema::SchemaDescriptor {
2114 use crate::schema::ScalarDescriptor;
2115 let mut schema = make_schema();
2116 schema.scalars.push(ScalarDescriptor {
2117 name: "OrderNumber".into(),
2118 module: "default".into(),
2119 base: "Sequence".into(),
2120 pg_type: "int8".into(),
2121 check_constraints: vec![],
2122 is_sequence: true,
2123 });
2124 schema
2125 }
2126
2127 fn make_schema_with_channels() -> SchemaDescriptor {
2128 let mut schema = make_schema();
2129 schema.channels.push(ChannelDescriptor {
2130 name: "Pings".into(),
2131 module: "default".into(),
2132 wire_name: "default__pings".into(),
2133 payload: ChannelPayload::Scalar("text".into()),
2134 description: None,
2135 });
2136 schema.channels.push(ChannelDescriptor {
2137 name: "SearchReady".into(),
2138 module: "default".into(),
2139 wire_name: "default__search_ready".into(),
2140 payload: ChannelPayload::Object(vec![
2141 ("doc_id".into(), "uuid".into()),
2142 ("score".into(), "float8".into()),
2143 ]),
2144 description: None,
2145 });
2146 schema.channels.push(ChannelDescriptor {
2147 name: "PersonUpdates".into(),
2148 module: "default".into(),
2149 wire_name: "default__person_updates".into(),
2150 payload: ChannelPayload::Type("default::Person".into()),
2151 description: None,
2152 });
2153 schema
2154 }
2155
2156 fn compile_notify_expr(query: &str) -> String {
2157 let schema = make_schema_with_channels();
2158 let ast = parse::parse(query).expect("parse failed");
2159 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2160 let IrStmt::Select(sel) = ir.stmt else {
2161 panic!("expected Select")
2162 };
2163 let items = free_items(&sel);
2164 let IrFreeExpr::Scalar(expr) = items[0] else {
2165 panic!("expected scalar")
2166 };
2167 crate::sql::emit_expr(expr)
2168 }
2169
2170 fn notify_compile_err(query: &str) -> String {
2171 let schema = make_schema_with_channels();
2172 let ast = parse::parse(query).expect("parse failed");
2173 format!(
2174 "{}",
2175 super::compile(&ast, &schema).err().expect("expected a compile error")
2176 )
2177 }
2178
2179 #[test]
2180 fn test_notify_scalar_channel_emits_pg_notify() {
2181 let sql = compile_notify_expr("SELECT notify(Pings, 'hello')");
2182 assert_eq!(sql, "pg_notify('default__pings', ('hello')::text)", "got: {sql}");
2183 }
2184
2185 #[test]
2186 fn test_notify_rejects_unknown_channel() {
2187 let err = notify_compile_err("SELECT notify(NoSuchChannel, 'hi')");
2188 assert!(err.contains("not a known Channel"), "got: {err}");
2189 }
2190
2191 #[test]
2192 fn test_notify_object_channel_emits_jsonb_build_object() {
2193 let sql = compile_notify_expr(
2194 "SELECT notify(SearchReady, { doc_id := <uuid>'3fa85f64-5717-4562-b3fc-2c963f66afa6', score := 0.5 })",
2195 );
2196 assert_eq!(
2197 sql,
2198 "pg_notify('default__search_ready', (jsonb_build_object('doc_id', ('3fa85f64-5717-4562-b3fc-2c963f66afa6')::uuid, 'score', (0.5::float8)))::text)",
2199 "got: {sql}"
2200 );
2201 }
2202
2203 #[test]
2204 fn test_notify_object_channel_rejects_wrong_fields() {
2205 let err = notify_compile_err("SELECT notify(SearchReady, { doc_id := 'x' })");
2206 assert!(
2207 err.contains("payload fields") && err.contains("don't match"),
2208 "got: {err}"
2209 );
2210 }
2211
2212 #[test]
2213 fn test_notify_object_channel_rejects_non_shape_payload() {
2214 let err = notify_compile_err("SELECT notify(SearchReady, 'not an object')");
2215 assert!(err.contains("free object literal"), "got: {err}");
2216 }
2217
2218 #[test]
2219 fn test_notify_type_channel_rejects_arbitrary_payload() {
2220 let err = notify_compile_err("SELECT notify(PersonUpdates, 'not an anchor')");
2221 assert!(err.contains("must name an object of that type"), "got: {err}");
2222 }
2223
2224 #[test]
2225 fn notify_composes_with_a_with_block_binding() {
2226 let sql = compile_notify_expr(
2231 "WITH updated := (UPDATE Person FILTER .id = <uuid>$id SET { name := 'x' }) \
2232 SELECT notify(PersonUpdates, updated)",
2233 );
2234 assert!(sql.contains("pg_notify"), "got: {sql}");
2235 assert!(sql.contains("\"id\""), "payload should be the CTE's id: {sql}");
2237 assert!(sql.contains("updated"), "should reference the with-block CTE: {sql}");
2238 }
2239
2240 #[test]
2241 fn notify_rejects_a_with_block_binding_of_the_wrong_type() {
2242 let err = notify_compile_err("WITH other := (SELECT Company) SELECT notify(PersonUpdates, other)");
2243 assert!(err.contains("expects a payload of type"), "got: {err}");
2244 }
2245
2246 #[test]
2247 fn test_notify_type_channel_via_trigger_new_anchor() {
2248 let schema = make_schema_with_channels();
2249 let ir_out = super::compile_trigger_handler(
2250 "select notify(PersonUpdates, __new__)",
2251 "Person",
2252 1, &schema,
2254 )
2255 .expect("trigger handler compile failed");
2256 let IrStmt::Select(sel) = ir_out.stmt else {
2257 panic!("expected Select")
2258 };
2259 let items = free_items(&sel);
2260 let IrFreeExpr::Scalar(expr) = items[0] else {
2261 panic!("expected scalar")
2262 };
2263 let sql = crate::sql::emit_expr(expr);
2264 assert_eq!(
2265 sql, "pg_notify('default__person_updates', (NEW.\"id\")::text)",
2266 "got: {sql}"
2267 );
2268 }
2269
2270 #[test]
2271 fn test_notify_scalar_channel_via_trigger_new_property_access() {
2272 let schema = make_schema_with_channels();
2279 let ir_out = super::compile_trigger_handler(
2280 "select notify(Pings, __new__.name)",
2281 "Person",
2282 1, &schema,
2284 )
2285 .expect("trigger handler compile failed");
2286 let IrStmt::Select(sel) = ir_out.stmt else {
2287 panic!("expected Select")
2288 };
2289 let items = free_items(&sel);
2290 let IrFreeExpr::Scalar(expr) = items[0] else {
2291 panic!("expected scalar")
2292 };
2293 let sql = crate::sql::emit_expr(expr);
2294 assert_eq!(sql, "pg_notify('default__pings', (NEW.\"name\")::text)", "got: {sql}");
2295 }
2296
2297 #[test]
2298 fn test_notify_type_channel_rejects_bare_reference_outside_trigger() {
2299 let err = notify_compile_err("SELECT notify(PersonUpdates, __new__)");
2301 assert!(err.contains("only bound inside a trigger handler"), "got: {err}");
2302 }
2303
2304 #[test]
2305 fn notify_rejects_an_oversized_concatenation_at_compile_time() {
2306 let half = "x".repeat(4500);
2310 let err = notify_compile_err(&format!("SELECT notify_raw('c', '{half}' ++ '{half}')"));
2311 assert!(err.contains("8000-byte"), "got: {err}");
2312 assert!(err.contains("at least"), "got: {err}");
2313 }
2314
2315 #[test]
2316 fn notify_allows_a_concatenation_that_still_fits() {
2317 let part = "x".repeat(3000);
2318 let sql = compile_notify_expr(&format!("SELECT notify_raw('c', '{part}' ++ '{part}')"));
2319 assert!(sql.contains("pg_notify"), "got: {sql}");
2320 }
2321
2322 #[test]
2323 fn test_notify_raw_emits_pg_notify_with_two_args() {
2324 let sql = compile_notify_expr("SELECT notify_raw('any_channel', 'raw payload')");
2325 assert_eq!(sql, "pg_notify('any_channel', 'raw payload')", "got: {sql}");
2326 }
2327
2328 #[test]
2329 fn test_notify_payload_literal_over_cap_rejected() {
2330 let huge = "x".repeat(8000);
2331 let err = notify_compile_err(&format!("SELECT notify(Pings, '{huge}')"));
2332 assert!(err.contains("NOTIFY payload limit"), "got: {err}");
2333 }
2334
2335 #[test]
2336 fn test_notify_arity_error() {
2337 let err = notify_compile_err("SELECT notify(Pings)");
2338 assert!(err.contains("takes exactly 2 arguments"), "got: {err}");
2339 }
2340
2341 fn compile_seq(query: &str) -> String {
2342 let schema = make_schema_with_sequence();
2343 let ast = parse::parse(query).expect("parse failed");
2344 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2345 let IrStmt::Select(sel) = ir.stmt else {
2346 panic!("expected Select")
2347 };
2348 let items = free_items(&sel);
2349 let IrFreeExpr::Scalar(expr) = items[0] else {
2350 panic!("expected scalar")
2351 };
2352 crate::sql::emit_expr(expr)
2353 }
2354
2355 #[test]
2356 fn test_sequence_next_emits_nextval() {
2357 let sql = compile_seq("SELECT sequence_next(OrderNumber)");
2358 assert_eq!(sql, r#"nextval('"default"."OrderNumber_seq"')"#, "got: {sql}");
2359 }
2360
2361 #[test]
2362 fn test_sequence_reset_no_val_emits_setval_initial() {
2363 let sql = compile_seq("SELECT sequence_reset(OrderNumber)");
2364 assert_eq!(sql, r#"setval('"default"."OrderNumber_seq"', 1, false)"#, "got: {sql}");
2365 }
2366
2367 #[test]
2368 fn test_sequence_reset_with_val_emits_setval() {
2369 let sql = compile_seq("SELECT sequence_reset(OrderNumber, 1000)");
2370 assert_eq!(
2371 sql, r#"setval('"default"."OrderNumber_seq"', 1000, true)"#,
2372 "got: {sql}"
2373 );
2374 }
2375
2376 #[test]
2377 fn test_sequence_next_rejects_non_sequence_type() {
2378 let schema = make_schema();
2379 let ast = parse::parse("SELECT sequence_next(Person)").unwrap();
2380 assert!(super::compile(&ast, &schema).is_err());
2381 }
2382
2383 #[test]
2384 fn test_alias_bare_compiles_to_type_select() {
2385 let schema = make_schema_with_alias();
2386 let ast = parse::parse("SELECT ActivePersons").unwrap();
2387 let ir = super::compile(&ast, &schema).expect("compile failed");
2388 let sql = crate::sql::emit(&ir).sql;
2389 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2390 assert!(sql.contains("18"), "expected age filter, got: {sql}");
2391 }
2392
2393 #[test]
2394 fn test_alias_with_outer_filter_merges() {
2395 let schema = make_schema_with_alias();
2396 let ast = parse::parse("SELECT ActivePersons FILTER .name = 'Alice'").unwrap();
2397 let ir = super::compile(&ast, &schema).expect("compile failed");
2398 let sql = crate::sql::emit(&ir).sql;
2399 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2400 assert!(sql.contains("18"), "expected alias filter, got: {sql}");
2401 assert!(sql.contains("'Alice'"), "expected outer filter, got: {sql}");
2402 }
2403
2404 #[test]
2405 fn test_alias_module_qualified_resolves() {
2406 let schema = make_schema_with_alias();
2407 let ast = parse::parse("SELECT default::ActivePersons").unwrap();
2408 let ir = super::compile(&ast, &schema).expect("compile failed");
2409 let sql = crate::sql::emit(&ir).sql;
2410 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2411 }
2412
2413 #[test]
2414 fn test_alias_with_shape() {
2415 let schema = make_schema_with_alias();
2416 let ast = parse::parse("SELECT ActivePersons { name, age }").unwrap();
2417 let ir = super::compile(&ast, &schema).expect("compile failed");
2418 let sql = crate::sql::emit(&ir).sql;
2419 assert!(sql.contains("\"name\""), "expected name pointer, got: {sql}");
2420 assert!(sql.contains("\"age\""), "expected age pointer, got: {sql}");
2421 }
2422
2423 #[test]
2424 fn test_alias_whose_own_body_has_a_shape_plus_outer_shape() {
2425 use crate::schema::AliasDescriptor;
2432 let mut schema = make_schema();
2433 schema.aliases.push(AliasDescriptor {
2434 name: "OldestActive".into(),
2435 module: "default".into(),
2436 expr: "select Person { name } order by .age desc limit 1".into(),
2437 });
2438 let ast = parse::parse("SELECT OldestActive { name, age }").unwrap();
2439 let ir = super::compile(&ast, &schema).expect("compile failed");
2440 let sql = crate::sql::emit(&ir).sql;
2441 assert!(sql.contains("\"name\""), "expected name pointer, got: {sql}");
2442 assert!(sql.contains("\"age\""), "expected age pointer, got: {sql}");
2443 assert!(
2444 sql.contains("ORDER BY") && sql.contains("LIMIT"),
2445 "alias's own order/limit must still apply, got: {sql}"
2446 );
2447 }
2448
2449 #[test]
2453 fn test_a_binding_named_like_a_generated_cte_gets_its_own_name() {
2454 let ast = parse::parse("WITH _dml := (SELECT Person LIMIT 1) SELECT (UPDATE Person FILTER .id = _dml.id SET { age := 1 }) { name }").unwrap();
2455 let ir = super::compile(&ast, &make_schema()).expect("IR compile failed");
2456 let sql = crate::sql::emit(&ir).sql;
2457 assert_eq!(
2458 sql.matches("\"_dml\" AS (").count(),
2459 1,
2460 "the generated wrapper must keep the name to itself:\n{sql}"
2461 );
2462 }
2463
2464 #[test]
2467 fn test_two_sibling_bindings_of_one_name_get_separate_with_names() {
2468 let ast = parse::parse(
2469 "WITH a := (FOR p IN (SELECT Person) UNION (WITH line := (SELECT Person) SELECT line.name)), \
2470 b := (FOR p IN (SELECT Person) UNION (WITH line := (SELECT Post) SELECT line.title)) \
2471 SELECT {a := a, b := b}",
2472 )
2473 .unwrap();
2474 let ir = super::compile(&ast, &make_schema()).expect("IR compile failed");
2475 let sql = crate::sql::emit(&ir).sql;
2476 assert_eq!(
2477 sql.matches("\"line\" AS (").count(),
2478 1,
2479 "one name can only be claimed once:\n{sql}"
2480 );
2481 }
2482
2483 #[test]
2488 fn test_a_computed_inlined_twice_hoists_its_binding_once() {
2489 let mut schema = make_schema();
2490 schema.types[0].computed.push(ComputedDescriptor {
2491 name: "ranked".into(),
2492 expression: "(with ordering := ['a', 'b'] select array_get(ordering, 0))".into(),
2493 return_type: Some("text".into()),
2494 link_target: None,
2495 link_multi: false,
2496 });
2497 let ast = parse::parse("SELECT Person { ranked } FILTER .ranked = 'a'").unwrap();
2498 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2499 let sql = crate::sql::emit(&ir).sql;
2500 assert_eq!(
2501 sql.matches("\"ordering\" AS (").count(),
2502 1,
2503 "the shared binding must be hoisted once:\n{sql}"
2504 );
2505 }
2506
2507 #[test]
2510 fn test_to_duration_takes_its_arguments_by_name() {
2511 let schema = make_schema();
2512 let ast = parse::parse("SELECT std::to_duration(seconds := 90.0)").unwrap();
2513 super::compile(&ast, &schema).expect("named arguments must resolve");
2514 }
2515
2516 #[test]
2522 fn test_an_ordering_comparison_reads_a_set_walk_as_one_value() {
2523 let schema = make_schema();
2524 let ast = parse::parse("SELECT Company FILTER ((.<company[is Person].age < 30) ?? true)").unwrap();
2525 let ir = super::compile(&ast, &schema).expect("compile failed");
2526 let sql = crate::sql::emit(&ir).sql;
2527 assert!(!sql.contains("ARRAY(SELECT"), "the operand must be one value:\n{sql}");
2528 assert!(sql.contains("COALESCE"), "the coalesce must survive:\n{sql}");
2529 }
2530
2531 #[test]
2536 fn test_a_single_link_walk_off_a_pinned_row_is_not_a_set() {
2537 let schema = make_schema();
2538 let ast =
2539 parse::parse("WITH i := (SELECT Person FILTER .id = <uuid>$0) SELECT { c := i.company.name }").unwrap();
2540 let ir = super::compile(&ast, &schema).expect("compile failed");
2541 let sql = crate::sql::emit(&ir).sql;
2542 assert!(!sql.contains("ARRAY(SELECT"), "expected a value, not a set:\n{sql}");
2543 }
2544
2545 #[test]
2548 fn test_a_single_link_walk_off_an_unpinned_row_is_still_a_set() {
2549 let schema = make_schema();
2550 let ast = parse::parse("WITH i := (SELECT Person FILTER .name = 'x') SELECT { c := i.company.name }").unwrap();
2551 let ir = super::compile(&ast, &schema).expect("compile failed");
2552 let sql = crate::sql::emit(&ir).sql;
2553 assert!(sql.contains("ARRAY(SELECT"), "a walk off many rows is a set:\n{sql}");
2554 }
2555
2556 #[test]
2558 fn test_a_multi_link_step_is_a_set_even_off_a_pinned_row() {
2559 let schema = make_schema();
2560 let ast =
2561 parse::parse("WITH i := (SELECT Person FILTER .id = <uuid>$0) SELECT { t := i.posts.title }").unwrap();
2562 let ir = super::compile(&ast, &schema).expect("compile failed");
2563 let sql = crate::sql::emit(&ir).sql;
2564 assert!(sql.contains("ARRAY(SELECT"), "a multi-link step is a set:\n{sql}");
2565 }
2566
2567 #[test]
2573 fn test_a_plain_read_does_not_see_the_statements_own_write() {
2574 let schema = make_schema();
2579 let ast =
2580 parse::parse("WITH made := (INSERT Person { name := 'a', age := 1 }) SELECT { after := count(Person) }")
2581 .unwrap();
2582 let ir = super::compile(&ast, &schema).expect("compile failed");
2583 let sql = crate::sql::emit(&ir).sql;
2584 assert!(
2585 sql.to_lowercase().contains("from \"public\".\"person\""),
2586 "the count must read the table, not the CTE that wrote to it:\n{sql}"
2587 );
2588 }
2589
2590 #[test]
2591 fn test_a_walk_off_a_mutation_sees_the_rows_it_just_wrote() {
2592 let schema = make_schema();
2593 let ast = parse::parse(
2594 "SELECT (UPDATE Person FILTER .name = 'a' SET { posts += (INSERT Post { title := 't' }) }).posts { title }",
2595 )
2596 .unwrap();
2597 let ir = super::compile(&ast, &schema).expect("compile failed");
2598 let sql = crate::sql::emit(&ir).sql;
2599 assert!(
2600 sql.contains("__ml_add_0\" AS \"") || sql.contains("JOIN \"_nested_dml_1__ml_add_0\""),
2601 "the junction must be read from the CTE that wrote it:\n{sql}"
2602 );
2603 assert!(
2604 !sql.contains("JOIN \"public\".\"Post\""),
2605 "the targets must come from their own CTE, not the base table:\n{sql}"
2606 );
2607 }
2608
2609 #[test]
2614 fn test_a_type_intersection_may_follow_a_sub_select() {
2615 let schema = make_schema();
2616 let ast = parse::parse("SELECT (SELECT Person LIMIT 1).company[is Company].name").unwrap();
2617 let ir = super::compile(&ast, &schema).expect("compile failed");
2618 let sql = crate::sql::emit(&ir).sql;
2619 assert!(
2623 !sql.contains("->'name'"),
2624 "the field must not be jsonb off an id:\n{sql}"
2625 );
2626 assert!(sql.contains("\"name\""), "the field must be read as a column:\n{sql}");
2627 }
2628
2629 #[test]
2632 fn test_a_type_intersection_on_a_value_is_rejected() {
2633 let schema = make_schema();
2634 let ast = parse::parse("SELECT (1 + 2)[is Company]").unwrap();
2635 let Err(error) = super::compile(&ast, &schema) else {
2636 panic!("a type intersection on a number is not meaningful");
2637 };
2638 assert!(error.to_string().contains("needs a path, a binding"), "got: {error}");
2639 }
2640
2641 #[test]
2647 fn test_a_select_may_wrap_a_nested_union_subject_select() {
2648 let schema = make_schema();
2649 let ast =
2650 parse::parse("SELECT (SELECT (Person.company UNION Person.company) { name } LIMIT 1) { name }").unwrap();
2651 let error = super::compile(&ast, &schema)
2652 .err()
2653 .map(|e| e.to_string())
2654 .unwrap_or_default();
2655 assert!(
2656 !error.contains("expected a type name as SELECT subject"),
2657 "the union subject must be hoisted, got: {error}"
2658 );
2659 }
2660
2661 #[test]
2665 fn test_a_select_subject_may_walk_off_a_mutation() {
2666 let schema = make_schema();
2667 let ast =
2668 parse::parse("SELECT (UPDATE Person FILTER .name = 'a' SET { name := 'b' }).company { name }").unwrap();
2669 let ir = super::compile(&ast, &schema).expect("compile failed");
2670 let sql = crate::sql::emit(&ir).sql;
2671 assert!(
2672 sql.contains("\"_nested_dml_0\" AS ("),
2673 "the mutation must run as a CTE:\n{sql}"
2674 );
2675 assert!(sql.contains("UPDATE"), "the mutation must still run:\n{sql}");
2676 }
2677
2678 #[test]
2683 fn test_an_update_subject_may_walk_off_a_sub_select() {
2684 let schema = make_schema();
2685 let inline = parse::parse("UPDATE (SELECT Person FILTER .name = 'a').company SET { name := 'b' }").unwrap();
2686 let bound =
2687 parse::parse("WITH s := (SELECT Person FILTER .name = 'a') UPDATE s.company SET { name := 'b' }").unwrap();
2688 let inline_err = super::compile(&inline, &schema).err().map(|e| e.to_string());
2692 let bound_err = super::compile(&bound, &schema).err().map(|e| e.to_string());
2693 assert_eq!(inline_err, bound_err, "the two spellings must compile alike");
2694 assert!(
2695 !inline_err
2696 .unwrap_or_default()
2697 .contains("expected a type name as SELECT subject"),
2698 "the sub-select subject must be accepted"
2699 );
2700 }
2701
2702 #[test]
2705 fn test_a_for_loop_may_be_a_select_subject() {
2706 let schema = make_schema();
2707 let ast = parse::parse("SELECT (FOR s IN {1, 2} UNION (SELECT { a := s }))").unwrap();
2708 let ir = super::compile(&ast, &schema).expect("compile failed");
2709 assert!(matches!(ir.stmt, super::IrStmt::For(_)), "expected the loop itself");
2710 }
2711
2712 #[test]
2717 fn test_a_json_cast_in_a_shape_is_a_named_pointer() {
2718 use crate::query::ShapeNode;
2719 let schema = make_schema();
2720 let ast = parse::parse("SELECT Person { j := <json>.name }").unwrap();
2721 let ir = super::compile(&ast, &schema).expect("compile failed");
2722 let shape = crate::sql::emit(&ir).shape;
2723 let ShapeNode::Object { pointers, .. } = &shape.root else {
2724 panic!("expected an object shape, got {:?}", shape.root);
2725 };
2726 let pointer = pointers
2727 .iter()
2728 .find(|node| matches!(node, ShapeNode::Scalar { name, .. } if name == "j"))
2729 .unwrap_or_else(|| panic!("no scalar pointer named 'j' in {pointers:?}"));
2730 assert!(matches!(pointer, ShapeNode::Scalar { .. }));
2731 }
2732
2733 #[test]
2736 fn test_a_top_level_json_cast_stays_a_root_shaped_node() {
2737 use crate::query::ShapeNode;
2738 let schema = make_schema();
2739 let ast = parse::parse("SELECT <json>'x'").unwrap();
2740 let ir = super::compile(&ast, &schema).expect("compile failed");
2741 let shape = crate::sql::emit(&ir).shape;
2742 assert!(matches!(shape.root, ShapeNode::JsonScalar), "got {:?}", shape.root);
2743 }
2744
2745 #[test]
2748 fn test_stdlib_enum_member_compiles_to_a_text_literal() {
2749 let schema = make_schema();
2750 let ast = parse::parse("SELECT std::Endian.Big").unwrap();
2751 let ir = super::compile(&ast, &schema).expect("compile failed");
2752 let sql = crate::sql::emit(&ir).sql;
2753 assert!(sql.contains("'Big'::text"), "expected a text literal, got: {sql}");
2754 }
2755
2756 #[test]
2757 fn test_unknown_stdlib_enum_member_is_rejected() {
2758 let schema = make_schema();
2759 let ast = parse::parse("SELECT std::Endian.Middle").unwrap();
2760 let Err(error) = super::compile(&ast, &schema) else {
2761 panic!("Middle is not a member of std::Endian");
2762 };
2763 assert!(error.to_string().contains("has no member 'Middle'"), "got: {error}");
2764 }
2765
2766 #[test]
2771 fn test_to_bytes_of_a_uuid_selects_the_uuid_overload() {
2772 let schema = make_schema();
2773 let ast = parse::parse("SELECT std::to_int32(std::to_bytes(<uuid>$0)[12:16], std::Endian.Big)").unwrap();
2774 let ir = super::compile(&ast, &schema).expect("compile failed");
2775 let sql = crate::sql::emit(&ir).sql;
2776 assert!(sql.contains("to_bytes_uuid"), "expected to_bytes_uuid, got: {sql}");
2777 assert!(sql.contains("to_int32_bytes"), "expected to_int32_bytes, got: {sql}");
2778 }
2779
2780 #[test]
2784 fn a_stdlib_call_with_the_wrong_argument_count_is_rejected() {
2785 let schema = make_schema();
2786 let ast = parse::parse("SELECT std::str_lower('A', 'B')").unwrap();
2787 let Err(err) = super::compile(&ast, &schema) else {
2788 panic!("wrong arity must not compile")
2789 };
2790 let msg = err.to_string();
2791 assert!(msg.contains("std::str_lower"), "{msg}");
2792 assert!(msg.contains("takes 1 argument(s), got 2"), "{msg}");
2793 }
2794
2795 #[test]
2799 fn an_unknown_function_suggests_the_closest_real_one() {
2800 let schema = make_schema();
2801 let ast = parse::parse("SELECT std::uuid_generate_v7j()").unwrap();
2802 let Err(err) = super::compile(&ast, &schema) else {
2803 panic!("an unknown function must not compile")
2804 };
2805 let msg = err.to_string();
2806 assert!(msg.contains("does not exist"), "{msg}");
2807 assert!(msg.contains("did you mean std::uuid_generate_v7()?"), "{msg}");
2808 }
2809
2810 #[test]
2814 fn a_function_in_another_namespace_says_where_it_lives() {
2815 let schema = make_schema();
2816 let ast = parse::parse("SELECT std::pi()").unwrap();
2817 let Err(err) = super::compile(&ast, &schema) else {
2818 panic!("pi lives in math, not std")
2819 };
2820 assert!(err.to_string().contains("it lives in math, use math::pi()"), "{err}");
2821 }
2822
2823 #[test]
2826 fn an_arity_error_names_every_arity_the_overload_set_accepts() {
2827 let schema = make_schema();
2828 let ast = parse::parse("SELECT std::str_trim('A', 'B', 'C')").unwrap();
2829 let Err(err) = super::compile(&ast, &schema) else {
2830 panic!("wrong arity must not compile")
2831 };
2832 assert!(err.to_string().contains("takes 1 or 2 argument(s), got 3"), "{err}");
2833 }
2834
2835 #[test]
2838 fn a_variadic_stdlib_call_accepts_extra_arguments() {
2839 let schema = make_schema();
2840 for query in [
2841 "SELECT std::json_get(<json>$0, 'a')",
2842 "SELECT std::json_get(<json>$0, 'a', 'b', 'c')",
2843 ] {
2844 let ast = parse::parse(query).unwrap();
2845 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2846 }
2847 }
2848
2849 #[test]
2852 fn a_stdlib_call_with_an_unacceptable_argument_type_is_rejected() {
2853 let schema = make_schema();
2854 let ast = parse::parse("SELECT std::str_lower(<int64>$0)").unwrap();
2855 let Err(err) = super::compile(&ast, &schema) else {
2856 panic!("wrong argument type must not compile")
2857 };
2858 let msg = err.to_string();
2859 assert!(msg.contains("no overload accepting (int8)"), "{msg}");
2860 assert!(msg.contains("(str)"), "{msg}");
2861 }
2862
2863 #[test]
2868 fn a_stdlib_call_over_a_coalesce_or_conditional_resolves_its_branch_type() {
2869 let schema = make_schema();
2870 for query in [
2871 "SELECT std::str_lower(<optional str>$0 ?? 'DE')",
2872 "SELECT std::str_lower('DE' ?? <optional str>$0)",
2873 "WITH j := (<optional str>$0 ?? 'DE') SELECT std::str_lower(j)",
2874 "SELECT std::str_lower(<str>$0 if <bool>$1 else 'DE')",
2875 "SELECT std::len(<optional str>$0 ?? 'DE')",
2876 ] {
2877 let ast = parse::parse(query).unwrap();
2878 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2879 }
2880 }
2881
2882 #[test]
2887 fn a_stdlib_call_over_a_pass_through_expression_resolves_the_value_type() {
2888 let schema = make_schema();
2889 for query in [
2890 "SELECT (FOR code IN std::array_unpack(<array<str>>$0) UNION (SELECT std::str_lower(code)))",
2892 "WITH x := { a := 'DE' } SELECT std::str_lower(x.a)",
2894 "SELECT math::abs(-3)",
2896 "SELECT math::abs(-(<int64>$0))",
2897 "SELECT std::str_lower((<array<str>>$0)[0])",
2899 "SELECT std::str_title(std::str_split(<str>$0, '::')[0])",
2901 "SELECT std::duration_to_seconds(std::datetime_of_transaction() - <datetime>$0)",
2903 "SELECT std::duration_to_seconds(<duration>$0 + <duration>$0)",
2904 ] {
2905 let ast = parse::parse(query).unwrap();
2906 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2907 }
2908 }
2909
2910 #[test]
2913 fn a_stdlib_call_over_a_coalesce_of_the_wrong_type_is_still_rejected() {
2914 let schema = make_schema();
2915 let ast = parse::parse("SELECT std::str_lower(<optional int64>$0 ?? 3)").unwrap();
2916 let Err(err) = super::compile(&ast, &schema) else {
2917 panic!("wrong argument type must not compile")
2918 };
2919 assert!(err.to_string().contains("no overload accepting (int8)"), "{err}");
2920 }
2921
2922 #[test]
2926 fn test_to_int16_and_to_int64_over_bytes_select_the_bytes_overload() {
2927 let schema = make_schema();
2928 for (query, expected) in [
2929 (
2930 "SELECT std::to_int16(std::to_bytes(<uuid>$0)[14:16], std::Endian.Big)",
2931 "to_int16_bytes",
2932 ),
2933 (
2934 "SELECT std::to_int64(std::to_bytes(<uuid>$0)[0:8], std::Endian.Little)",
2935 "to_int64_bytes",
2936 ),
2937 ] {
2938 let ast = parse::parse(query).unwrap();
2939 let ir = super::compile(&ast, &schema).expect("compile failed");
2940 let sql = crate::sql::emit(&ir).sql;
2941 assert!(sql.contains(expected), "expected {expected}, got: {sql}");
2942 }
2943 }
2944
2945 #[test]
2946 fn test_positional_param_names() {
2947 let schema = make_schema();
2948 let ast = parse::parse("SELECT Person FILTER .name = $0").unwrap();
2949 let ir = super::compile(&ast, &schema).expect("compile failed");
2950 assert_eq!(ir.params, vec!["0"]);
2951 }
2952
2953 #[test]
2954 fn test_multiple_positional_param_names_in_order() {
2955 let schema = make_schema();
2956 let ast = parse::parse("SELECT Person FILTER .name = $0 AND .age > $1").unwrap();
2957 let ir = super::compile(&ast, &schema).expect("compile failed");
2958 assert_eq!(ir.params, vec!["0", "1"]);
2959 }
2960
2961 #[test]
2962 fn test_repeated_positional_param_single_slot() {
2963 let schema = make_schema();
2964 let ast = parse::parse("SELECT Person FILTER .name = $0 OR .name = $0").unwrap();
2965 let ir = super::compile(&ast, &schema).expect("compile failed");
2966 assert_eq!(ir.params, vec!["0"], "repeated $0 must occupy a single slot");
2967 }
2968}