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
245impl IrPathJoin {
246 pub fn target(&self) -> &IrSource {
248 match self {
249 IrPathJoin::Single { target, .. }
250 | IrPathJoin::Multi { target, .. }
251 | IrPathJoin::BacklinkSingle { target, .. }
252 | IrPathJoin::BacklinkMulti { target, .. }
253 | IrPathJoin::Function { target, .. }
254 | IrPathJoin::Lateral { target, .. } => target,
255 }
256 }
257}
258
259#[derive(Debug, Clone)]
260pub enum IrPathResult {
261 Scalar(IrExpr, Option<TupleCastShape>),
269 Object {
271 alias: String,
272 type_name: String,
273 shape: Vec<IrShapePointer>,
274 },
275}
276
277#[derive(Debug, Clone)]
280pub enum IrFreeExpr {
281 Scalar(IrExpr),
283 FreeObject(Vec<(String, IrExpr)>),
285 NamedTupleRow(Vec<(String, IrExpr)>),
290 Tuple(Vec<IrExpr>),
292 AssertSet {
295 fn_name: String,
296 inner: Box<IrArraySource>,
297 message: Option<IrExpr>,
299 },
300 CtePassthrough(String),
302}
303
304#[derive(Debug, Clone)]
306pub enum IrArraySource {
307 Select(IrSelect),
309 ObjectFunction(Box<IrFunctionSelect>),
313 ObjectSelect(Box<IrSelect>),
317 PathSelect(Box<IrPathSelect>),
318 Group(Box<IrGroup>),
320 StmtColumn {
323 stmt: Box<IrStmt>,
324 column: String,
325 },
326 RawExpr {
328 source: IrSource,
329 poly_implementors: Vec<IrPolyImplementor>,
331 poly_columns: Vec<String>,
332 expr: IrExpr,
333 },
334}
335
336#[derive(Debug, Clone)]
339pub struct IrSelect {
340 pub rows: Vec<IrRowSource>,
344 pub filter: Option<IrExpr>,
347 pub order_by: Vec<IrSort>,
348 pub offset: Option<IrExpr>,
349 pub limit: Option<IrExpr>,
350 pub distinct: bool,
351 pub dml_source: Option<Box<IrStmt>>,
356 pub polymorphic: bool,
360 pub poly_implementors: Vec<IrPolyImplementor>,
362 pub poly_columns: Vec<String>,
364 pub lock: Option<IrLockClause>,
371}
372
373#[derive(Debug, Clone)]
374pub struct IrLockClause {
375 pub strength: IrLockStrength,
376 pub wait: IrLockWait,
377}
378
379#[derive(Debug, Clone)]
380pub enum IrLockStrength {
381 Update,
382 NoKeyUpdate,
383 Share,
384 KeyShare,
385}
386
387#[derive(Debug, Clone)]
388pub enum IrLockWait {
389 Block,
390 NoWait,
391 SkipLocked,
392}
393
394#[derive(Debug, Clone)]
400pub enum IrRowSource {
401 Bound {
402 source: IrSource,
403 shape: Vec<IrShapePointer>,
404 },
405 Free(IrFreeExpr),
406}
407
408impl IrSelect {
409 pub fn schema_bound(source: IrSource, shape: Vec<IrShapePointer>, filter: Option<IrExpr>) -> Self {
412 IrSelect {
413 rows: vec![IrRowSource::Bound { source, shape }],
414 filter,
415 order_by: vec![],
416 offset: None,
417 limit: None,
418 distinct: false,
419 dml_source: None,
420 polymorphic: false,
421 poly_implementors: vec![],
422 poly_columns: vec![],
423 lock: None,
424 }
425 }
426}
427
428#[derive(Debug, Clone)]
430pub struct IrPolyImplementor {
431 pub type_name: String,
433 pub table: String,
435 pub module: String,
437}
438
439#[derive(Debug, Clone)]
442pub struct IrSource {
443 pub type_name: String,
445 pub table: String,
447 pub alias: String,
449 pub poly: Option<IrPolyFanout>,
461}
462
463#[derive(Debug, Clone)]
466pub struct IrPolyFanout {
467 pub implementors: Vec<IrPolyImplementor>,
468 pub columns: Vec<String>,
469}
470
471#[derive(Debug, Clone)]
474pub struct IrScalarSetPointer {
475 pub alias: String,
476 pub source: IrSource,
477 pub poly_implementors: Vec<IrPolyImplementor>,
478 pub poly_columns: Vec<String>,
479 pub bool_expr: IrExpr,
480}
481
482#[derive(Debug, Clone)]
483pub enum IrShapePointer {
484 Scalar(IrScalarPointer),
485 SingleLink(IrSingleLinkPointer),
486 MultiLink(IrMultiLinkPointer),
487 Computed(IrComputedPointer),
488 ScalarSet(IrScalarSetPointer),
489 Asserted(Box<IrAssertedPointer>),
494}
495
496#[derive(Debug, Clone)]
497pub struct IrAssertedPointer {
498 pub fn_name: String,
503 pub inner: IrShapePointer,
504 pub check: Option<IrShapePointer>,
508 pub message: Option<IrExpr>,
510}
511
512impl IrShapePointer {
513 pub fn alias(&self) -> &str {
515 match self {
516 IrShapePointer::Scalar(p) => &p.alias,
517 IrShapePointer::SingleLink(p) => &p.alias,
518 IrShapePointer::MultiLink(p) => &p.alias,
519 IrShapePointer::Computed(p) => &p.alias,
520 IrShapePointer::ScalarSet(p) => &p.alias,
521 IrShapePointer::Asserted(p) => p.inner.alias(),
522 }
523 }
524
525 pub fn is_object_pointer(&self) -> bool {
529 match self {
530 IrShapePointer::SingleLink(_) | IrShapePointer::MultiLink(_) => true,
531 IrShapePointer::Asserted(a) => a.inner.is_object_pointer(),
532 IrShapePointer::Computed(c) => match &c.expr {
533 IrExpr::ObjectSubquery(_) | IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. } => true,
534 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
538 IrArraySource::ObjectFunction(_) | IrArraySource::ObjectSelect(_) => true,
539 IrArraySource::PathSelect(ps) => matches!(ps.result, IrPathResult::Object { .. }),
540 _ => false,
541 },
542 _ => false,
543 },
544 IrShapePointer::Scalar(_) | IrShapePointer::ScalarSet(_) => false,
545 }
546 }
547}
548
549#[derive(Debug, Clone)]
551pub struct IrScalarPointer {
552 pub alias: String,
554 pub column: String,
556 pub pg_type: String,
558 pub tuple_shape: Option<TupleCastShape>,
562 pub marker_offset: Option<usize>,
567 pub implicit_id: bool,
572}
573
574#[derive(Debug, Clone)]
579pub struct IrSingleLinkPointer {
580 pub alias: String,
581 pub correlation: IrSingleLinkCorrelation,
582 pub subquery: IrSelect,
584 pub link_properties: Vec<IrLinkProp>,
588 pub marker_offset: Option<usize>,
590}
591
592#[derive(Debug, Clone)]
593pub enum IrSingleLinkCorrelation {
594 Fk {
596 fk_column: String,
598 target_pk: String,
600 },
601 Junction {
604 join: IrMultiLinkJoin,
605 target_pk: String,
607 },
608}
609
610#[derive(Debug, Clone)]
613pub struct IrMultiLinkPointer {
614 pub alias: String,
615 pub join: IrMultiLinkJoin,
617 pub subquery: IrSelect,
619 pub link_properties: Vec<IrLinkProp>,
621 pub marker_offset: Option<usize>,
623 pub single: bool,
626}
627
628#[derive(Debug, Clone)]
630pub struct IrLinkProp {
631 pub name: String,
633}
634
635#[derive(Debug, Clone)]
636pub enum IrMultiLinkJoin {
637 Standard {
640 junction_table: String,
642 module: String,
644 },
645 Through {
647 junction_table: String,
649 module: String,
651 source_col: String,
653 target_col: String,
655 },
656 BacklinkFk {
662 fk_col: String,
664 },
665 BacklinkJunction {
670 junction_table: String,
671 module: String,
672 owner_col: String,
674 current_col: String,
676 },
677}
678
679#[derive(Debug, Clone)]
681pub struct IrComputedPointer {
682 pub alias: String,
683 pub expr: IrExpr,
684 pub marker_offset: Option<usize>,
686}
687
688#[derive(Debug, Clone)]
693pub struct VectorEnqueueInfo {
694 pub type_name: String,
696 pub index_name: Option<String>,
698}
699
700#[derive(Debug, Clone)]
704pub struct SearchEnqueueInfo {
705 pub type_name: String,
706 pub index_name: Option<String>,
707 pub operation: &'static str,
709 pub backend: crate::schema::SearchBackend,
712}
713
714#[derive(Debug, Clone)]
717pub struct IrInsert {
718 pub guard: Option<IrExpr>,
723 pub target: IrSource,
724 pub assignments: Vec<(String, IrExpr)>,
726 pub unless_conflict: Option<IrConflict>,
727 pub rewrites: Vec<IrRewrite>,
729 pub returning: Vec<IrShapePointer>,
731 pub enqueue_vector: Vec<VectorEnqueueInfo>,
733 pub enqueue_search: Vec<SearchEnqueueInfo>,
735 pub multi_link_appends: Vec<IrMultiLinkMutation>,
740 pub id_default_sql: Option<String>,
745 pub nested_ctes: Vec<IrCteDef>,
754}
755
756#[derive(Debug, Clone)]
757pub struct IrConflict {
758 pub on: Option<IrExpr>,
760 pub do_update: Option<Vec<(String, IrExpr)>>,
762 pub do_update_where: Option<IrExpr>,
768}
769
770#[derive(Debug, Clone)]
773pub struct IrUpdate {
774 pub target: IrSource,
775 pub filter: Option<IrExpr>,
776 pub assignments: Vec<(String, IrExpr)>,
777 pub rewrites: Vec<IrRewrite>,
779 pub returning: Vec<IrShapePointer>,
780 pub enqueue_vector: Vec<VectorEnqueueInfo>,
782 pub enqueue_search: Vec<SearchEnqueueInfo>,
784 pub poly_implementors: Vec<IrPolyImplementor>,
786 pub poly_columns: Vec<String>,
792 pub multi_link_clears: Vec<IrMultiLinkClear>,
794 pub multi_link_replaces: Vec<IrMultiLinkMutation>,
796 pub multi_link_appends: Vec<IrMultiLinkMutation>,
798 pub multi_link_removals: Vec<IrMultiLinkMutation>,
800 pub nested_ctes: Vec<IrCteDef>,
807}
808
809#[derive(Debug, Clone)]
810pub struct IrMultiLinkClear {
811 pub junction_table: String,
812 pub module: String,
813 pub source_col: String,
815}
816
817#[derive(Debug, Clone)]
819pub struct IrMultiLinkMutation {
820 pub junction_table: String,
821 pub module: String,
822 pub source_col: String,
824 pub target_col: String,
826 pub values: IrMultiLinkValues,
829 pub single: bool,
834}
835
836#[derive(Debug, Clone)]
844pub struct IrMultiLinkValues {
845 pub source: IrMultiLinkValueSource,
846 pub link_props: Vec<(String, IrExpr)>,
847}
848
849#[derive(Debug, Clone)]
851pub enum IrMultiLinkValueSource {
852 CteRef(String),
854 Select(Box<IrSelect>),
856 PathSelect(Box<IrPathSelect>),
858 Function(Box<IrFunctionSelect>),
861 Union(Box<IrMultiLinkValues>, Box<IrMultiLinkValues>),
864 Asserted {
869 fn_name: String,
870 inner: Box<IrMultiLinkValues>,
871 message: Option<IrExpr>,
873 },
874}
875
876#[derive(Debug, Clone)]
879pub struct IrDelete {
880 pub target: IrSource,
881 pub filter: Option<IrExpr>,
882 pub returning: Vec<IrShapePointer>,
883 pub poly_implementors: Vec<IrPolyImplementor>,
885 pub poly_columns: Vec<String>,
887 pub enqueue_search: Vec<SearchEnqueueInfo>,
889}
890
891#[derive(Debug, Clone, PartialEq, Eq)]
897pub enum SetOpMode {
898 Array,
899 Exists,
900 Aggregate(String),
901}
902
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
904pub enum SetOpKind {
905 Intersect,
906 Except,
907}
908
909impl SetOpKind {
910 pub fn sql(self) -> &'static str {
911 match self {
912 Self::Intersect => "INTERSECT",
913 Self::Except => "EXCEPT",
914 }
915 }
916}
917
918#[derive(Debug, Clone)]
919pub enum IrExpr {
920 ColumnRef {
922 alias: String,
923 column: String,
924 pg_type: String,
925 },
926 Param {
928 index: usize,
929 },
930 Literal(IrLiteral),
931 BinOp(Box<IrBinOp>),
932 UnaryOp(Box<IrUnaryOp>),
933 FunctionCall(IrFunctionCall),
934 TypeCast(Box<IrTypeCast>),
935 IfElse(Box<IrIfElse>),
936 Subquery(Box<IrSelect>),
938 Array(Vec<IrExpr>),
940 Null,
942 AggOverSet {
945 fn_name: String,
946 schema: Option<String>,
947 elems: Vec<IrExpr>,
948 },
949 AggOverQuery {
952 fn_name: String,
953 inner: Box<IrSelect>,
954 },
955 AggOverCte {
961 fn_name: String,
962 cte: String,
963 column: Option<String>,
964 },
965 ExistsOverCte {
971 cte: String,
972 column: Option<String>,
973 },
974 SetOp {
978 op: SetOpKind,
979 left: Box<IrExpr>,
980 right: Box<IrExpr>,
981 mode: SetOpMode,
982 },
983 CteRef {
987 name: String,
988 scalar: bool,
989 pg_type: Option<String>,
994 },
995 CteFieldRef {
1002 name: String,
1003 field: String,
1004 pg_type: Option<String>,
1007 },
1008 ForVar {
1011 name: String,
1012 pg_type: Option<String>,
1016 },
1017 ArrayFromSelect(Box<IrArraySource>),
1020 ScalarSubquery(Box<IrSelect>),
1025 EnumLiteral {
1027 pg_type: String,
1028 variant: String,
1029 },
1030 NamedTuple {
1038 fields: Vec<(String, IrExpr)>,
1039 is_free_object: bool,
1040 },
1041 ObjectPathUnion {
1046 branches: Vec<IrPathSelect>,
1047 limit: Option<Box<IrExpr>>,
1048 multi: bool,
1052 },
1053 ObjectPathSubquery(Box<IrPathSelect>),
1058 ObjectSubquery(Box<IrSelect>),
1064 Tuple(Vec<IrExpr>),
1066 CompositeField {
1074 expr: Box<IrExpr>,
1075 field: String,
1076 pg_type: Option<String>,
1077 },
1078 Row {
1097 elements: Vec<IrExpr>,
1098 names: Option<Vec<String>>,
1099 is_free_object: bool,
1100 },
1101
1102 GlobalParam {
1104 index: usize,
1105 pg_type: String,
1106 },
1107 GlobalRef {
1109 cte_name: String,
1110 },
1111 Subscript {
1113 expr: Box<IrExpr>,
1114 index: Box<IrExpr>,
1115 is_array: bool,
1116 },
1117 JsonbField {
1119 expr: Box<IrExpr>,
1120 field: String,
1121 },
1122 JsonbIndex {
1126 expr: Box<IrExpr>,
1127 index: usize,
1128 },
1129 Slice {
1131 expr: Box<IrExpr>,
1132 lower: Option<Box<IrExpr>>,
1133 upper: Option<Box<IrExpr>>,
1134 is_array: bool,
1135 },
1136 FnSubquery(Box<IrFunctionSelect>),
1142 PathSubquery(Box<IrPathSelect>),
1145 FnParam {
1148 name: String,
1149 pg_type: String,
1150 },
1151 RawSql(String),
1161}
1162
1163#[derive(Debug, Clone)]
1173pub struct IrVectorSearch {
1174 pub source: IrSource,
1176 pub vector_col: String,
1178 pub distance_op: &'static str,
1180 pub query_expr: IrExpr,
1183 pub object_shape: Vec<IrShapePointer>,
1186 pub filter: Option<IrExpr>,
1187 pub order_by_distance: Option<IrSortDir>,
1190 pub offset: Option<IrExpr>,
1191 pub limit: Option<IrExpr>,
1192 pub inference_query_param_name: Option<String>,
1195 pub inference_query_literal: Option<String>,
1197 pub inference_model: Option<String>,
1199 pub inference_type_name: Option<String>,
1201 pub inference_index_name: Option<Option<String>>,
1203}
1204
1205#[derive(Debug, Clone)]
1212pub struct IrFtsSearch {
1213 pub source: IrSource,
1215 pub backend: crate::schema::SearchBackend,
1217 pub search_col: String,
1219 pub tsquery_fn: &'static str,
1221 pub query_expr: IrExpr,
1223 pub object_shape: Vec<IrShapePointer>,
1225 pub filter: Option<IrExpr>,
1226 pub order_by_rank: Option<IrSortDir>,
1227 pub offset: Option<IrExpr>,
1228 pub limit: Option<IrExpr>,
1229 pub deferred_index_name: Option<String>,
1231 pub deferred_query_param_name: Option<String>,
1233 pub deferred_query_literal: Option<String>,
1235 pub deferred_ids_param: Option<usize>,
1237 pub deferred_scores_param: Option<usize>,
1239}
1240
1241#[derive(Debug, Clone)]
1246pub struct IrFunctionSelect {
1247 pub fn_module: String,
1248 pub fn_name: String,
1249 pub fn_args: Vec<IrExpr>,
1250 pub alias: String,
1252 pub type_name: String,
1254 pub polymorphic: bool,
1256 pub poly_implementors: Vec<IrPolyImplementor>,
1258 pub poly_columns: Vec<String>,
1260 pub shape: Vec<IrShapePointer>,
1261 pub filter: Option<IrExpr>,
1262 pub order_by: Vec<IrSort>,
1263 pub offset: Option<IrExpr>,
1264 pub limit: Option<IrExpr>,
1265 pub distinct: bool,
1266}
1267
1268#[derive(Debug, Clone)]
1269pub struct IrBinOp {
1270 pub left: IrExpr,
1271 pub op: BinOpKind,
1272 pub right: IrExpr,
1273}
1274
1275#[derive(Debug, Clone)]
1276pub struct IrUnaryOp {
1277 pub op: UnaryOpKind,
1278 pub operand: IrExpr,
1279}
1280
1281#[derive(Debug, Clone)]
1282pub struct IrFunctionCall {
1283 pub return_pg_type: Option<String>,
1297 pub schema: Option<String>,
1298 pub name: String,
1299 pub args: Vec<IrExpr>,
1300 pub sql_template: Option<String>,
1303}
1304
1305impl IrExpr {
1306 pub fn free_object_fields(&self) -> Option<Vec<(&str, &IrExpr)>> {
1313 let IrExpr::Row {
1314 elements,
1315 names: Some(names),
1316 is_free_object: true,
1317 } = self
1318 else {
1319 return None;
1320 };
1321 Some(names.iter().map(String::as_str).zip(elements).collect())
1322 }
1323}
1324
1325#[derive(Debug, Clone)]
1326pub struct IrTypeCast {
1327 pub expr: IrExpr,
1328 pub pg_type: String,
1330 pub tuple_shape: Option<TupleCastShape>,
1335}
1336
1337#[derive(Debug, Clone)]
1338pub struct TupleCastShape {
1339 pub type_name: Option<String>,
1342 pub members: Vec<crate::query::JsonMember>,
1343}
1344
1345#[derive(Debug, Clone)]
1346pub struct IrIfElse {
1347 pub condition: IrExpr,
1348 pub if_: IrExpr,
1349 pub else_: IrExpr,
1350}
1351
1352#[derive(Debug, Clone)]
1353pub enum IrLiteral {
1354 Str(String),
1355 Int(i64),
1356 Float(f64),
1357 Bool(bool),
1358}
1359
1360#[derive(Debug, Clone)]
1363pub struct IrSort {
1364 pub expr: IrExpr,
1365 pub direction: IrSortDir,
1366 pub nulls: IrNulls,
1367}
1368
1369#[derive(Debug, Clone)]
1370pub enum IrSortDir {
1371 Asc,
1372 Desc,
1373}
1374
1375#[derive(Debug, Clone)]
1376pub enum IrNulls {
1377 First,
1378 Last,
1379}
1380
1381#[derive(Debug, Clone)]
1386pub struct IrRewrite {
1387 pub column: String,
1389 pub expr: IrExpr,
1394}
1395
1396#[derive(Debug, Clone)]
1398pub struct IrCteDef {
1399 pub name: String,
1400 pub stmt: IrStmt,
1401 pub type_name: String,
1404 pub correlated_to: Option<String>,
1409}
1410
1411#[derive(Debug, Clone)]
1413pub struct IrSessionGlobalCte {
1414 pub cte_name: String,
1415 pub qualified_name: String,
1416 pub param_index: usize,
1417 pub pg_type: String,
1418}
1419
1420#[derive(Debug, Clone)]
1422pub struct IrComputedGlobalCte {
1423 pub cte_name: String,
1424 pub qualified_name: String,
1425 pub stmt: IrStmt,
1426}
1427
1428#[derive(Debug, Clone)]
1429pub enum IrGlobalCte {
1430 Session(IrSessionGlobalCte),
1431 Computed(Box<IrComputedGlobalCte>),
1435}
1436
1437impl IrGlobalCte {
1438 pub fn cte_name(&self) -> &str {
1439 match self {
1440 Self::Session(s) => &s.cte_name,
1441 Self::Computed(c) => &c.cte_name,
1442 }
1443 }
1444}
1445
1446pub struct IrOutput {
1450 pub stmt: IrStmt,
1451 pub params: Vec<String>,
1454 pub param_tuple_types: Vec<Option<crate::query::ParamTupleType>>,
1457 pub ctes: Vec<IrCteDef>,
1459 pub global_ctes: Vec<IrGlobalCte>,
1461 pub warnings: Vec<String>,
1463 pub uses_globals_arg: bool,
1467 pub subtype_fanouts: HashMap<(String, String), IrPolyFanout>,
1471}
1472
1473pub type QualifiedTable = (String, String);
1475
1476const INHERITED_JUNCTION: &str = "@inherited:";
1480
1481pub fn inherited_junction(tables: &[QualifiedTable], columns: &[String]) -> String {
1484 let tables = tables
1485 .iter()
1486 .map(|(module, table)| format!("{module}\u{1f}{table}"))
1487 .collect::<Vec<_>>()
1488 .join("\u{1e}");
1489 format!("{INHERITED_JUNCTION}{tables}\u{1d}{}", columns.join("\u{1f}"))
1490}
1491
1492pub fn parse_inherited_junction(name: &str) -> Option<(Vec<QualifiedTable>, Vec<String>)> {
1495 let (tables, columns) = name.strip_prefix(INHERITED_JUNCTION)?.split_once('\u{1d}')?;
1496 let tables = tables
1497 .split('\u{1e}')
1498 .filter_map(|entry| entry.split_once('\u{1f}'))
1499 .map(|(module, table)| (module.to_string(), table.to_string()))
1500 .collect();
1501 Some((tables, columns.split('\u{1f}').map(str::to_string).collect()))
1502}
1503
1504#[cfg(test)]
1505mod tests {
1506 use super::*;
1507 #[allow(unused_imports)]
1508 use super::{IrFreeExpr, IrLiteral};
1509 use crate::parse;
1510 use crate::schema::{
1511 ChannelDescriptor, ChannelPayload, ComputedDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
1512 PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
1513 };
1514
1515 fn make_schema() -> SchemaDescriptor {
1516 SchemaDescriptor {
1517 types: vec![
1518 TypeDescriptor {
1519 name: "Person".into(),
1520 module: "default".into(),
1521 table: "person".into(),
1522 abstract_: false,
1523 materialized: false,
1524 description: None,
1525 parents: vec![],
1526 interfaces: vec![],
1527 bases: vec![],
1528 properties: vec![
1529 PropertyDescriptor {
1530 name: "id".into(),
1531 pg_type: "uuid".into(),
1532 nullable: false,
1533 default_sql: Some("uuidv7()".into()),
1534 default_pyql: None,
1535 description: None,
1536 check_constraints: vec![],
1537 is_exclusive: true,
1538 is_pk: true,
1539 is_readonly: true,
1540 rewrites: vec![],
1541 tuple_members: None,
1542 column_type: None,
1543 },
1544 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 PropertyDescriptor {
1560 name: "age".into(),
1561 pg_type: "int8".into(),
1562 nullable: true,
1563 default_sql: None,
1564 default_pyql: None,
1565 description: None,
1566 check_constraints: vec![],
1567 is_exclusive: false,
1568 is_pk: false,
1569 is_readonly: false,
1570 rewrites: vec![],
1571 tuple_members: None,
1572 column_type: None,
1573 },
1574 ],
1575 links: vec![LinkDescriptor {
1576 name: "company".into(),
1577 target: "default::Company".into(),
1578 nullable: true,
1579 through: None,
1580 description: None,
1581 default_pyql: None,
1582 is_exclusive: false,
1583 is_readonly: false,
1584 rewrites: vec![],
1585 on_delete: vec![],
1586 }],
1587 multilinks: vec![MultiLinkDescriptor {
1588 name: "posts".into(),
1589 target: "default::Post".into(),
1590 through: None,
1591 nullable: false,
1592 description: None,
1593 default_pyql: None,
1594 on_delete: vec![],
1595 is_exclusive: false,
1596 }],
1597 computed: vec![],
1598 constraints: vec![],
1599 indexes: vec![],
1600 partition: None,
1601 vector_indexes: vec![],
1602 search_indexes: vec![],
1603 triggers: vec![],
1604 junction: false,
1605 signals: vec![],
1606 },
1607 TypeDescriptor {
1608 name: "Company".into(),
1609 module: "default".into(),
1610 table: "company".into(),
1611 abstract_: false,
1612 materialized: false,
1613 description: None,
1614 parents: vec![],
1615 interfaces: vec![],
1616 bases: vec![],
1617 properties: vec![PropertyDescriptor {
1618 name: "name".into(),
1619 pg_type: "text".into(),
1620 nullable: false,
1621 default_sql: None,
1622 default_pyql: None,
1623 description: None,
1624 check_constraints: vec![],
1625 is_exclusive: false,
1626 is_pk: false,
1627 is_readonly: false,
1628 rewrites: vec![],
1629 tuple_members: None,
1630 column_type: None,
1631 }],
1632 links: vec![],
1633 multilinks: vec![],
1634 computed: vec![],
1635 constraints: vec![],
1636 indexes: vec![],
1637 partition: None,
1638 vector_indexes: vec![],
1639 search_indexes: vec![],
1640 triggers: vec![],
1641 junction: false,
1642 signals: vec![],
1643 },
1644 TypeDescriptor {
1645 name: "Post".into(),
1646 module: "default".into(),
1647 table: "post".into(),
1648 abstract_: false,
1649 materialized: false,
1650 description: None,
1651 parents: vec![],
1652 interfaces: vec![],
1653 bases: vec![],
1654 properties: vec![PropertyDescriptor {
1655 name: "title".into(),
1656 pg_type: "text".into(),
1657 nullable: false,
1658 default_sql: None,
1659 default_pyql: None,
1660 description: None,
1661 check_constraints: vec![],
1662 is_exclusive: false,
1663 is_pk: false,
1664 is_readonly: false,
1665 rewrites: vec![],
1666 tuple_members: None,
1667 column_type: None,
1668 }],
1669 links: vec![],
1670 multilinks: vec![],
1671 computed: vec![],
1672 constraints: vec![],
1673 indexes: vec![],
1674 partition: None,
1675 vector_indexes: vec![],
1676 search_indexes: vec![],
1677 triggers: vec![],
1678 junction: false,
1679 signals: vec![],
1680 },
1681 ],
1682 scalars: vec![],
1683 enums: vec![],
1684 named_tuples: vec![],
1685 globals: vec![],
1686 functions: vec![],
1687 aliases: vec![],
1688 channels: vec![],
1689 ..Default::default()
1690 }
1691 }
1692
1693 fn compile(query: &str) -> IrOutput {
1694 let schema = make_schema();
1695 let ast = parse::parse(query).expect("parse failed");
1696 super::compile(&ast, &schema).expect("IR compile failed")
1697 }
1698
1699 fn bound(sel: &IrSelect) -> (&IrSource, &[IrShapePointer]) {
1703 match sel.rows.as_slice() {
1704 [IrRowSource::Bound { source, shape }] => (source, shape),
1705 _ => panic!("expected a single schema-bound row"),
1706 }
1707 }
1708
1709 fn free_items(sel: &IrSelect) -> Vec<&IrFreeExpr> {
1712 sel.rows
1713 .iter()
1714 .map(|r| match r {
1715 IrRowSource::Free(item) => item,
1716 IrRowSource::Bound { .. } => panic!("expected a free row"),
1717 })
1718 .collect()
1719 }
1720
1721 #[test]
1722 fn test_select_resolves_source() {
1723 let ir = compile("SELECT Person { name, age }");
1724 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1725 let (source, shape) = bound(&sel);
1726 assert_eq!(source.table, "person");
1727 assert_eq!(source.type_name, "default::Person");
1728 assert_eq!(shape.len(), 3);
1730 assert!(matches!(&shape[0], IrShapePointer::Scalar(p) if p.alias == "id" && p.implicit_id));
1731 assert!(matches!(shape[1], IrShapePointer::Scalar(_)));
1732 }
1733
1734 #[test]
1735 fn test_select_filter_param_ordering() {
1736 let ir = compile("SELECT Person { name } FILTER .name = $name AND .age > $min_age");
1737 assert_eq!(ir.params, vec!["name", "min_age"]);
1738 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1739 assert!(sel.filter.is_some());
1740 }
1741
1742 #[test]
1743 fn test_select_single_link() {
1744 let ir = compile("SELECT Person { name, company { name } }");
1745 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1746 let (_, shape) = bound(&sel);
1747 assert_eq!(shape.len(), 3);
1748 let IrShapePointer::SingleLink(link) = &shape[2] else {
1749 panic!("expected SingleLink")
1750 };
1751 assert_eq!(link.alias, "company");
1752 let IrSingleLinkCorrelation::Fk { fk_column, .. } = &link.correlation else {
1753 panic!("expected Fk correlation")
1754 };
1755 assert_eq!(fk_column, "company_id");
1756 assert_eq!(bound(&link.subquery).0.table, "company");
1757 }
1758
1759 #[test]
1760 fn test_select_multi_link() {
1761 let ir = compile("SELECT Person { name, posts { title } }");
1762 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1763 let (_, shape) = bound(&sel);
1764 let IrShapePointer::MultiLink(ml) = &shape[2] else {
1765 panic!("expected MultiLink")
1766 };
1767 assert_eq!(ml.alias, "posts");
1768 assert_eq!(bound(&ml.subquery).0.table, "post");
1769 let IrMultiLinkJoin::Standard { junction_table, .. } = &ml.join else {
1770 panic!()
1771 };
1772 assert_eq!(junction_table, "person.posts");
1773 }
1774
1775 #[test]
1776 fn test_select_no_shape_returns_id_only() {
1777 let ir = compile("SELECT Person");
1778 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1779 let (_, shape) = bound(&sel);
1781 assert_eq!(shape.len(), 1);
1782 let IrShapePointer::Scalar(f) = &shape[0] else { panic!() };
1783 assert_eq!(f.alias, "id");
1784 }
1785
1786 #[test]
1787 fn test_free_select_set_literal() {
1788 let schema = make_schema();
1789 let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
1790 let ir = super::compile(&ast, &schema).unwrap();
1791 let IrStmt::Select(sel) = ir.stmt else {
1792 panic!("expected Select")
1793 };
1794 let items = free_items(&sel);
1795 assert_eq!(items.len(), 3);
1796 assert!(matches!(
1797 items[0],
1798 IrFreeExpr::Scalar(IrExpr::Literal(IrLiteral::Int(1)))
1799 ));
1800 }
1801
1802 #[test]
1803 fn test_free_select_free_object() {
1804 let schema = make_schema();
1805 let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
1806 let ir = super::compile(&ast, &schema).unwrap();
1807 let IrStmt::Select(sel) = ir.stmt else {
1808 panic!("expected Select")
1809 };
1810 let items = free_items(&sel);
1811 assert_eq!(items.len(), 1);
1812 let IrFreeExpr::FreeObject(fields) = &items[0] else {
1813 panic!("expected FreeObject")
1814 };
1815 assert_eq!(fields.len(), 2);
1816 assert_eq!(fields[0].0, "foo");
1817 assert_eq!(fields[1].0, "n");
1818 }
1819
1820 #[test]
1821 fn test_free_select_tuple() {
1822 let schema = make_schema();
1823 let ast = parse::parse("SELECT (1, 'hello')").unwrap();
1824 let ir = super::compile(&ast, &schema).unwrap();
1825 let IrStmt::Select(sel) = ir.stmt else {
1826 panic!("expected Select")
1827 };
1828 let items = free_items(&sel);
1829 assert_eq!(items.len(), 1);
1830 assert!(matches!(items[0], IrFreeExpr::Tuple(_)));
1831 }
1832
1833 #[test]
1834 fn test_free_select_scalar_literal() {
1835 let schema = make_schema();
1836 let ast = parse::parse("SELECT 42").unwrap();
1837 let ir = super::compile(&ast, &schema).unwrap();
1838 let IrStmt::Select(sel) = ir.stmt else {
1839 panic!("expected Select")
1840 };
1841 let items = free_items(&sel);
1842 assert_eq!(items.len(), 1);
1843 assert!(matches!(
1844 items[0],
1845 IrFreeExpr::Scalar(IrExpr::Literal(IrLiteral::Int(42)))
1846 ));
1847 }
1848
1849 #[test]
1850 fn test_free_select_function_call() {
1851 let schema = make_schema();
1852 let ast = parse::parse("SELECT str_lower('HELLO')").unwrap();
1853 let ir = super::compile(&ast, &schema).unwrap();
1854 let IrStmt::Select(sel) = ir.stmt else {
1855 panic!("expected Select")
1856 };
1857 let items = free_items(&sel);
1858 assert!(matches!(items[0], IrFreeExpr::Scalar(IrExpr::FunctionCall(_))));
1859 }
1860
1861 #[test]
1862 fn test_free_select_rejects_dot_path() {
1863 let schema = make_schema();
1864 let ast = parse::parse("SELECT {.name}").unwrap();
1865 assert!(super::compile(&ast, &schema).is_err());
1866 }
1867
1868 #[test]
1869 fn test_type_error_uuid_eq_str() {
1870 let schema = make_schema();
1871 let ast = parse::parse("SELECT Person FILTER .id = 'not-a-uuid'").unwrap();
1872 let err = super::compile(&ast, &schema).err().expect("expected type error");
1873 let msg = err.to_string();
1874 assert!(
1875 msg.contains("std::uuid") && msg.contains("std::str"),
1876 "unexpected: {msg}"
1877 );
1878 }
1879
1880 #[test]
1881 fn test_type_error_str_eq_int() {
1882 let schema = make_schema();
1883 let ast = parse::parse("SELECT Person FILTER .name = 42").unwrap();
1884 let err = super::compile(&ast, &schema).err().expect("expected type error");
1885 let msg = err.to_string();
1886 assert!(
1887 msg.contains("std::str") && msg.contains("std::int64"),
1888 "unexpected: {msg}"
1889 );
1890 }
1891
1892 #[test]
1893 fn test_int_literal_compatible_with_all_int_columns() {
1894 let schema = make_schema();
1896 let ast = parse::parse("SELECT Person FILTER .age = 30").unwrap();
1897 assert!(super::compile(&ast, &schema).is_ok());
1898 }
1899
1900 #[test]
1901 fn test_cast_int16_compatible_with_int8_column() {
1902 let schema = make_schema();
1903 let ast = parse::parse("SELECT Person FILTER .age = <int16>30").unwrap();
1904 assert!(super::compile(&ast, &schema).is_ok());
1905 }
1906
1907 #[test]
1908 fn test_unknown_type_error() {
1909 let schema = make_schema();
1910 let ast = parse::parse("SELECT Ghost { name }").unwrap();
1911 assert!(super::compile(&ast, &schema).is_err());
1912 }
1913
1914 #[test]
1915 fn test_nested_dml_link_value_combines_with_multilink_mutation_in_the_same_update() {
1916 let schema = make_schema();
1926 let ast = parse::parse(
1927 "UPDATE Person FILTER .id = $id SET { \
1928 company := (select (insert Company { name := 'Acme' }) { id }), \
1929 posts += (SELECT Post FILTER .title = $t) \
1930 }",
1931 )
1932 .unwrap();
1933 let ir = super::compile(&ast, &schema).unwrap();
1934 let IrStmt::Update(upd) = ir.stmt else {
1935 panic!("expected Update")
1936 };
1937 assert_eq!(upd.nested_ctes.len(), 1);
1938 assert_eq!(upd.multi_link_appends.len(), 1);
1939 }
1940
1941 #[test]
1942 fn test_unknown_pointer_error() {
1943 let schema = make_schema();
1944 let ast = parse::parse("SELECT Person { nonexistent }").unwrap();
1945 assert!(super::compile(&ast, &schema).is_err());
1946 }
1947
1948 #[test]
1949 fn test_insert_compiles_assignments() {
1950 let ir = compile("INSERT Person { name := 'Alice', age := 30 }");
1951 let IrStmt::Insert(ins) = ir.stmt else { panic!() };
1952 assert_eq!(ins.target.table, "person");
1953 assert_eq!(ins.assignments.len(), 2);
1954 assert_eq!(ins.assignments[0].0, "name");
1955 assert_eq!(ins.assignments[1].0, "age");
1956 }
1957
1958 #[test]
1959 fn test_delete_compiles_filter() {
1960 let ir = compile("DELETE Person FILTER .name = $name");
1961 let IrStmt::Delete(del) = ir.stmt else { panic!() };
1962 assert!(del.filter.is_some());
1963 assert_eq!(ir.params, vec!["name"]);
1964 }
1965
1966 fn make_schema_with_computed() -> SchemaDescriptor {
1967 let mut schema = make_schema();
1968 schema.types[0].computed.push(ComputedDescriptor {
1970 name: "upper_name".into(),
1971 expression: "str_upper(.name)".into(),
1972 return_type: Some("text".into()),
1973 link_target: None,
1974 link_multi: false,
1975 });
1976 schema
1977 }
1978
1979 #[test]
1980 fn test_computed_pointer_in_shape() {
1981 let schema = make_schema_with_computed();
1982 let ast = parse::parse("SELECT Person { upper_name }").unwrap();
1983 let ir = super::compile(&ast, &schema).expect("IR compile failed");
1984 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1985 let (_, shape) = bound(&sel);
1987 assert!(
1988 shape
1989 .iter()
1990 .any(|f| matches!(f, IrShapePointer::Computed(c) if c.alias == "upper_name"))
1991 );
1992 }
1993
1994 #[test]
1995 fn test_computed_pointer_in_expression_context() {
1996 let schema = make_schema_with_computed();
1997 let ast = parse::parse("SELECT Person { x := str_lower(.upper_name) }").unwrap();
1998 let ir = super::compile(&ast, &schema).expect("IR compile failed");
1999 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2000 let (_, shape) = bound(&sel);
2001 assert!(
2002 shape
2003 .iter()
2004 .any(|f| matches!(f, IrShapePointer::Computed(c) if c.alias == "x"))
2005 );
2006 }
2007
2008 #[test]
2009 fn test_count_over_multilink_in_computed_shape_element() {
2010 let ir = compile("SELECT Person { post_count := count(.posts) }");
2015 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2016 let (_, shape) = bound(&sel);
2017 let computed = shape
2018 .iter()
2019 .find_map(|f| match f {
2020 IrShapePointer::Computed(c) if c.alias == "post_count" => Some(c),
2021 _ => None,
2022 })
2023 .expect("expected post_count computed pointer");
2024 assert!(matches!(computed.expr, IrExpr::AggOverQuery { .. }));
2025 }
2026
2027 #[test]
2028 fn test_multi_sort_with_then() {
2029 let ir = compile("SELECT Person { name } ORDER BY .name THEN .age");
2030 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2031 assert_eq!(sel.order_by.len(), 2);
2032 }
2033
2034 #[test]
2035 fn test_multi_link_filter_emits_warning() {
2036 let ir = compile("SELECT Person { name } FILTER .posts.title = 'hello'");
2037 assert!(!ir.warnings.is_empty(), "expected a warning for multi-link in filter");
2038 assert!(ir.warnings[0].contains("posts"));
2039 }
2040
2041 #[test]
2042 fn test_session_global_produces_cte() {
2043 let mut schema = make_schema();
2044 schema.globals.push(GlobalDescriptor {
2045 name: "viewer_id".into(),
2046 module: "default".into(),
2047 scalar_type: "std::uuid".into(),
2048 required: false,
2049 default_expr: None,
2050 computed_expr: None,
2051 });
2052 let ast = parse::parse("SELECT Person FILTER .id = global viewer_id").unwrap();
2053 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2054 assert_eq!(ir.global_ctes.len(), 1);
2055 assert_eq!(ir.global_ctes[0].cte_name(), "__global__default::viewer_id");
2056 assert_eq!(ir.params, vec!["__global__default::viewer_id"]);
2057 }
2058
2059 #[test]
2060 fn test_session_global_pg_type_matches_pyql_type_name() {
2061 let mut schema = make_schema();
2069 schema.globals.push(GlobalDescriptor {
2070 name: "viewer_id".into(),
2071 module: "default".into(),
2072 scalar_type: "std::uuid".into(),
2073 required: false,
2074 default_expr: None,
2075 computed_expr: None,
2076 });
2077 let ast = parse::parse("SELECT Person FILTER .id = global viewer_id").unwrap();
2078 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2079 let IrGlobalCte::Session(session) = &ir.global_ctes[0] else {
2080 panic!("expected a session global CTE");
2081 };
2082 assert_eq!(session.pg_type, "uuid");
2083 }
2084
2085 #[test]
2086 fn test_computed_global_field_access_compiles_as_path_select() {
2087 let mut schema = make_schema();
2092 schema.globals.push(GlobalDescriptor {
2093 name: "current_user".into(),
2094 module: "default".into(),
2095 scalar_type: "Person".into(),
2096 required: false,
2097 default_expr: None,
2098 computed_expr: Some("select default::Person filter .id = <uuid>$session_user_id".into()),
2099 });
2100 let ast = parse::parse("SELECT global current_user.id").unwrap();
2101 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2102 let IrStmt::PathSelect(sel) = ir.stmt else {
2103 panic!("expected a path select, not a free select")
2104 };
2105 assert_eq!(sel.root.type_name, "default::Person");
2106 }
2107
2108 #[test]
2109 fn test_subquery_field_access_compiles_as_path_select() {
2110 let ast = parse::parse("SELECT (SELECT default::Person FILTER .age > 20).name").unwrap();
2115 let schema = make_schema();
2116 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2117 let IrStmt::PathSelect(sel) = ir.stmt else {
2118 panic!("expected a path select, not a free select")
2119 };
2120 assert_eq!(sel.root.type_name, "default::Person");
2121 }
2122
2123 #[test]
2124 fn test_string_index_compiles() {
2125 let ast = parse::parse("SELECT 'hello'[1]").unwrap();
2126 let schema = make_schema();
2127 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2128 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2129 let items = free_items(&sel);
2130 assert!(matches!(
2131 items[0],
2132 IrFreeExpr::Scalar(IrExpr::Subscript { is_array: false, .. })
2133 ));
2134 }
2135
2136 #[test]
2137 fn test_array_index_compiles() {
2138 let ast = parse::parse("SELECT [1, 2, 3][0]").unwrap();
2139 let schema = make_schema();
2140 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2141 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2142 let items = free_items(&sel);
2143 assert!(matches!(
2144 items[0],
2145 IrFreeExpr::Scalar(IrExpr::Subscript { is_array: true, .. })
2146 ));
2147 }
2148
2149 #[test]
2150 fn test_string_slice_compiles() {
2151 let ast = parse::parse("SELECT 'hello'[1:3]").unwrap();
2152 let schema = make_schema();
2153 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2154 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2155 let items = free_items(&sel);
2156 assert!(matches!(
2157 items[0],
2158 IrFreeExpr::Scalar(IrExpr::Slice { is_array: false, .. })
2159 ));
2160 }
2161
2162 #[test]
2163 fn test_array_slice_compiles() {
2164 let ast = parse::parse("SELECT [1, 2, 3][0:2]").unwrap();
2165 let schema = make_schema();
2166 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2167 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2168 let items = free_items(&sel);
2169 assert!(matches!(
2170 items[0],
2171 IrFreeExpr::Scalar(IrExpr::Slice { is_array: true, .. })
2172 ));
2173 }
2174
2175 fn make_schema_with_alias() -> SchemaDescriptor {
2176 use crate::schema::AliasDescriptor;
2177 let mut schema = make_schema();
2178 schema.aliases.push(AliasDescriptor {
2179 name: "ActivePersons".into(),
2180 module: "default".into(),
2181 expr: "select Person filter .age >= 18".into(),
2182 });
2183 schema
2184 }
2185
2186 fn make_schema_with_sequence() -> crate::schema::SchemaDescriptor {
2187 use crate::schema::ScalarDescriptor;
2188 let mut schema = make_schema();
2189 schema.scalars.push(ScalarDescriptor {
2190 name: "OrderNumber".into(),
2191 module: "default".into(),
2192 base: "Sequence".into(),
2193 pg_type: "int8".into(),
2194 check_constraints: vec![],
2195 is_sequence: true,
2196 });
2197 schema
2198 }
2199
2200 fn make_schema_with_channels() -> SchemaDescriptor {
2201 let mut schema = make_schema();
2202 schema.channels.push(ChannelDescriptor {
2203 name: "Pings".into(),
2204 module: "default".into(),
2205 wire_name: "default__pings".into(),
2206 payload: ChannelPayload::Scalar("text".into()),
2207 description: None,
2208 });
2209 schema.channels.push(ChannelDescriptor {
2210 name: "SearchReady".into(),
2211 module: "default".into(),
2212 wire_name: "default__search_ready".into(),
2213 payload: ChannelPayload::Object(vec![
2214 ("doc_id".into(), "uuid".into()),
2215 ("score".into(), "float8".into()),
2216 ]),
2217 description: None,
2218 });
2219 schema.channels.push(ChannelDescriptor {
2220 name: "PersonUpdates".into(),
2221 module: "default".into(),
2222 wire_name: "default__person_updates".into(),
2223 payload: ChannelPayload::Type("default::Person".into()),
2224 description: None,
2225 });
2226 schema
2227 }
2228
2229 fn compile_notify_expr(query: &str) -> String {
2230 let schema = make_schema_with_channels();
2231 let ast = parse::parse(query).expect("parse failed");
2232 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2233 let IrStmt::Select(sel) = ir.stmt else {
2234 panic!("expected Select")
2235 };
2236 let items = free_items(&sel);
2237 let IrFreeExpr::Scalar(expr) = items[0] else {
2238 panic!("expected scalar")
2239 };
2240 crate::sql::emit_expr(expr)
2241 }
2242
2243 fn notify_compile_err(query: &str) -> String {
2244 let schema = make_schema_with_channels();
2245 let ast = parse::parse(query).expect("parse failed");
2246 format!(
2247 "{}",
2248 super::compile(&ast, &schema).err().expect("expected a compile error")
2249 )
2250 }
2251
2252 #[test]
2253 fn test_notify_scalar_channel_emits_pg_notify() {
2254 let sql = compile_notify_expr("SELECT notify(Pings, 'hello')");
2255 assert_eq!(sql, "pg_notify('default__pings', ('hello')::text)", "got: {sql}");
2256 }
2257
2258 #[test]
2259 fn test_notify_rejects_unknown_channel() {
2260 let err = notify_compile_err("SELECT notify(NoSuchChannel, 'hi')");
2261 assert!(err.contains("not a known Channel"), "got: {err}");
2262 }
2263
2264 #[test]
2265 fn test_notify_object_channel_emits_jsonb_build_object() {
2266 let sql = compile_notify_expr(
2267 "SELECT notify(SearchReady, { doc_id := <uuid>'3fa85f64-5717-4562-b3fc-2c963f66afa6', score := 0.5 })",
2268 );
2269 assert_eq!(
2270 sql,
2271 "pg_notify('default__search_ready', (jsonb_build_object('doc_id', ('3fa85f64-5717-4562-b3fc-2c963f66afa6')::uuid, 'score', (0.5::float8)))::text)",
2272 "got: {sql}"
2273 );
2274 }
2275
2276 #[test]
2277 fn test_notify_object_channel_rejects_wrong_fields() {
2278 let err = notify_compile_err("SELECT notify(SearchReady, { doc_id := 'x' })");
2279 assert!(
2280 err.contains("payload fields") && err.contains("don't match"),
2281 "got: {err}"
2282 );
2283 }
2284
2285 #[test]
2286 fn test_notify_object_channel_rejects_non_shape_payload() {
2287 let err = notify_compile_err("SELECT notify(SearchReady, 'not an object')");
2288 assert!(err.contains("free object literal"), "got: {err}");
2289 }
2290
2291 #[test]
2292 fn test_notify_type_channel_rejects_arbitrary_payload() {
2293 let err = notify_compile_err("SELECT notify(PersonUpdates, 'not an anchor')");
2294 assert!(err.contains("must name an object of that type"), "got: {err}");
2295 }
2296
2297 #[test]
2298 fn notify_composes_with_a_with_block_binding() {
2299 let sql = compile_notify_expr(
2304 "WITH updated := (UPDATE Person FILTER .id = <uuid>$id SET { name := 'x' }) \
2305 SELECT notify(PersonUpdates, updated)",
2306 );
2307 assert!(sql.contains("pg_notify"), "got: {sql}");
2308 assert!(sql.contains("\"id\""), "payload should be the CTE's id: {sql}");
2310 assert!(sql.contains("updated"), "should reference the with-block CTE: {sql}");
2311 }
2312
2313 #[test]
2314 fn notify_rejects_a_with_block_binding_of_the_wrong_type() {
2315 let err = notify_compile_err("WITH other := (SELECT Company) SELECT notify(PersonUpdates, other)");
2316 assert!(err.contains("expects a payload of type"), "got: {err}");
2317 }
2318
2319 #[test]
2320 fn test_notify_type_channel_via_trigger_new_anchor() {
2321 let schema = make_schema_with_channels();
2322 let ir_out = super::compile_trigger_handler(
2323 "select notify(PersonUpdates, __new__)",
2324 "Person",
2325 1, &schema,
2327 )
2328 .expect("trigger handler compile failed");
2329 let IrStmt::Select(sel) = ir_out.stmt else {
2330 panic!("expected Select")
2331 };
2332 let items = free_items(&sel);
2333 let IrFreeExpr::Scalar(expr) = items[0] else {
2334 panic!("expected scalar")
2335 };
2336 let sql = crate::sql::emit_expr(expr);
2337 assert_eq!(
2338 sql, "pg_notify('default__person_updates', (NEW.\"id\")::text)",
2339 "got: {sql}"
2340 );
2341 }
2342
2343 #[test]
2344 fn test_notify_scalar_channel_via_trigger_new_property_access() {
2345 let schema = make_schema_with_channels();
2352 let ir_out = super::compile_trigger_handler(
2353 "select notify(Pings, __new__.name)",
2354 "Person",
2355 1, &schema,
2357 )
2358 .expect("trigger handler compile failed");
2359 let IrStmt::Select(sel) = ir_out.stmt else {
2360 panic!("expected Select")
2361 };
2362 let items = free_items(&sel);
2363 let IrFreeExpr::Scalar(expr) = items[0] else {
2364 panic!("expected scalar")
2365 };
2366 let sql = crate::sql::emit_expr(expr);
2367 assert_eq!(sql, "pg_notify('default__pings', (NEW.\"name\")::text)", "got: {sql}");
2368 }
2369
2370 #[test]
2371 fn test_notify_type_channel_rejects_bare_reference_outside_trigger() {
2372 let err = notify_compile_err("SELECT notify(PersonUpdates, __new__)");
2374 assert!(err.contains("only bound inside a trigger handler"), "got: {err}");
2375 }
2376
2377 #[test]
2378 fn notify_rejects_an_oversized_concatenation_at_compile_time() {
2379 let half = "x".repeat(4500);
2383 let err = notify_compile_err(&format!("SELECT notify_raw('c', '{half}' ++ '{half}')"));
2384 assert!(err.contains("8000-byte"), "got: {err}");
2385 assert!(err.contains("at least"), "got: {err}");
2386 }
2387
2388 #[test]
2389 fn notify_allows_a_concatenation_that_still_fits() {
2390 let part = "x".repeat(3000);
2391 let sql = compile_notify_expr(&format!("SELECT notify_raw('c', '{part}' ++ '{part}')"));
2392 assert!(sql.contains("pg_notify"), "got: {sql}");
2393 }
2394
2395 #[test]
2396 fn test_notify_raw_emits_pg_notify_with_two_args() {
2397 let sql = compile_notify_expr("SELECT notify_raw('any_channel', 'raw payload')");
2398 assert_eq!(sql, "pg_notify('any_channel', 'raw payload')", "got: {sql}");
2399 }
2400
2401 #[test]
2402 fn test_notify_payload_literal_over_cap_rejected() {
2403 let huge = "x".repeat(8000);
2404 let err = notify_compile_err(&format!("SELECT notify(Pings, '{huge}')"));
2405 assert!(err.contains("NOTIFY payload limit"), "got: {err}");
2406 }
2407
2408 #[test]
2409 fn test_notify_arity_error() {
2410 let err = notify_compile_err("SELECT notify(Pings)");
2411 assert!(err.contains("takes exactly 2 arguments"), "got: {err}");
2412 }
2413
2414 fn compile_seq(query: &str) -> String {
2415 let schema = make_schema_with_sequence();
2416 let ast = parse::parse(query).expect("parse failed");
2417 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2418 let IrStmt::Select(sel) = ir.stmt else {
2419 panic!("expected Select")
2420 };
2421 let items = free_items(&sel);
2422 let IrFreeExpr::Scalar(expr) = items[0] else {
2423 panic!("expected scalar")
2424 };
2425 crate::sql::emit_expr(expr)
2426 }
2427
2428 #[test]
2429 fn test_sequence_next_emits_nextval() {
2430 let sql = compile_seq("SELECT sequence_next(OrderNumber)");
2431 assert_eq!(sql, r#"nextval('"default"."OrderNumber_seq"')"#, "got: {sql}");
2432 }
2433
2434 #[test]
2435 fn test_sequence_reset_no_val_emits_setval_initial() {
2436 let sql = compile_seq("SELECT sequence_reset(OrderNumber)");
2437 assert_eq!(sql, r#"setval('"default"."OrderNumber_seq"', 1, false)"#, "got: {sql}");
2438 }
2439
2440 #[test]
2441 fn test_sequence_reset_with_val_emits_setval() {
2442 let sql = compile_seq("SELECT sequence_reset(OrderNumber, 1000)");
2443 assert_eq!(
2444 sql, r#"setval('"default"."OrderNumber_seq"', 1000, true)"#,
2445 "got: {sql}"
2446 );
2447 }
2448
2449 #[test]
2450 fn test_sequence_next_rejects_non_sequence_type() {
2451 let schema = make_schema();
2452 let ast = parse::parse("SELECT sequence_next(Person)").unwrap();
2453 assert!(super::compile(&ast, &schema).is_err());
2454 }
2455
2456 #[test]
2457 fn test_alias_bare_compiles_to_type_select() {
2458 let schema = make_schema_with_alias();
2459 let ast = parse::parse("SELECT ActivePersons").unwrap();
2460 let ir = super::compile(&ast, &schema).expect("compile failed");
2461 let sql = crate::sql::emit(&ir).sql;
2462 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2463 assert!(sql.contains("18"), "expected age filter, got: {sql}");
2464 }
2465
2466 #[test]
2467 fn test_alias_with_outer_filter_merges() {
2468 let schema = make_schema_with_alias();
2469 let ast = parse::parse("SELECT ActivePersons FILTER .name = 'Alice'").unwrap();
2470 let ir = super::compile(&ast, &schema).expect("compile failed");
2471 let sql = crate::sql::emit(&ir).sql;
2472 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2473 assert!(sql.contains("18"), "expected alias filter, got: {sql}");
2474 assert!(sql.contains("'Alice'"), "expected outer filter, got: {sql}");
2475 }
2476
2477 #[test]
2478 fn test_alias_module_qualified_resolves() {
2479 let schema = make_schema_with_alias();
2480 let ast = parse::parse("SELECT default::ActivePersons").unwrap();
2481 let ir = super::compile(&ast, &schema).expect("compile failed");
2482 let sql = crate::sql::emit(&ir).sql;
2483 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2484 }
2485
2486 #[test]
2487 fn test_alias_with_shape() {
2488 let schema = make_schema_with_alias();
2489 let ast = parse::parse("SELECT ActivePersons { name, age }").unwrap();
2490 let ir = super::compile(&ast, &schema).expect("compile failed");
2491 let sql = crate::sql::emit(&ir).sql;
2492 assert!(sql.contains("\"name\""), "expected name pointer, got: {sql}");
2493 assert!(sql.contains("\"age\""), "expected age pointer, got: {sql}");
2494 }
2495
2496 #[test]
2497 fn test_alias_whose_own_body_has_a_shape_plus_outer_shape() {
2498 use crate::schema::AliasDescriptor;
2505 let mut schema = make_schema();
2506 schema.aliases.push(AliasDescriptor {
2507 name: "OldestActive".into(),
2508 module: "default".into(),
2509 expr: "select Person { name } order by .age desc limit 1".into(),
2510 });
2511 let ast = parse::parse("SELECT OldestActive { name, age }").unwrap();
2512 let ir = super::compile(&ast, &schema).expect("compile failed");
2513 let sql = crate::sql::emit(&ir).sql;
2514 assert!(sql.contains("\"name\""), "expected name pointer, got: {sql}");
2515 assert!(sql.contains("\"age\""), "expected age pointer, got: {sql}");
2516 assert!(
2517 sql.contains("ORDER BY") && sql.contains("LIMIT"),
2518 "alias's own order/limit must still apply, got: {sql}"
2519 );
2520 }
2521
2522 #[test]
2526 fn test_a_binding_named_like_a_generated_cte_gets_its_own_name() {
2527 let ast = parse::parse("WITH _dml := (SELECT Person LIMIT 1) SELECT (UPDATE Person FILTER .id = _dml.id SET { age := 1 }) { name }").unwrap();
2528 let ir = super::compile(&ast, &make_schema()).expect("IR compile failed");
2529 let sql = crate::sql::emit(&ir).sql;
2530 assert_eq!(
2531 sql.matches("\"_dml\" AS (").count(),
2532 1,
2533 "the generated wrapper must keep the name to itself:\n{sql}"
2534 );
2535 }
2536
2537 #[test]
2540 fn test_two_sibling_bindings_of_one_name_get_separate_with_names() {
2541 let ast = parse::parse(
2542 "WITH a := (FOR p IN (SELECT Person) UNION (WITH line := (SELECT Person) SELECT line.name)), \
2543 b := (FOR p IN (SELECT Person) UNION (WITH line := (SELECT Post) SELECT line.title)) \
2544 SELECT {a := a, b := b}",
2545 )
2546 .unwrap();
2547 let ir = super::compile(&ast, &make_schema()).expect("IR compile failed");
2548 let sql = crate::sql::emit(&ir).sql;
2549 assert_eq!(
2550 sql.matches("\"line\" AS (").count(),
2551 1,
2552 "one name can only be claimed once:\n{sql}"
2553 );
2554 }
2555
2556 #[test]
2561 fn test_a_computed_inlined_twice_hoists_its_binding_once() {
2562 let mut schema = make_schema();
2563 schema.types[0].computed.push(ComputedDescriptor {
2564 name: "ranked".into(),
2565 expression: "(with ordering := ['a', 'b'] select array_get(ordering, 0))".into(),
2566 return_type: Some("text".into()),
2567 link_target: None,
2568 link_multi: false,
2569 });
2570 let ast = parse::parse("SELECT Person { ranked } FILTER .ranked = 'a'").unwrap();
2571 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2572 let sql = crate::sql::emit(&ir).sql;
2573 assert_eq!(
2574 sql.matches("\"ordering\" AS (").count(),
2575 1,
2576 "the shared binding must be hoisted once:\n{sql}"
2577 );
2578 }
2579
2580 #[test]
2583 fn test_to_duration_takes_its_arguments_by_name() {
2584 let schema = make_schema();
2585 let ast = parse::parse("SELECT std::to_duration(seconds := 90.0)").unwrap();
2586 super::compile(&ast, &schema).expect("named arguments must resolve");
2587 }
2588
2589 #[test]
2595 fn test_an_ordering_comparison_reads_a_set_walk_as_one_value() {
2596 let schema = make_schema();
2597 let ast = parse::parse("SELECT Company FILTER ((.<company[is Person].age < 30) ?? true)").unwrap();
2598 let ir = super::compile(&ast, &schema).expect("compile failed");
2599 let sql = crate::sql::emit(&ir).sql;
2600 assert!(!sql.contains("ARRAY(SELECT"), "the operand must be one value:\n{sql}");
2601 assert!(sql.contains("COALESCE"), "the coalesce must survive:\n{sql}");
2602 }
2603
2604 #[test]
2609 fn test_a_single_link_walk_off_a_pinned_row_is_not_a_set() {
2610 let schema = make_schema();
2611 let ast =
2612 parse::parse("WITH i := (SELECT Person FILTER .id = <uuid>$0) SELECT { c := i.company.name }").unwrap();
2613 let ir = super::compile(&ast, &schema).expect("compile failed");
2614 let sql = crate::sql::emit(&ir).sql;
2615 assert!(!sql.contains("ARRAY(SELECT"), "expected a value, not a set:\n{sql}");
2616 }
2617
2618 #[test]
2621 fn test_a_single_link_walk_off_an_unpinned_row_is_still_a_set() {
2622 let schema = make_schema();
2623 let ast = parse::parse("WITH i := (SELECT Person FILTER .name = 'x') SELECT { c := i.company.name }").unwrap();
2624 let ir = super::compile(&ast, &schema).expect("compile failed");
2625 let sql = crate::sql::emit(&ir).sql;
2626 assert!(sql.contains("ARRAY(SELECT"), "a walk off many rows is a set:\n{sql}");
2627 }
2628
2629 #[test]
2631 fn test_a_multi_link_step_is_a_set_even_off_a_pinned_row() {
2632 let schema = make_schema();
2633 let ast =
2634 parse::parse("WITH i := (SELECT Person FILTER .id = <uuid>$0) SELECT { t := i.posts.title }").unwrap();
2635 let ir = super::compile(&ast, &schema).expect("compile failed");
2636 let sql = crate::sql::emit(&ir).sql;
2637 assert!(sql.contains("ARRAY(SELECT"), "a multi-link step is a set:\n{sql}");
2638 }
2639
2640 #[test]
2646 fn test_a_plain_read_does_not_see_the_statements_own_write() {
2647 let schema = make_schema();
2652 let ast =
2653 parse::parse("WITH made := (INSERT Person { name := 'a', age := 1 }) SELECT { after := count(Person) }")
2654 .unwrap();
2655 let ir = super::compile(&ast, &schema).expect("compile failed");
2656 let sql = crate::sql::emit(&ir).sql;
2657 assert!(
2658 sql.to_lowercase().contains("from \"public\".\"person\""),
2659 "the count must read the table, not the CTE that wrote to it:\n{sql}"
2660 );
2661 }
2662
2663 #[test]
2664 fn test_a_walk_off_a_mutation_sees_the_rows_it_just_wrote() {
2665 let schema = make_schema();
2666 let ast = parse::parse(
2667 "SELECT (UPDATE Person FILTER .name = 'a' SET { posts += (INSERT Post { title := 't' }) }).posts { title }",
2668 )
2669 .unwrap();
2670 let ir = super::compile(&ast, &schema).expect("compile failed");
2671 let sql = crate::sql::emit(&ir).sql;
2672 assert!(
2673 sql.contains("__ml_add_0\" AS \"") || sql.contains("JOIN \"_nested_dml_1__ml_add_0\""),
2674 "the junction must be read from the CTE that wrote it:\n{sql}"
2675 );
2676 assert!(
2677 !sql.contains("JOIN \"public\".\"Post\""),
2678 "the targets must come from their own CTE, not the base table:\n{sql}"
2679 );
2680 }
2681
2682 #[test]
2687 fn test_a_type_intersection_may_follow_a_sub_select() {
2688 let schema = make_schema();
2689 let ast = parse::parse("SELECT (SELECT Person LIMIT 1).company[is Company].name").unwrap();
2690 let ir = super::compile(&ast, &schema).expect("compile failed");
2691 let sql = crate::sql::emit(&ir).sql;
2692 assert!(
2696 !sql.contains("->'name'"),
2697 "the field must not be jsonb off an id:\n{sql}"
2698 );
2699 assert!(sql.contains("\"name\""), "the field must be read as a column:\n{sql}");
2700 }
2701
2702 #[test]
2705 fn test_a_type_intersection_on_a_value_is_rejected() {
2706 let schema = make_schema();
2707 let ast = parse::parse("SELECT (1 + 2)[is Company]").unwrap();
2708 let Err(error) = super::compile(&ast, &schema) else {
2709 panic!("a type intersection on a number is not meaningful");
2710 };
2711 assert!(error.to_string().contains("needs a path, a binding"), "got: {error}");
2712 }
2713
2714 #[test]
2720 fn test_a_select_may_wrap_a_nested_union_subject_select() {
2721 let schema = make_schema();
2722 let ast =
2723 parse::parse("SELECT (SELECT (Person.company UNION Person.company) { name } LIMIT 1) { name }").unwrap();
2724 let error = super::compile(&ast, &schema)
2725 .err()
2726 .map(|e| e.to_string())
2727 .unwrap_or_default();
2728 assert!(
2729 !error.contains("expected a type name as SELECT subject"),
2730 "the union subject must be hoisted, got: {error}"
2731 );
2732 }
2733
2734 #[test]
2738 fn test_a_select_subject_may_walk_off_a_mutation() {
2739 let schema = make_schema();
2740 let ast =
2741 parse::parse("SELECT (UPDATE Person FILTER .name = 'a' SET { name := 'b' }).company { name }").unwrap();
2742 let ir = super::compile(&ast, &schema).expect("compile failed");
2743 let sql = crate::sql::emit(&ir).sql;
2744 assert!(
2745 sql.contains("\"_nested_dml_0\" AS ("),
2746 "the mutation must run as a CTE:\n{sql}"
2747 );
2748 assert!(sql.contains("UPDATE"), "the mutation must still run:\n{sql}");
2749 }
2750
2751 #[test]
2756 fn test_an_update_subject_may_walk_off_a_sub_select() {
2757 let schema = make_schema();
2758 let inline = parse::parse("UPDATE (SELECT Person FILTER .name = 'a').company SET { name := 'b' }").unwrap();
2759 let bound =
2760 parse::parse("WITH s := (SELECT Person FILTER .name = 'a') UPDATE s.company SET { name := 'b' }").unwrap();
2761 let inline_err = super::compile(&inline, &schema).err().map(|e| e.to_string());
2765 let bound_err = super::compile(&bound, &schema).err().map(|e| e.to_string());
2766 assert_eq!(inline_err, bound_err, "the two spellings must compile alike");
2767 assert!(
2768 !inline_err
2769 .unwrap_or_default()
2770 .contains("expected a type name as SELECT subject"),
2771 "the sub-select subject must be accepted"
2772 );
2773 }
2774
2775 #[test]
2778 fn test_a_for_loop_may_be_a_select_subject() {
2779 let schema = make_schema();
2780 let ast = parse::parse("SELECT (FOR s IN {1, 2} UNION (SELECT { a := s }))").unwrap();
2781 let ir = super::compile(&ast, &schema).expect("compile failed");
2782 assert!(matches!(ir.stmt, super::IrStmt::For(_)), "expected the loop itself");
2783 }
2784
2785 #[test]
2790 fn test_a_json_cast_in_a_shape_is_a_named_pointer() {
2791 use crate::query::ShapeNode;
2792 let schema = make_schema();
2793 let ast = parse::parse("SELECT Person { j := <json>.name }").unwrap();
2794 let ir = super::compile(&ast, &schema).expect("compile failed");
2795 let shape = crate::sql::emit(&ir).shape;
2796 let ShapeNode::Object { pointers, .. } = &shape.root else {
2797 panic!("expected an object shape, got {:?}", shape.root);
2798 };
2799 let pointer = pointers
2800 .iter()
2801 .find(|node| matches!(node, ShapeNode::Scalar { name, .. } if name == "j"))
2802 .unwrap_or_else(|| panic!("no scalar pointer named 'j' in {pointers:?}"));
2803 assert!(matches!(pointer, ShapeNode::Scalar { .. }));
2804 }
2805
2806 #[test]
2809 fn test_a_top_level_json_cast_stays_a_root_shaped_node() {
2810 use crate::query::ShapeNode;
2811 let schema = make_schema();
2812 let ast = parse::parse("SELECT <json>'x'").unwrap();
2813 let ir = super::compile(&ast, &schema).expect("compile failed");
2814 let shape = crate::sql::emit(&ir).shape;
2815 assert!(matches!(shape.root, ShapeNode::JsonScalar), "got {:?}", shape.root);
2816 }
2817
2818 #[test]
2821 fn test_stdlib_enum_member_compiles_to_a_text_literal() {
2822 let schema = make_schema();
2823 let ast = parse::parse("SELECT std::Endian.Big").unwrap();
2824 let ir = super::compile(&ast, &schema).expect("compile failed");
2825 let sql = crate::sql::emit(&ir).sql;
2826 assert!(sql.contains("'Big'::text"), "expected a text literal, got: {sql}");
2827 }
2828
2829 #[test]
2830 fn test_unknown_stdlib_enum_member_is_rejected() {
2831 let schema = make_schema();
2832 let ast = parse::parse("SELECT std::Endian.Middle").unwrap();
2833 let Err(error) = super::compile(&ast, &schema) else {
2834 panic!("Middle is not a member of std::Endian");
2835 };
2836 assert!(error.to_string().contains("has no member 'Middle'"), "got: {error}");
2837 }
2838
2839 #[test]
2844 fn test_to_bytes_of_a_uuid_selects_the_uuid_overload() {
2845 let schema = make_schema();
2846 let ast = parse::parse("SELECT std::to_int32(std::to_bytes(<uuid>$0)[12:16], std::Endian.Big)").unwrap();
2847 let ir = super::compile(&ast, &schema).expect("compile failed");
2848 let sql = crate::sql::emit(&ir).sql;
2849 assert!(sql.contains("to_bytes_uuid"), "expected to_bytes_uuid, got: {sql}");
2850 assert!(sql.contains("to_int32_bytes"), "expected to_int32_bytes, got: {sql}");
2851 }
2852
2853 #[test]
2857 fn a_stdlib_call_with_the_wrong_argument_count_is_rejected() {
2858 let schema = make_schema();
2859 let ast = parse::parse("SELECT std::str_lower('A', 'B')").unwrap();
2860 let Err(err) = super::compile(&ast, &schema) else {
2861 panic!("wrong arity must not compile")
2862 };
2863 let msg = err.to_string();
2864 assert!(msg.contains("std::str_lower"), "{msg}");
2865 assert!(msg.contains("takes 1 argument(s), got 2"), "{msg}");
2866 }
2867
2868 #[test]
2872 fn an_unknown_function_suggests_the_closest_real_one() {
2873 let schema = make_schema();
2874 let ast = parse::parse("SELECT std::uuid_generate_v7j()").unwrap();
2875 let Err(err) = super::compile(&ast, &schema) else {
2876 panic!("an unknown function must not compile")
2877 };
2878 let msg = err.to_string();
2879 assert!(msg.contains("does not exist"), "{msg}");
2880 assert!(msg.contains("did you mean std::uuid_generate_v7()?"), "{msg}");
2881 }
2882
2883 #[test]
2887 fn a_function_in_another_namespace_says_where_it_lives() {
2888 let schema = make_schema();
2889 let ast = parse::parse("SELECT std::pi()").unwrap();
2890 let Err(err) = super::compile(&ast, &schema) else {
2891 panic!("pi lives in math, not std")
2892 };
2893 assert!(err.to_string().contains("it lives in math, use math::pi()"), "{err}");
2894 }
2895
2896 #[test]
2899 fn an_arity_error_names_every_arity_the_overload_set_accepts() {
2900 let schema = make_schema();
2901 let ast = parse::parse("SELECT std::str_trim('A', 'B', 'C')").unwrap();
2902 let Err(err) = super::compile(&ast, &schema) else {
2903 panic!("wrong arity must not compile")
2904 };
2905 assert!(err.to_string().contains("takes 1 or 2 argument(s), got 3"), "{err}");
2906 }
2907
2908 #[test]
2911 fn a_variadic_stdlib_call_accepts_extra_arguments() {
2912 let schema = make_schema();
2913 for query in [
2914 "SELECT std::json_get(<json>$0, 'a')",
2915 "SELECT std::json_get(<json>$0, 'a', 'b', 'c')",
2916 ] {
2917 let ast = parse::parse(query).unwrap();
2918 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2919 }
2920 }
2921
2922 #[test]
2925 fn a_stdlib_call_with_an_unacceptable_argument_type_is_rejected() {
2926 let schema = make_schema();
2927 let ast = parse::parse("SELECT std::str_lower(<int64>$0)").unwrap();
2928 let Err(err) = super::compile(&ast, &schema) else {
2929 panic!("wrong argument type must not compile")
2930 };
2931 let msg = err.to_string();
2932 assert!(msg.contains("no overload accepting (int8)"), "{msg}");
2933 assert!(msg.contains("(str)"), "{msg}");
2934 }
2935
2936 #[test]
2941 fn a_stdlib_call_over_a_coalesce_or_conditional_resolves_its_branch_type() {
2942 let schema = make_schema();
2943 for query in [
2944 "SELECT std::str_lower(<optional str>$0 ?? 'DE')",
2945 "SELECT std::str_lower('DE' ?? <optional str>$0)",
2946 "WITH j := (<optional str>$0 ?? 'DE') SELECT std::str_lower(j)",
2947 "SELECT std::str_lower(<str>$0 if <bool>$1 else 'DE')",
2948 "SELECT std::len(<optional str>$0 ?? 'DE')",
2949 ] {
2950 let ast = parse::parse(query).unwrap();
2951 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2952 }
2953 }
2954
2955 #[test]
2960 fn a_stdlib_call_over_a_pass_through_expression_resolves_the_value_type() {
2961 let schema = make_schema();
2962 for query in [
2963 "SELECT (FOR code IN std::array_unpack(<array<str>>$0) UNION (SELECT std::str_lower(code)))",
2965 "WITH x := { a := 'DE' } SELECT std::str_lower(x.a)",
2967 "SELECT math::abs(-3)",
2969 "SELECT math::abs(-(<int64>$0))",
2970 "SELECT std::str_lower((<array<str>>$0)[0])",
2972 "SELECT std::str_title(std::str_split(<str>$0, '::')[0])",
2974 "SELECT std::duration_to_seconds(std::datetime_of_transaction() - <datetime>$0)",
2976 "SELECT std::duration_to_seconds(<duration>$0 + <duration>$0)",
2977 ] {
2978 let ast = parse::parse(query).unwrap();
2979 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2980 }
2981 }
2982
2983 #[test]
2986 fn a_stdlib_call_over_a_coalesce_of_the_wrong_type_is_still_rejected() {
2987 let schema = make_schema();
2988 let ast = parse::parse("SELECT std::str_lower(<optional int64>$0 ?? 3)").unwrap();
2989 let Err(err) = super::compile(&ast, &schema) else {
2990 panic!("wrong argument type must not compile")
2991 };
2992 assert!(err.to_string().contains("no overload accepting (int8)"), "{err}");
2993 }
2994
2995 #[test]
2999 fn test_to_int16_and_to_int64_over_bytes_select_the_bytes_overload() {
3000 let schema = make_schema();
3001 for (query, expected) in [
3002 (
3003 "SELECT std::to_int16(std::to_bytes(<uuid>$0)[14:16], std::Endian.Big)",
3004 "to_int16_bytes",
3005 ),
3006 (
3007 "SELECT std::to_int64(std::to_bytes(<uuid>$0)[0:8], std::Endian.Little)",
3008 "to_int64_bytes",
3009 ),
3010 ] {
3011 let ast = parse::parse(query).unwrap();
3012 let ir = super::compile(&ast, &schema).expect("compile failed");
3013 let sql = crate::sql::emit(&ir).sql;
3014 assert!(sql.contains(expected), "expected {expected}, got: {sql}");
3015 }
3016 }
3017
3018 #[test]
3019 fn test_positional_param_names() {
3020 let schema = make_schema();
3021 let ast = parse::parse("SELECT Person FILTER .name = $0").unwrap();
3022 let ir = super::compile(&ast, &schema).expect("compile failed");
3023 assert_eq!(ir.params, vec!["0"]);
3024 }
3025
3026 #[test]
3027 fn test_multiple_positional_param_names_in_order() {
3028 let schema = make_schema();
3029 let ast = parse::parse("SELECT Person FILTER .name = $0 AND .age > $1").unwrap();
3030 let ir = super::compile(&ast, &schema).expect("compile failed");
3031 assert_eq!(ir.params, vec!["0", "1"]);
3032 }
3033
3034 #[test]
3035 fn test_repeated_positional_param_single_slot() {
3036 let schema = make_schema();
3037 let ast = parse::parse("SELECT Person FILTER .name = $0 OR .name = $0").unwrap();
3038 let ir = super::compile(&ast, &schema).expect("compile failed");
3039 assert_eq!(ir.params, vec!["0"], "repeated $0 must occupy a single slot");
3040 }
3041
3042 #[derive(Debug, PartialEq)]
3047 struct ParamTupleKeys {
3048 is_array: bool,
3049 type_name: Option<String>,
3050 keys: Vec<String>,
3051 }
3052
3053 fn param_tuple_keys(query: &str, schema: &SchemaDescriptor) -> Vec<Option<ParamTupleKeys>> {
3054 let ast = parse::parse(query).unwrap();
3055 let ir = super::compile(&ast, schema).expect("compile failed");
3056 ir.param_tuple_types
3057 .iter()
3058 .map(|plan| {
3059 plan.as_ref().map(|plan| ParamTupleKeys {
3060 is_array: plan.is_array,
3061 type_name: plan.type_name.clone(),
3062 keys: plan.members.iter().map(|m| m.key.clone().unwrap_or_default()).collect(),
3063 })
3064 })
3065 .collect()
3066 }
3067
3068 #[test]
3069 fn test_a_tuple_cast_records_its_member_names_against_the_parameter() {
3070 let schema = make_schema();
3071 assert_eq!(
3072 param_tuple_keys("SELECT <tuple<street: str, zip: str>>$address", &schema),
3073 vec![Some(ParamTupleKeys {
3074 is_array: false,
3075 type_name: None,
3076 keys: vec!["street".to_string(), "zip".to_string()],
3077 })]
3078 );
3079 }
3080
3081 #[test]
3082 fn test_an_array_of_tuples_cast_records_one_element_s_member_names() {
3083 let schema = make_schema();
3084 assert_eq!(
3085 param_tuple_keys("SELECT <array<tuple<name: str, value: str>>>$headers", &schema),
3086 vec![Some(ParamTupleKeys {
3087 is_array: true,
3088 type_name: None,
3089 keys: vec!["name".to_string(), "value".to_string()],
3090 })]
3091 );
3092 }
3093
3094 #[test]
3095 fn test_a_nominal_named_tuple_cast_records_its_type_name_too() {
3096 use crate::schema::{NamedTupleDescriptor, TupleMemberDescriptor, TupleMemberKind};
3097 let mut schema = make_schema();
3098 schema.named_tuples.push(NamedTupleDescriptor {
3099 name: "Point".into(),
3100 module: "default".into(),
3101 members: vec![
3102 TupleMemberDescriptor {
3103 name: Some("x".into()),
3104 kind: TupleMemberKind::Scalar {
3105 pg_type: "float8".into(),
3106 },
3107 },
3108 TupleMemberDescriptor {
3109 name: Some("y".into()),
3110 kind: TupleMemberKind::Scalar {
3111 pg_type: "float8".into(),
3112 },
3113 },
3114 ],
3115 });
3116 assert_eq!(
3117 param_tuple_keys("SELECT <array<default::Point>>$points", &schema),
3118 vec![Some(ParamTupleKeys {
3119 is_array: true,
3120 type_name: Some("default::Point".to_string()),
3121 keys: vec!["x".to_string(), "y".to_string()],
3122 })]
3123 );
3124 }
3125
3126 #[test]
3127 fn test_a_parameter_cast_to_a_plain_scalar_records_no_tuple_plan() {
3128 let schema = make_schema();
3129 assert_eq!(param_tuple_keys("SELECT <str>$name", &schema), vec![None]);
3130 }
3131
3132 #[test]
3133 fn test_a_tuple_cast_in_expression_position_records_its_member_names() {
3134 let schema = make_schema();
3138 assert_eq!(
3139 param_tuple_keys(
3140 "SELECT { name := <str>$name, address := <tuple<street: str, zip: str>>$address }",
3141 &schema
3142 ),
3143 vec![
3144 None,
3145 Some(ParamTupleKeys {
3146 is_array: false,
3147 type_name: None,
3148 keys: vec!["street".to_string(), "zip".to_string()],
3149 })
3150 ]
3151 );
3152 }
3153}