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 GlobalParam {
1068 index: usize,
1069 pg_type: String,
1070 },
1071 GlobalRef {
1073 cte_name: String,
1074 },
1075 Subscript {
1077 expr: Box<IrExpr>,
1078 index: Box<IrExpr>,
1079 is_array: bool,
1080 },
1081 JsonbField {
1083 expr: Box<IrExpr>,
1084 field: String,
1085 },
1086 JsonbIndex {
1090 expr: Box<IrExpr>,
1091 index: usize,
1092 },
1093 Slice {
1095 expr: Box<IrExpr>,
1096 lower: Option<Box<IrExpr>>,
1097 upper: Option<Box<IrExpr>>,
1098 is_array: bool,
1099 },
1100 FnSubquery(Box<IrFunctionSelect>),
1106 PathSubquery(Box<IrPathSelect>),
1109 FnParam {
1112 name: String,
1113 pg_type: String,
1114 },
1115 RawSql(String),
1125}
1126
1127#[derive(Debug, Clone)]
1137pub struct IrVectorSearch {
1138 pub source: IrSource,
1140 pub vector_col: String,
1142 pub distance_op: &'static str,
1144 pub query_expr: IrExpr,
1147 pub object_shape: Vec<IrShapePointer>,
1150 pub filter: Option<IrExpr>,
1151 pub order_by_distance: Option<IrSortDir>,
1154 pub offset: Option<IrExpr>,
1155 pub limit: Option<IrExpr>,
1156 pub inference_query_param_name: Option<String>,
1159 pub inference_query_literal: Option<String>,
1161 pub inference_model: Option<String>,
1163 pub inference_type_name: Option<String>,
1165 pub inference_index_name: Option<Option<String>>,
1167}
1168
1169#[derive(Debug, Clone)]
1176pub struct IrFtsSearch {
1177 pub source: IrSource,
1179 pub backend: crate::schema::SearchBackend,
1181 pub search_col: String,
1183 pub tsquery_fn: &'static str,
1185 pub query_expr: IrExpr,
1187 pub object_shape: Vec<IrShapePointer>,
1189 pub filter: Option<IrExpr>,
1190 pub order_by_rank: Option<IrSortDir>,
1191 pub offset: Option<IrExpr>,
1192 pub limit: Option<IrExpr>,
1193 pub deferred_index_name: Option<String>,
1195 pub deferred_query_param_name: Option<String>,
1197 pub deferred_query_literal: Option<String>,
1199 pub deferred_ids_param: Option<usize>,
1201 pub deferred_scores_param: Option<usize>,
1203}
1204
1205#[derive(Debug, Clone)]
1210pub struct IrFunctionSelect {
1211 pub fn_module: String,
1212 pub fn_name: String,
1213 pub fn_args: Vec<IrExpr>,
1214 pub alias: String,
1216 pub type_name: String,
1218 pub polymorphic: bool,
1220 pub poly_implementors: Vec<IrPolyImplementor>,
1222 pub poly_columns: Vec<String>,
1224 pub shape: Vec<IrShapePointer>,
1225 pub filter: Option<IrExpr>,
1226 pub order_by: Vec<IrSort>,
1227 pub offset: Option<IrExpr>,
1228 pub limit: Option<IrExpr>,
1229 pub distinct: bool,
1230}
1231
1232#[derive(Debug, Clone)]
1233pub struct IrBinOp {
1234 pub left: IrExpr,
1235 pub op: BinOpKind,
1236 pub right: IrExpr,
1237}
1238
1239#[derive(Debug, Clone)]
1240pub struct IrUnaryOp {
1241 pub op: UnaryOpKind,
1242 pub operand: IrExpr,
1243}
1244
1245#[derive(Debug, Clone)]
1246pub struct IrFunctionCall {
1247 pub return_pg_type: Option<String>,
1261 pub schema: Option<String>,
1262 pub name: String,
1263 pub args: Vec<IrExpr>,
1264 pub sql_template: Option<String>,
1267}
1268
1269#[derive(Debug, Clone)]
1270pub struct IrTypeCast {
1271 pub expr: IrExpr,
1272 pub pg_type: String,
1274 pub tuple_shape: Option<TupleCastShape>,
1279}
1280
1281#[derive(Debug, Clone)]
1282pub struct TupleCastShape {
1283 pub type_name: Option<String>,
1286 pub members: Vec<crate::query::JsonMember>,
1287}
1288
1289#[derive(Debug, Clone)]
1290pub struct IrIfElse {
1291 pub condition: IrExpr,
1292 pub if_: IrExpr,
1293 pub else_: IrExpr,
1294}
1295
1296#[derive(Debug, Clone)]
1297pub enum IrLiteral {
1298 Str(String),
1299 Int(i64),
1300 Float(f64),
1301 Bool(bool),
1302}
1303
1304#[derive(Debug, Clone)]
1307pub struct IrSort {
1308 pub expr: IrExpr,
1309 pub direction: IrSortDir,
1310 pub nulls: IrNulls,
1311}
1312
1313#[derive(Debug, Clone)]
1314pub enum IrSortDir {
1315 Asc,
1316 Desc,
1317}
1318
1319#[derive(Debug, Clone)]
1320pub enum IrNulls {
1321 First,
1322 Last,
1323}
1324
1325#[derive(Debug, Clone)]
1330pub struct IrRewrite {
1331 pub column: String,
1333 pub expr: IrExpr,
1338}
1339
1340#[derive(Debug, Clone)]
1342pub struct IrCteDef {
1343 pub name: String,
1344 pub stmt: IrStmt,
1345 pub type_name: String,
1348 pub correlated_to: Option<String>,
1353}
1354
1355#[derive(Debug, Clone)]
1357pub struct IrSessionGlobalCte {
1358 pub cte_name: String,
1359 pub qualified_name: String,
1360 pub param_index: usize,
1361 pub pg_type: String,
1362}
1363
1364#[derive(Debug, Clone)]
1366pub struct IrComputedGlobalCte {
1367 pub cte_name: String,
1368 pub qualified_name: String,
1369 pub stmt: IrStmt,
1370}
1371
1372#[derive(Debug, Clone)]
1373pub enum IrGlobalCte {
1374 Session(IrSessionGlobalCte),
1375 Computed(Box<IrComputedGlobalCte>),
1379}
1380
1381impl IrGlobalCte {
1382 pub fn cte_name(&self) -> &str {
1383 match self {
1384 Self::Session(s) => &s.cte_name,
1385 Self::Computed(c) => &c.cte_name,
1386 }
1387 }
1388}
1389
1390pub struct IrOutput {
1394 pub stmt: IrStmt,
1395 pub params: Vec<String>,
1398 pub param_tuple_types: Vec<Option<crate::query::ParamTupleType>>,
1401 pub ctes: Vec<IrCteDef>,
1403 pub global_ctes: Vec<IrGlobalCte>,
1405 pub warnings: Vec<String>,
1407 pub uses_globals_arg: bool,
1411 pub subtype_fanouts: HashMap<(String, String), IrPolyFanout>,
1415}
1416
1417pub type QualifiedTable = (String, String);
1419
1420const INHERITED_JUNCTION: &str = "@inherited:";
1424
1425pub fn inherited_junction(tables: &[QualifiedTable], columns: &[String]) -> String {
1428 let tables = tables
1429 .iter()
1430 .map(|(module, table)| format!("{module}\u{1f}{table}"))
1431 .collect::<Vec<_>>()
1432 .join("\u{1e}");
1433 format!("{INHERITED_JUNCTION}{tables}\u{1d}{}", columns.join("\u{1f}"))
1434}
1435
1436pub fn parse_inherited_junction(name: &str) -> Option<(Vec<QualifiedTable>, Vec<String>)> {
1439 let (tables, columns) = name.strip_prefix(INHERITED_JUNCTION)?.split_once('\u{1d}')?;
1440 let tables = tables
1441 .split('\u{1e}')
1442 .filter_map(|entry| entry.split_once('\u{1f}'))
1443 .map(|(module, table)| (module.to_string(), table.to_string()))
1444 .collect();
1445 Some((tables, columns.split('\u{1f}').map(str::to_string).collect()))
1446}
1447
1448#[cfg(test)]
1449mod tests {
1450 use super::*;
1451 #[allow(unused_imports)]
1452 use super::{IrFreeExpr, IrLiteral};
1453 use crate::parse;
1454 use crate::schema::{
1455 ChannelDescriptor, ChannelPayload, ComputedDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
1456 PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
1457 };
1458
1459 fn make_schema() -> SchemaDescriptor {
1460 SchemaDescriptor {
1461 types: vec![
1462 TypeDescriptor {
1463 name: "Person".into(),
1464 module: "default".into(),
1465 table: "person".into(),
1466 abstract_: false,
1467 materialized: false,
1468 description: None,
1469 parents: vec![],
1470 interfaces: vec![],
1471 bases: vec![],
1472 properties: vec![
1473 PropertyDescriptor {
1474 name: "id".into(),
1475 pg_type: "uuid".into(),
1476 nullable: false,
1477 default_sql: Some("uuidv7()".into()),
1478 default_pyql: None,
1479 description: None,
1480 check_constraints: vec![],
1481 is_exclusive: true,
1482 is_pk: true,
1483 is_readonly: true,
1484 rewrites: vec![],
1485 tuple_members: None,
1486 column_type: None,
1487 },
1488 PropertyDescriptor {
1489 name: "name".into(),
1490 pg_type: "text".into(),
1491 nullable: false,
1492 default_sql: None,
1493 default_pyql: None,
1494 description: None,
1495 check_constraints: vec![],
1496 is_exclusive: false,
1497 is_pk: false,
1498 is_readonly: false,
1499 rewrites: vec![],
1500 tuple_members: None,
1501 column_type: None,
1502 },
1503 PropertyDescriptor {
1504 name: "age".into(),
1505 pg_type: "int8".into(),
1506 nullable: true,
1507 default_sql: None,
1508 default_pyql: None,
1509 description: None,
1510 check_constraints: vec![],
1511 is_exclusive: false,
1512 is_pk: false,
1513 is_readonly: false,
1514 rewrites: vec![],
1515 tuple_members: None,
1516 column_type: None,
1517 },
1518 ],
1519 links: vec![LinkDescriptor {
1520 name: "company".into(),
1521 target: "default::Company".into(),
1522 nullable: true,
1523 through: None,
1524 description: None,
1525 default_pyql: None,
1526 is_exclusive: false,
1527 is_readonly: false,
1528 rewrites: vec![],
1529 on_delete: vec![],
1530 }],
1531 multilinks: vec![MultiLinkDescriptor {
1532 name: "posts".into(),
1533 target: "default::Post".into(),
1534 through: None,
1535 nullable: false,
1536 description: None,
1537 default_pyql: None,
1538 on_delete: vec![],
1539 is_exclusive: false,
1540 }],
1541 computed: vec![],
1542 constraints: vec![],
1543 indexes: vec![],
1544 partition: None,
1545 vector_indexes: vec![],
1546 search_indexes: vec![],
1547 triggers: vec![],
1548 junction: false,
1549 signals: vec![],
1550 },
1551 TypeDescriptor {
1552 name: "Company".into(),
1553 module: "default".into(),
1554 table: "company".into(),
1555 abstract_: false,
1556 materialized: false,
1557 description: None,
1558 parents: vec![],
1559 interfaces: vec![],
1560 bases: vec![],
1561 properties: vec![PropertyDescriptor {
1562 name: "name".into(),
1563 pg_type: "text".into(),
1564 nullable: false,
1565 default_sql: None,
1566 default_pyql: None,
1567 description: None,
1568 check_constraints: vec![],
1569 is_exclusive: false,
1570 is_pk: false,
1571 is_readonly: false,
1572 rewrites: vec![],
1573 tuple_members: None,
1574 column_type: None,
1575 }],
1576 links: vec![],
1577 multilinks: vec![],
1578 computed: vec![],
1579 constraints: vec![],
1580 indexes: vec![],
1581 partition: None,
1582 vector_indexes: vec![],
1583 search_indexes: vec![],
1584 triggers: vec![],
1585 junction: false,
1586 signals: vec![],
1587 },
1588 TypeDescriptor {
1589 name: "Post".into(),
1590 module: "default".into(),
1591 table: "post".into(),
1592 abstract_: false,
1593 materialized: false,
1594 description: None,
1595 parents: vec![],
1596 interfaces: vec![],
1597 bases: vec![],
1598 properties: vec![PropertyDescriptor {
1599 name: "title".into(),
1600 pg_type: "text".into(),
1601 nullable: false,
1602 default_sql: None,
1603 default_pyql: None,
1604 description: None,
1605 check_constraints: vec![],
1606 is_exclusive: false,
1607 is_pk: false,
1608 is_readonly: false,
1609 rewrites: vec![],
1610 tuple_members: None,
1611 column_type: None,
1612 }],
1613 links: vec![],
1614 multilinks: vec![],
1615 computed: vec![],
1616 constraints: vec![],
1617 indexes: vec![],
1618 partition: None,
1619 vector_indexes: vec![],
1620 search_indexes: vec![],
1621 triggers: vec![],
1622 junction: false,
1623 signals: vec![],
1624 },
1625 ],
1626 scalars: vec![],
1627 enums: vec![],
1628 named_tuples: vec![],
1629 globals: vec![],
1630 functions: vec![],
1631 aliases: vec![],
1632 channels: vec![],
1633 ..Default::default()
1634 }
1635 }
1636
1637 fn compile(query: &str) -> IrOutput {
1638 let schema = make_schema();
1639 let ast = parse::parse(query).expect("parse failed");
1640 super::compile(&ast, &schema).expect("IR compile failed")
1641 }
1642
1643 fn bound(sel: &IrSelect) -> (&IrSource, &[IrShapePointer]) {
1647 match sel.rows.as_slice() {
1648 [IrRowSource::Bound { source, shape }] => (source, shape),
1649 _ => panic!("expected a single schema-bound row"),
1650 }
1651 }
1652
1653 fn free_items(sel: &IrSelect) -> Vec<&IrFreeExpr> {
1656 sel.rows
1657 .iter()
1658 .map(|r| match r {
1659 IrRowSource::Free(item) => item,
1660 IrRowSource::Bound { .. } => panic!("expected a free row"),
1661 })
1662 .collect()
1663 }
1664
1665 #[test]
1666 fn test_select_resolves_source() {
1667 let ir = compile("SELECT Person { name, age }");
1668 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1669 let (source, shape) = bound(&sel);
1670 assert_eq!(source.table, "person");
1671 assert_eq!(source.type_name, "default::Person");
1672 assert_eq!(shape.len(), 3);
1674 assert!(matches!(&shape[0], IrShapePointer::Scalar(p) if p.alias == "id" && p.implicit_id));
1675 assert!(matches!(shape[1], IrShapePointer::Scalar(_)));
1676 }
1677
1678 #[test]
1679 fn test_select_filter_param_ordering() {
1680 let ir = compile("SELECT Person { name } FILTER .name = $name AND .age > $min_age");
1681 assert_eq!(ir.params, vec!["name", "min_age"]);
1682 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1683 assert!(sel.filter.is_some());
1684 }
1685
1686 #[test]
1687 fn test_select_single_link() {
1688 let ir = compile("SELECT Person { name, company { name } }");
1689 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1690 let (_, shape) = bound(&sel);
1691 assert_eq!(shape.len(), 3);
1692 let IrShapePointer::SingleLink(link) = &shape[2] else {
1693 panic!("expected SingleLink")
1694 };
1695 assert_eq!(link.alias, "company");
1696 let IrSingleLinkCorrelation::Fk { fk_column, .. } = &link.correlation else {
1697 panic!("expected Fk correlation")
1698 };
1699 assert_eq!(fk_column, "company_id");
1700 assert_eq!(bound(&link.subquery).0.table, "company");
1701 }
1702
1703 #[test]
1704 fn test_select_multi_link() {
1705 let ir = compile("SELECT Person { name, posts { title } }");
1706 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1707 let (_, shape) = bound(&sel);
1708 let IrShapePointer::MultiLink(ml) = &shape[2] else {
1709 panic!("expected MultiLink")
1710 };
1711 assert_eq!(ml.alias, "posts");
1712 assert_eq!(bound(&ml.subquery).0.table, "post");
1713 let IrMultiLinkJoin::Standard { junction_table, .. } = &ml.join else {
1714 panic!()
1715 };
1716 assert_eq!(junction_table, "person.posts");
1717 }
1718
1719 #[test]
1720 fn test_select_no_shape_returns_id_only() {
1721 let ir = compile("SELECT Person");
1722 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1723 let (_, shape) = bound(&sel);
1725 assert_eq!(shape.len(), 1);
1726 let IrShapePointer::Scalar(f) = &shape[0] else { panic!() };
1727 assert_eq!(f.alias, "id");
1728 }
1729
1730 #[test]
1731 fn test_free_select_set_literal() {
1732 let schema = make_schema();
1733 let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
1734 let ir = super::compile(&ast, &schema).unwrap();
1735 let IrStmt::Select(sel) = ir.stmt else {
1736 panic!("expected Select")
1737 };
1738 let items = free_items(&sel);
1739 assert_eq!(items.len(), 3);
1740 assert!(matches!(
1741 items[0],
1742 IrFreeExpr::Scalar(IrExpr::Literal(IrLiteral::Int(1)))
1743 ));
1744 }
1745
1746 #[test]
1747 fn test_free_select_free_object() {
1748 let schema = make_schema();
1749 let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
1750 let ir = super::compile(&ast, &schema).unwrap();
1751 let IrStmt::Select(sel) = ir.stmt else {
1752 panic!("expected Select")
1753 };
1754 let items = free_items(&sel);
1755 assert_eq!(items.len(), 1);
1756 let IrFreeExpr::FreeObject(fields) = &items[0] else {
1757 panic!("expected FreeObject")
1758 };
1759 assert_eq!(fields.len(), 2);
1760 assert_eq!(fields[0].0, "foo");
1761 assert_eq!(fields[1].0, "n");
1762 }
1763
1764 #[test]
1765 fn test_free_select_tuple() {
1766 let schema = make_schema();
1767 let ast = parse::parse("SELECT (1, 'hello')").unwrap();
1768 let ir = super::compile(&ast, &schema).unwrap();
1769 let IrStmt::Select(sel) = ir.stmt else {
1770 panic!("expected Select")
1771 };
1772 let items = free_items(&sel);
1773 assert_eq!(items.len(), 1);
1774 assert!(matches!(items[0], IrFreeExpr::Tuple(_)));
1775 }
1776
1777 #[test]
1778 fn test_free_select_scalar_literal() {
1779 let schema = make_schema();
1780 let ast = parse::parse("SELECT 42").unwrap();
1781 let ir = super::compile(&ast, &schema).unwrap();
1782 let IrStmt::Select(sel) = ir.stmt else {
1783 panic!("expected Select")
1784 };
1785 let items = free_items(&sel);
1786 assert_eq!(items.len(), 1);
1787 assert!(matches!(
1788 items[0],
1789 IrFreeExpr::Scalar(IrExpr::Literal(IrLiteral::Int(42)))
1790 ));
1791 }
1792
1793 #[test]
1794 fn test_free_select_function_call() {
1795 let schema = make_schema();
1796 let ast = parse::parse("SELECT str_lower('HELLO')").unwrap();
1797 let ir = super::compile(&ast, &schema).unwrap();
1798 let IrStmt::Select(sel) = ir.stmt else {
1799 panic!("expected Select")
1800 };
1801 let items = free_items(&sel);
1802 assert!(matches!(items[0], IrFreeExpr::Scalar(IrExpr::FunctionCall(_))));
1803 }
1804
1805 #[test]
1806 fn test_free_select_rejects_dot_path() {
1807 let schema = make_schema();
1808 let ast = parse::parse("SELECT {.name}").unwrap();
1809 assert!(super::compile(&ast, &schema).is_err());
1810 }
1811
1812 #[test]
1813 fn test_type_error_uuid_eq_str() {
1814 let schema = make_schema();
1815 let ast = parse::parse("SELECT Person FILTER .id = 'not-a-uuid'").unwrap();
1816 let err = super::compile(&ast, &schema).err().expect("expected type error");
1817 let msg = err.to_string();
1818 assert!(
1819 msg.contains("std::uuid") && msg.contains("std::str"),
1820 "unexpected: {msg}"
1821 );
1822 }
1823
1824 #[test]
1825 fn test_type_error_str_eq_int() {
1826 let schema = make_schema();
1827 let ast = parse::parse("SELECT Person FILTER .name = 42").unwrap();
1828 let err = super::compile(&ast, &schema).err().expect("expected type error");
1829 let msg = err.to_string();
1830 assert!(
1831 msg.contains("std::str") && msg.contains("std::int64"),
1832 "unexpected: {msg}"
1833 );
1834 }
1835
1836 #[test]
1837 fn test_int_literal_compatible_with_all_int_columns() {
1838 let schema = make_schema();
1840 let ast = parse::parse("SELECT Person FILTER .age = 30").unwrap();
1841 assert!(super::compile(&ast, &schema).is_ok());
1842 }
1843
1844 #[test]
1845 fn test_cast_int16_compatible_with_int8_column() {
1846 let schema = make_schema();
1847 let ast = parse::parse("SELECT Person FILTER .age = <int16>30").unwrap();
1848 assert!(super::compile(&ast, &schema).is_ok());
1849 }
1850
1851 #[test]
1852 fn test_unknown_type_error() {
1853 let schema = make_schema();
1854 let ast = parse::parse("SELECT Ghost { name }").unwrap();
1855 assert!(super::compile(&ast, &schema).is_err());
1856 }
1857
1858 #[test]
1859 fn test_nested_dml_link_value_combines_with_multilink_mutation_in_the_same_update() {
1860 let schema = make_schema();
1870 let ast = parse::parse(
1871 "UPDATE Person FILTER .id = $id SET { \
1872 company := (select (insert Company { name := 'Acme' }) { id }), \
1873 posts += (SELECT Post FILTER .title = $t) \
1874 }",
1875 )
1876 .unwrap();
1877 let ir = super::compile(&ast, &schema).unwrap();
1878 let IrStmt::Update(upd) = ir.stmt else {
1879 panic!("expected Update")
1880 };
1881 assert_eq!(upd.nested_ctes.len(), 1);
1882 assert_eq!(upd.multi_link_appends.len(), 1);
1883 }
1884
1885 #[test]
1886 fn test_unknown_pointer_error() {
1887 let schema = make_schema();
1888 let ast = parse::parse("SELECT Person { nonexistent }").unwrap();
1889 assert!(super::compile(&ast, &schema).is_err());
1890 }
1891
1892 #[test]
1893 fn test_insert_compiles_assignments() {
1894 let ir = compile("INSERT Person { name := 'Alice', age := 30 }");
1895 let IrStmt::Insert(ins) = ir.stmt else { panic!() };
1896 assert_eq!(ins.target.table, "person");
1897 assert_eq!(ins.assignments.len(), 2);
1898 assert_eq!(ins.assignments[0].0, "name");
1899 assert_eq!(ins.assignments[1].0, "age");
1900 }
1901
1902 #[test]
1903 fn test_delete_compiles_filter() {
1904 let ir = compile("DELETE Person FILTER .name = $name");
1905 let IrStmt::Delete(del) = ir.stmt else { panic!() };
1906 assert!(del.filter.is_some());
1907 assert_eq!(ir.params, vec!["name"]);
1908 }
1909
1910 fn make_schema_with_computed() -> SchemaDescriptor {
1911 let mut schema = make_schema();
1912 schema.types[0].computed.push(ComputedDescriptor {
1914 name: "upper_name".into(),
1915 expression: "str_upper(.name)".into(),
1916 return_type: Some("text".into()),
1917 link_target: None,
1918 link_multi: false,
1919 });
1920 schema
1921 }
1922
1923 #[test]
1924 fn test_computed_pointer_in_shape() {
1925 let schema = make_schema_with_computed();
1926 let ast = parse::parse("SELECT Person { upper_name }").unwrap();
1927 let ir = super::compile(&ast, &schema).expect("IR compile failed");
1928 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1929 let (_, shape) = bound(&sel);
1931 assert!(
1932 shape
1933 .iter()
1934 .any(|f| matches!(f, IrShapePointer::Computed(c) if c.alias == "upper_name"))
1935 );
1936 }
1937
1938 #[test]
1939 fn test_computed_pointer_in_expression_context() {
1940 let schema = make_schema_with_computed();
1941 let ast = parse::parse("SELECT Person { x := str_lower(.upper_name) }").unwrap();
1942 let ir = super::compile(&ast, &schema).expect("IR compile failed");
1943 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1944 let (_, shape) = bound(&sel);
1945 assert!(
1946 shape
1947 .iter()
1948 .any(|f| matches!(f, IrShapePointer::Computed(c) if c.alias == "x"))
1949 );
1950 }
1951
1952 #[test]
1953 fn test_count_over_multilink_in_computed_shape_element() {
1954 let ir = compile("SELECT Person { post_count := count(.posts) }");
1959 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1960 let (_, shape) = bound(&sel);
1961 let computed = shape
1962 .iter()
1963 .find_map(|f| match f {
1964 IrShapePointer::Computed(c) if c.alias == "post_count" => Some(c),
1965 _ => None,
1966 })
1967 .expect("expected post_count computed pointer");
1968 assert!(matches!(computed.expr, IrExpr::AggOverQuery { .. }));
1969 }
1970
1971 #[test]
1972 fn test_multi_sort_with_then() {
1973 let ir = compile("SELECT Person { name } ORDER BY .name THEN .age");
1974 let IrStmt::Select(sel) = ir.stmt else { panic!() };
1975 assert_eq!(sel.order_by.len(), 2);
1976 }
1977
1978 #[test]
1979 fn test_multi_link_filter_emits_warning() {
1980 let ir = compile("SELECT Person { name } FILTER .posts.title = 'hello'");
1981 assert!(!ir.warnings.is_empty(), "expected a warning for multi-link in filter");
1982 assert!(ir.warnings[0].contains("posts"));
1983 }
1984
1985 #[test]
1986 fn test_session_global_produces_cte() {
1987 let mut schema = make_schema();
1988 schema.globals.push(GlobalDescriptor {
1989 name: "viewer_id".into(),
1990 module: "default".into(),
1991 scalar_type: "std::uuid".into(),
1992 required: false,
1993 default_expr: None,
1994 computed_expr: None,
1995 });
1996 let ast = parse::parse("SELECT Person FILTER .id = global viewer_id").unwrap();
1997 let ir = super::compile(&ast, &schema).expect("IR compile failed");
1998 assert_eq!(ir.global_ctes.len(), 1);
1999 assert_eq!(ir.global_ctes[0].cte_name(), "__global__default::viewer_id");
2000 assert_eq!(ir.params, vec!["__global__default::viewer_id"]);
2001 }
2002
2003 #[test]
2004 fn test_session_global_pg_type_matches_pyql_type_name() {
2005 let mut schema = make_schema();
2013 schema.globals.push(GlobalDescriptor {
2014 name: "viewer_id".into(),
2015 module: "default".into(),
2016 scalar_type: "std::uuid".into(),
2017 required: false,
2018 default_expr: None,
2019 computed_expr: None,
2020 });
2021 let ast = parse::parse("SELECT Person FILTER .id = global viewer_id").unwrap();
2022 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2023 let IrGlobalCte::Session(session) = &ir.global_ctes[0] else {
2024 panic!("expected a session global CTE");
2025 };
2026 assert_eq!(session.pg_type, "uuid");
2027 }
2028
2029 #[test]
2030 fn test_computed_global_field_access_compiles_as_path_select() {
2031 let mut schema = make_schema();
2036 schema.globals.push(GlobalDescriptor {
2037 name: "current_user".into(),
2038 module: "default".into(),
2039 scalar_type: "Person".into(),
2040 required: false,
2041 default_expr: None,
2042 computed_expr: Some("select default::Person filter .id = <uuid>$session_user_id".into()),
2043 });
2044 let ast = parse::parse("SELECT global current_user.id").unwrap();
2045 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2046 let IrStmt::PathSelect(sel) = ir.stmt else {
2047 panic!("expected a path select, not a free select")
2048 };
2049 assert_eq!(sel.root.type_name, "default::Person");
2050 }
2051
2052 #[test]
2053 fn test_subquery_field_access_compiles_as_path_select() {
2054 let ast = parse::parse("SELECT (SELECT default::Person FILTER .age > 20).name").unwrap();
2059 let schema = make_schema();
2060 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2061 let IrStmt::PathSelect(sel) = ir.stmt else {
2062 panic!("expected a path select, not a free select")
2063 };
2064 assert_eq!(sel.root.type_name, "default::Person");
2065 }
2066
2067 #[test]
2068 fn test_string_index_compiles() {
2069 let ast = parse::parse("SELECT 'hello'[1]").unwrap();
2070 let schema = make_schema();
2071 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2072 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2073 let items = free_items(&sel);
2074 assert!(matches!(
2075 items[0],
2076 IrFreeExpr::Scalar(IrExpr::Subscript { is_array: false, .. })
2077 ));
2078 }
2079
2080 #[test]
2081 fn test_array_index_compiles() {
2082 let ast = parse::parse("SELECT [1, 2, 3][0]").unwrap();
2083 let schema = make_schema();
2084 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2085 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2086 let items = free_items(&sel);
2087 assert!(matches!(
2088 items[0],
2089 IrFreeExpr::Scalar(IrExpr::Subscript { is_array: true, .. })
2090 ));
2091 }
2092
2093 #[test]
2094 fn test_string_slice_compiles() {
2095 let ast = parse::parse("SELECT 'hello'[1:3]").unwrap();
2096 let schema = make_schema();
2097 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2098 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2099 let items = free_items(&sel);
2100 assert!(matches!(
2101 items[0],
2102 IrFreeExpr::Scalar(IrExpr::Slice { is_array: false, .. })
2103 ));
2104 }
2105
2106 #[test]
2107 fn test_array_slice_compiles() {
2108 let ast = parse::parse("SELECT [1, 2, 3][0:2]").unwrap();
2109 let schema = make_schema();
2110 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2111 let IrStmt::Select(sel) = ir.stmt else { panic!() };
2112 let items = free_items(&sel);
2113 assert!(matches!(
2114 items[0],
2115 IrFreeExpr::Scalar(IrExpr::Slice { is_array: true, .. })
2116 ));
2117 }
2118
2119 fn make_schema_with_alias() -> SchemaDescriptor {
2120 use crate::schema::AliasDescriptor;
2121 let mut schema = make_schema();
2122 schema.aliases.push(AliasDescriptor {
2123 name: "ActivePersons".into(),
2124 module: "default".into(),
2125 expr: "select Person filter .age >= 18".into(),
2126 });
2127 schema
2128 }
2129
2130 fn make_schema_with_sequence() -> crate::schema::SchemaDescriptor {
2131 use crate::schema::ScalarDescriptor;
2132 let mut schema = make_schema();
2133 schema.scalars.push(ScalarDescriptor {
2134 name: "OrderNumber".into(),
2135 module: "default".into(),
2136 base: "Sequence".into(),
2137 pg_type: "int8".into(),
2138 check_constraints: vec![],
2139 is_sequence: true,
2140 });
2141 schema
2142 }
2143
2144 fn make_schema_with_channels() -> SchemaDescriptor {
2145 let mut schema = make_schema();
2146 schema.channels.push(ChannelDescriptor {
2147 name: "Pings".into(),
2148 module: "default".into(),
2149 wire_name: "default__pings".into(),
2150 payload: ChannelPayload::Scalar("text".into()),
2151 description: None,
2152 });
2153 schema.channels.push(ChannelDescriptor {
2154 name: "SearchReady".into(),
2155 module: "default".into(),
2156 wire_name: "default__search_ready".into(),
2157 payload: ChannelPayload::Object(vec![
2158 ("doc_id".into(), "uuid".into()),
2159 ("score".into(), "float8".into()),
2160 ]),
2161 description: None,
2162 });
2163 schema.channels.push(ChannelDescriptor {
2164 name: "PersonUpdates".into(),
2165 module: "default".into(),
2166 wire_name: "default__person_updates".into(),
2167 payload: ChannelPayload::Type("default::Person".into()),
2168 description: None,
2169 });
2170 schema
2171 }
2172
2173 fn compile_notify_expr(query: &str) -> String {
2174 let schema = make_schema_with_channels();
2175 let ast = parse::parse(query).expect("parse failed");
2176 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2177 let IrStmt::Select(sel) = ir.stmt else {
2178 panic!("expected Select")
2179 };
2180 let items = free_items(&sel);
2181 let IrFreeExpr::Scalar(expr) = items[0] else {
2182 panic!("expected scalar")
2183 };
2184 crate::sql::emit_expr(expr)
2185 }
2186
2187 fn notify_compile_err(query: &str) -> String {
2188 let schema = make_schema_with_channels();
2189 let ast = parse::parse(query).expect("parse failed");
2190 format!(
2191 "{}",
2192 super::compile(&ast, &schema).err().expect("expected a compile error")
2193 )
2194 }
2195
2196 #[test]
2197 fn test_notify_scalar_channel_emits_pg_notify() {
2198 let sql = compile_notify_expr("SELECT notify(Pings, 'hello')");
2199 assert_eq!(sql, "pg_notify('default__pings', ('hello')::text)", "got: {sql}");
2200 }
2201
2202 #[test]
2203 fn test_notify_rejects_unknown_channel() {
2204 let err = notify_compile_err("SELECT notify(NoSuchChannel, 'hi')");
2205 assert!(err.contains("not a known Channel"), "got: {err}");
2206 }
2207
2208 #[test]
2209 fn test_notify_object_channel_emits_jsonb_build_object() {
2210 let sql = compile_notify_expr(
2211 "SELECT notify(SearchReady, { doc_id := <uuid>'3fa85f64-5717-4562-b3fc-2c963f66afa6', score := 0.5 })",
2212 );
2213 assert_eq!(
2214 sql,
2215 "pg_notify('default__search_ready', (jsonb_build_object('doc_id', ('3fa85f64-5717-4562-b3fc-2c963f66afa6')::uuid, 'score', (0.5::float8)))::text)",
2216 "got: {sql}"
2217 );
2218 }
2219
2220 #[test]
2221 fn test_notify_object_channel_rejects_wrong_fields() {
2222 let err = notify_compile_err("SELECT notify(SearchReady, { doc_id := 'x' })");
2223 assert!(
2224 err.contains("payload fields") && err.contains("don't match"),
2225 "got: {err}"
2226 );
2227 }
2228
2229 #[test]
2230 fn test_notify_object_channel_rejects_non_shape_payload() {
2231 let err = notify_compile_err("SELECT notify(SearchReady, 'not an object')");
2232 assert!(err.contains("free object literal"), "got: {err}");
2233 }
2234
2235 #[test]
2236 fn test_notify_type_channel_rejects_arbitrary_payload() {
2237 let err = notify_compile_err("SELECT notify(PersonUpdates, 'not an anchor')");
2238 assert!(err.contains("must name an object of that type"), "got: {err}");
2239 }
2240
2241 #[test]
2242 fn notify_composes_with_a_with_block_binding() {
2243 let sql = compile_notify_expr(
2248 "WITH updated := (UPDATE Person FILTER .id = <uuid>$id SET { name := 'x' }) \
2249 SELECT notify(PersonUpdates, updated)",
2250 );
2251 assert!(sql.contains("pg_notify"), "got: {sql}");
2252 assert!(sql.contains("\"id\""), "payload should be the CTE's id: {sql}");
2254 assert!(sql.contains("updated"), "should reference the with-block CTE: {sql}");
2255 }
2256
2257 #[test]
2258 fn notify_rejects_a_with_block_binding_of_the_wrong_type() {
2259 let err = notify_compile_err("WITH other := (SELECT Company) SELECT notify(PersonUpdates, other)");
2260 assert!(err.contains("expects a payload of type"), "got: {err}");
2261 }
2262
2263 #[test]
2264 fn test_notify_type_channel_via_trigger_new_anchor() {
2265 let schema = make_schema_with_channels();
2266 let ir_out = super::compile_trigger_handler(
2267 "select notify(PersonUpdates, __new__)",
2268 "Person",
2269 1, &schema,
2271 )
2272 .expect("trigger handler compile failed");
2273 let IrStmt::Select(sel) = ir_out.stmt else {
2274 panic!("expected Select")
2275 };
2276 let items = free_items(&sel);
2277 let IrFreeExpr::Scalar(expr) = items[0] else {
2278 panic!("expected scalar")
2279 };
2280 let sql = crate::sql::emit_expr(expr);
2281 assert_eq!(
2282 sql, "pg_notify('default__person_updates', (NEW.\"id\")::text)",
2283 "got: {sql}"
2284 );
2285 }
2286
2287 #[test]
2288 fn test_notify_scalar_channel_via_trigger_new_property_access() {
2289 let schema = make_schema_with_channels();
2296 let ir_out = super::compile_trigger_handler(
2297 "select notify(Pings, __new__.name)",
2298 "Person",
2299 1, &schema,
2301 )
2302 .expect("trigger handler compile failed");
2303 let IrStmt::Select(sel) = ir_out.stmt else {
2304 panic!("expected Select")
2305 };
2306 let items = free_items(&sel);
2307 let IrFreeExpr::Scalar(expr) = items[0] else {
2308 panic!("expected scalar")
2309 };
2310 let sql = crate::sql::emit_expr(expr);
2311 assert_eq!(sql, "pg_notify('default__pings', (NEW.\"name\")::text)", "got: {sql}");
2312 }
2313
2314 #[test]
2315 fn test_notify_type_channel_rejects_bare_reference_outside_trigger() {
2316 let err = notify_compile_err("SELECT notify(PersonUpdates, __new__)");
2318 assert!(err.contains("only bound inside a trigger handler"), "got: {err}");
2319 }
2320
2321 #[test]
2322 fn notify_rejects_an_oversized_concatenation_at_compile_time() {
2323 let half = "x".repeat(4500);
2327 let err = notify_compile_err(&format!("SELECT notify_raw('c', '{half}' ++ '{half}')"));
2328 assert!(err.contains("8000-byte"), "got: {err}");
2329 assert!(err.contains("at least"), "got: {err}");
2330 }
2331
2332 #[test]
2333 fn notify_allows_a_concatenation_that_still_fits() {
2334 let part = "x".repeat(3000);
2335 let sql = compile_notify_expr(&format!("SELECT notify_raw('c', '{part}' ++ '{part}')"));
2336 assert!(sql.contains("pg_notify"), "got: {sql}");
2337 }
2338
2339 #[test]
2340 fn test_notify_raw_emits_pg_notify_with_two_args() {
2341 let sql = compile_notify_expr("SELECT notify_raw('any_channel', 'raw payload')");
2342 assert_eq!(sql, "pg_notify('any_channel', 'raw payload')", "got: {sql}");
2343 }
2344
2345 #[test]
2346 fn test_notify_payload_literal_over_cap_rejected() {
2347 let huge = "x".repeat(8000);
2348 let err = notify_compile_err(&format!("SELECT notify(Pings, '{huge}')"));
2349 assert!(err.contains("NOTIFY payload limit"), "got: {err}");
2350 }
2351
2352 #[test]
2353 fn test_notify_arity_error() {
2354 let err = notify_compile_err("SELECT notify(Pings)");
2355 assert!(err.contains("takes exactly 2 arguments"), "got: {err}");
2356 }
2357
2358 fn compile_seq(query: &str) -> String {
2359 let schema = make_schema_with_sequence();
2360 let ast = parse::parse(query).expect("parse failed");
2361 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2362 let IrStmt::Select(sel) = ir.stmt else {
2363 panic!("expected Select")
2364 };
2365 let items = free_items(&sel);
2366 let IrFreeExpr::Scalar(expr) = items[0] else {
2367 panic!("expected scalar")
2368 };
2369 crate::sql::emit_expr(expr)
2370 }
2371
2372 #[test]
2373 fn test_sequence_next_emits_nextval() {
2374 let sql = compile_seq("SELECT sequence_next(OrderNumber)");
2375 assert_eq!(sql, r#"nextval('"default"."OrderNumber_seq"')"#, "got: {sql}");
2376 }
2377
2378 #[test]
2379 fn test_sequence_reset_no_val_emits_setval_initial() {
2380 let sql = compile_seq("SELECT sequence_reset(OrderNumber)");
2381 assert_eq!(sql, r#"setval('"default"."OrderNumber_seq"', 1, false)"#, "got: {sql}");
2382 }
2383
2384 #[test]
2385 fn test_sequence_reset_with_val_emits_setval() {
2386 let sql = compile_seq("SELECT sequence_reset(OrderNumber, 1000)");
2387 assert_eq!(
2388 sql, r#"setval('"default"."OrderNumber_seq"', 1000, true)"#,
2389 "got: {sql}"
2390 );
2391 }
2392
2393 #[test]
2394 fn test_sequence_next_rejects_non_sequence_type() {
2395 let schema = make_schema();
2396 let ast = parse::parse("SELECT sequence_next(Person)").unwrap();
2397 assert!(super::compile(&ast, &schema).is_err());
2398 }
2399
2400 #[test]
2401 fn test_alias_bare_compiles_to_type_select() {
2402 let schema = make_schema_with_alias();
2403 let ast = parse::parse("SELECT ActivePersons").unwrap();
2404 let ir = super::compile(&ast, &schema).expect("compile failed");
2405 let sql = crate::sql::emit(&ir).sql;
2406 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2407 assert!(sql.contains("18"), "expected age filter, got: {sql}");
2408 }
2409
2410 #[test]
2411 fn test_alias_with_outer_filter_merges() {
2412 let schema = make_schema_with_alias();
2413 let ast = parse::parse("SELECT ActivePersons FILTER .name = 'Alice'").unwrap();
2414 let ir = super::compile(&ast, &schema).expect("compile failed");
2415 let sql = crate::sql::emit(&ir).sql;
2416 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2417 assert!(sql.contains("18"), "expected alias filter, got: {sql}");
2418 assert!(sql.contains("'Alice'"), "expected outer filter, got: {sql}");
2419 }
2420
2421 #[test]
2422 fn test_alias_module_qualified_resolves() {
2423 let schema = make_schema_with_alias();
2424 let ast = parse::parse("SELECT default::ActivePersons").unwrap();
2425 let ir = super::compile(&ast, &schema).expect("compile failed");
2426 let sql = crate::sql::emit(&ir).sql;
2427 assert!(sql.contains("\"person\""), "expected person table, got: {sql}");
2428 }
2429
2430 #[test]
2431 fn test_alias_with_shape() {
2432 let schema = make_schema_with_alias();
2433 let ast = parse::parse("SELECT ActivePersons { name, age }").unwrap();
2434 let ir = super::compile(&ast, &schema).expect("compile failed");
2435 let sql = crate::sql::emit(&ir).sql;
2436 assert!(sql.contains("\"name\""), "expected name pointer, got: {sql}");
2437 assert!(sql.contains("\"age\""), "expected age pointer, got: {sql}");
2438 }
2439
2440 #[test]
2441 fn test_alias_whose_own_body_has_a_shape_plus_outer_shape() {
2442 use crate::schema::AliasDescriptor;
2449 let mut schema = make_schema();
2450 schema.aliases.push(AliasDescriptor {
2451 name: "OldestActive".into(),
2452 module: "default".into(),
2453 expr: "select Person { name } order by .age desc limit 1".into(),
2454 });
2455 let ast = parse::parse("SELECT OldestActive { name, age }").unwrap();
2456 let ir = super::compile(&ast, &schema).expect("compile failed");
2457 let sql = crate::sql::emit(&ir).sql;
2458 assert!(sql.contains("\"name\""), "expected name pointer, got: {sql}");
2459 assert!(sql.contains("\"age\""), "expected age pointer, got: {sql}");
2460 assert!(
2461 sql.contains("ORDER BY") && sql.contains("LIMIT"),
2462 "alias's own order/limit must still apply, got: {sql}"
2463 );
2464 }
2465
2466 #[test]
2470 fn test_a_binding_named_like_a_generated_cte_gets_its_own_name() {
2471 let ast = parse::parse("WITH _dml := (SELECT Person LIMIT 1) SELECT (UPDATE Person FILTER .id = _dml.id SET { age := 1 }) { name }").unwrap();
2472 let ir = super::compile(&ast, &make_schema()).expect("IR compile failed");
2473 let sql = crate::sql::emit(&ir).sql;
2474 assert_eq!(
2475 sql.matches("\"_dml\" AS (").count(),
2476 1,
2477 "the generated wrapper must keep the name to itself:\n{sql}"
2478 );
2479 }
2480
2481 #[test]
2484 fn test_two_sibling_bindings_of_one_name_get_separate_with_names() {
2485 let ast = parse::parse(
2486 "WITH a := (FOR p IN (SELECT Person) UNION (WITH line := (SELECT Person) SELECT line.name)), \
2487 b := (FOR p IN (SELECT Person) UNION (WITH line := (SELECT Post) SELECT line.title)) \
2488 SELECT {a := a, b := b}",
2489 )
2490 .unwrap();
2491 let ir = super::compile(&ast, &make_schema()).expect("IR compile failed");
2492 let sql = crate::sql::emit(&ir).sql;
2493 assert_eq!(
2494 sql.matches("\"line\" AS (").count(),
2495 1,
2496 "one name can only be claimed once:\n{sql}"
2497 );
2498 }
2499
2500 #[test]
2505 fn test_a_computed_inlined_twice_hoists_its_binding_once() {
2506 let mut schema = make_schema();
2507 schema.types[0].computed.push(ComputedDescriptor {
2508 name: "ranked".into(),
2509 expression: "(with ordering := ['a', 'b'] select array_get(ordering, 0))".into(),
2510 return_type: Some("text".into()),
2511 link_target: None,
2512 link_multi: false,
2513 });
2514 let ast = parse::parse("SELECT Person { ranked } FILTER .ranked = 'a'").unwrap();
2515 let ir = super::compile(&ast, &schema).expect("IR compile failed");
2516 let sql = crate::sql::emit(&ir).sql;
2517 assert_eq!(
2518 sql.matches("\"ordering\" AS (").count(),
2519 1,
2520 "the shared binding must be hoisted once:\n{sql}"
2521 );
2522 }
2523
2524 #[test]
2527 fn test_to_duration_takes_its_arguments_by_name() {
2528 let schema = make_schema();
2529 let ast = parse::parse("SELECT std::to_duration(seconds := 90.0)").unwrap();
2530 super::compile(&ast, &schema).expect("named arguments must resolve");
2531 }
2532
2533 #[test]
2539 fn test_an_ordering_comparison_reads_a_set_walk_as_one_value() {
2540 let schema = make_schema();
2541 let ast = parse::parse("SELECT Company FILTER ((.<company[is Person].age < 30) ?? true)").unwrap();
2542 let ir = super::compile(&ast, &schema).expect("compile failed");
2543 let sql = crate::sql::emit(&ir).sql;
2544 assert!(!sql.contains("ARRAY(SELECT"), "the operand must be one value:\n{sql}");
2545 assert!(sql.contains("COALESCE"), "the coalesce must survive:\n{sql}");
2546 }
2547
2548 #[test]
2553 fn test_a_single_link_walk_off_a_pinned_row_is_not_a_set() {
2554 let schema = make_schema();
2555 let ast =
2556 parse::parse("WITH i := (SELECT Person FILTER .id = <uuid>$0) SELECT { c := i.company.name }").unwrap();
2557 let ir = super::compile(&ast, &schema).expect("compile failed");
2558 let sql = crate::sql::emit(&ir).sql;
2559 assert!(!sql.contains("ARRAY(SELECT"), "expected a value, not a set:\n{sql}");
2560 }
2561
2562 #[test]
2565 fn test_a_single_link_walk_off_an_unpinned_row_is_still_a_set() {
2566 let schema = make_schema();
2567 let ast = parse::parse("WITH i := (SELECT Person FILTER .name = 'x') SELECT { c := i.company.name }").unwrap();
2568 let ir = super::compile(&ast, &schema).expect("compile failed");
2569 let sql = crate::sql::emit(&ir).sql;
2570 assert!(sql.contains("ARRAY(SELECT"), "a walk off many rows is a set:\n{sql}");
2571 }
2572
2573 #[test]
2575 fn test_a_multi_link_step_is_a_set_even_off_a_pinned_row() {
2576 let schema = make_schema();
2577 let ast =
2578 parse::parse("WITH i := (SELECT Person FILTER .id = <uuid>$0) SELECT { t := i.posts.title }").unwrap();
2579 let ir = super::compile(&ast, &schema).expect("compile failed");
2580 let sql = crate::sql::emit(&ir).sql;
2581 assert!(sql.contains("ARRAY(SELECT"), "a multi-link step is a set:\n{sql}");
2582 }
2583
2584 #[test]
2590 fn test_a_plain_read_does_not_see_the_statements_own_write() {
2591 let schema = make_schema();
2596 let ast =
2597 parse::parse("WITH made := (INSERT Person { name := 'a', age := 1 }) SELECT { after := count(Person) }")
2598 .unwrap();
2599 let ir = super::compile(&ast, &schema).expect("compile failed");
2600 let sql = crate::sql::emit(&ir).sql;
2601 assert!(
2602 sql.to_lowercase().contains("from \"public\".\"person\""),
2603 "the count must read the table, not the CTE that wrote to it:\n{sql}"
2604 );
2605 }
2606
2607 #[test]
2608 fn test_a_walk_off_a_mutation_sees_the_rows_it_just_wrote() {
2609 let schema = make_schema();
2610 let ast = parse::parse(
2611 "SELECT (UPDATE Person FILTER .name = 'a' SET { posts += (INSERT Post { title := 't' }) }).posts { title }",
2612 )
2613 .unwrap();
2614 let ir = super::compile(&ast, &schema).expect("compile failed");
2615 let sql = crate::sql::emit(&ir).sql;
2616 assert!(
2617 sql.contains("__ml_add_0\" AS \"") || sql.contains("JOIN \"_nested_dml_1__ml_add_0\""),
2618 "the junction must be read from the CTE that wrote it:\n{sql}"
2619 );
2620 assert!(
2621 !sql.contains("JOIN \"public\".\"Post\""),
2622 "the targets must come from their own CTE, not the base table:\n{sql}"
2623 );
2624 }
2625
2626 #[test]
2631 fn test_a_type_intersection_may_follow_a_sub_select() {
2632 let schema = make_schema();
2633 let ast = parse::parse("SELECT (SELECT Person LIMIT 1).company[is Company].name").unwrap();
2634 let ir = super::compile(&ast, &schema).expect("compile failed");
2635 let sql = crate::sql::emit(&ir).sql;
2636 assert!(
2640 !sql.contains("->'name'"),
2641 "the field must not be jsonb off an id:\n{sql}"
2642 );
2643 assert!(sql.contains("\"name\""), "the field must be read as a column:\n{sql}");
2644 }
2645
2646 #[test]
2649 fn test_a_type_intersection_on_a_value_is_rejected() {
2650 let schema = make_schema();
2651 let ast = parse::parse("SELECT (1 + 2)[is Company]").unwrap();
2652 let Err(error) = super::compile(&ast, &schema) else {
2653 panic!("a type intersection on a number is not meaningful");
2654 };
2655 assert!(error.to_string().contains("needs a path, a binding"), "got: {error}");
2656 }
2657
2658 #[test]
2664 fn test_a_select_may_wrap_a_nested_union_subject_select() {
2665 let schema = make_schema();
2666 let ast =
2667 parse::parse("SELECT (SELECT (Person.company UNION Person.company) { name } LIMIT 1) { name }").unwrap();
2668 let error = super::compile(&ast, &schema)
2669 .err()
2670 .map(|e| e.to_string())
2671 .unwrap_or_default();
2672 assert!(
2673 !error.contains("expected a type name as SELECT subject"),
2674 "the union subject must be hoisted, got: {error}"
2675 );
2676 }
2677
2678 #[test]
2682 fn test_a_select_subject_may_walk_off_a_mutation() {
2683 let schema = make_schema();
2684 let ast =
2685 parse::parse("SELECT (UPDATE Person FILTER .name = 'a' SET { name := 'b' }).company { name }").unwrap();
2686 let ir = super::compile(&ast, &schema).expect("compile failed");
2687 let sql = crate::sql::emit(&ir).sql;
2688 assert!(
2689 sql.contains("\"_nested_dml_0\" AS ("),
2690 "the mutation must run as a CTE:\n{sql}"
2691 );
2692 assert!(sql.contains("UPDATE"), "the mutation must still run:\n{sql}");
2693 }
2694
2695 #[test]
2700 fn test_an_update_subject_may_walk_off_a_sub_select() {
2701 let schema = make_schema();
2702 let inline = parse::parse("UPDATE (SELECT Person FILTER .name = 'a').company SET { name := 'b' }").unwrap();
2703 let bound =
2704 parse::parse("WITH s := (SELECT Person FILTER .name = 'a') UPDATE s.company SET { name := 'b' }").unwrap();
2705 let inline_err = super::compile(&inline, &schema).err().map(|e| e.to_string());
2709 let bound_err = super::compile(&bound, &schema).err().map(|e| e.to_string());
2710 assert_eq!(inline_err, bound_err, "the two spellings must compile alike");
2711 assert!(
2712 !inline_err
2713 .unwrap_or_default()
2714 .contains("expected a type name as SELECT subject"),
2715 "the sub-select subject must be accepted"
2716 );
2717 }
2718
2719 #[test]
2722 fn test_a_for_loop_may_be_a_select_subject() {
2723 let schema = make_schema();
2724 let ast = parse::parse("SELECT (FOR s IN {1, 2} UNION (SELECT { a := s }))").unwrap();
2725 let ir = super::compile(&ast, &schema).expect("compile failed");
2726 assert!(matches!(ir.stmt, super::IrStmt::For(_)), "expected the loop itself");
2727 }
2728
2729 #[test]
2734 fn test_a_json_cast_in_a_shape_is_a_named_pointer() {
2735 use crate::query::ShapeNode;
2736 let schema = make_schema();
2737 let ast = parse::parse("SELECT Person { j := <json>.name }").unwrap();
2738 let ir = super::compile(&ast, &schema).expect("compile failed");
2739 let shape = crate::sql::emit(&ir).shape;
2740 let ShapeNode::Object { pointers, .. } = &shape.root else {
2741 panic!("expected an object shape, got {:?}", shape.root);
2742 };
2743 let pointer = pointers
2744 .iter()
2745 .find(|node| matches!(node, ShapeNode::Scalar { name, .. } if name == "j"))
2746 .unwrap_or_else(|| panic!("no scalar pointer named 'j' in {pointers:?}"));
2747 assert!(matches!(pointer, ShapeNode::Scalar { .. }));
2748 }
2749
2750 #[test]
2753 fn test_a_top_level_json_cast_stays_a_root_shaped_node() {
2754 use crate::query::ShapeNode;
2755 let schema = make_schema();
2756 let ast = parse::parse("SELECT <json>'x'").unwrap();
2757 let ir = super::compile(&ast, &schema).expect("compile failed");
2758 let shape = crate::sql::emit(&ir).shape;
2759 assert!(matches!(shape.root, ShapeNode::JsonScalar), "got {:?}", shape.root);
2760 }
2761
2762 #[test]
2765 fn test_stdlib_enum_member_compiles_to_a_text_literal() {
2766 let schema = make_schema();
2767 let ast = parse::parse("SELECT std::Endian.Big").unwrap();
2768 let ir = super::compile(&ast, &schema).expect("compile failed");
2769 let sql = crate::sql::emit(&ir).sql;
2770 assert!(sql.contains("'Big'::text"), "expected a text literal, got: {sql}");
2771 }
2772
2773 #[test]
2774 fn test_unknown_stdlib_enum_member_is_rejected() {
2775 let schema = make_schema();
2776 let ast = parse::parse("SELECT std::Endian.Middle").unwrap();
2777 let Err(error) = super::compile(&ast, &schema) else {
2778 panic!("Middle is not a member of std::Endian");
2779 };
2780 assert!(error.to_string().contains("has no member 'Middle'"), "got: {error}");
2781 }
2782
2783 #[test]
2788 fn test_to_bytes_of_a_uuid_selects_the_uuid_overload() {
2789 let schema = make_schema();
2790 let ast = parse::parse("SELECT std::to_int32(std::to_bytes(<uuid>$0)[12:16], std::Endian.Big)").unwrap();
2791 let ir = super::compile(&ast, &schema).expect("compile failed");
2792 let sql = crate::sql::emit(&ir).sql;
2793 assert!(sql.contains("to_bytes_uuid"), "expected to_bytes_uuid, got: {sql}");
2794 assert!(sql.contains("to_int32_bytes"), "expected to_int32_bytes, got: {sql}");
2795 }
2796
2797 #[test]
2801 fn a_stdlib_call_with_the_wrong_argument_count_is_rejected() {
2802 let schema = make_schema();
2803 let ast = parse::parse("SELECT std::str_lower('A', 'B')").unwrap();
2804 let Err(err) = super::compile(&ast, &schema) else {
2805 panic!("wrong arity must not compile")
2806 };
2807 let msg = err.to_string();
2808 assert!(msg.contains("std::str_lower"), "{msg}");
2809 assert!(msg.contains("takes 1 argument(s), got 2"), "{msg}");
2810 }
2811
2812 #[test]
2816 fn an_unknown_function_suggests_the_closest_real_one() {
2817 let schema = make_schema();
2818 let ast = parse::parse("SELECT std::uuid_generate_v7j()").unwrap();
2819 let Err(err) = super::compile(&ast, &schema) else {
2820 panic!("an unknown function must not compile")
2821 };
2822 let msg = err.to_string();
2823 assert!(msg.contains("does not exist"), "{msg}");
2824 assert!(msg.contains("did you mean std::uuid_generate_v7()?"), "{msg}");
2825 }
2826
2827 #[test]
2831 fn a_function_in_another_namespace_says_where_it_lives() {
2832 let schema = make_schema();
2833 let ast = parse::parse("SELECT std::pi()").unwrap();
2834 let Err(err) = super::compile(&ast, &schema) else {
2835 panic!("pi lives in math, not std")
2836 };
2837 assert!(err.to_string().contains("it lives in math, use math::pi()"), "{err}");
2838 }
2839
2840 #[test]
2843 fn an_arity_error_names_every_arity_the_overload_set_accepts() {
2844 let schema = make_schema();
2845 let ast = parse::parse("SELECT std::str_trim('A', 'B', 'C')").unwrap();
2846 let Err(err) = super::compile(&ast, &schema) else {
2847 panic!("wrong arity must not compile")
2848 };
2849 assert!(err.to_string().contains("takes 1 or 2 argument(s), got 3"), "{err}");
2850 }
2851
2852 #[test]
2855 fn a_variadic_stdlib_call_accepts_extra_arguments() {
2856 let schema = make_schema();
2857 for query in [
2858 "SELECT std::json_get(<json>$0, 'a')",
2859 "SELECT std::json_get(<json>$0, 'a', 'b', 'c')",
2860 ] {
2861 let ast = parse::parse(query).unwrap();
2862 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2863 }
2864 }
2865
2866 #[test]
2869 fn a_stdlib_call_with_an_unacceptable_argument_type_is_rejected() {
2870 let schema = make_schema();
2871 let ast = parse::parse("SELECT std::str_lower(<int64>$0)").unwrap();
2872 let Err(err) = super::compile(&ast, &schema) else {
2873 panic!("wrong argument type must not compile")
2874 };
2875 let msg = err.to_string();
2876 assert!(msg.contains("no overload accepting (int8)"), "{msg}");
2877 assert!(msg.contains("(str)"), "{msg}");
2878 }
2879
2880 #[test]
2885 fn a_stdlib_call_over_a_coalesce_or_conditional_resolves_its_branch_type() {
2886 let schema = make_schema();
2887 for query in [
2888 "SELECT std::str_lower(<optional str>$0 ?? 'DE')",
2889 "SELECT std::str_lower('DE' ?? <optional str>$0)",
2890 "WITH j := (<optional str>$0 ?? 'DE') SELECT std::str_lower(j)",
2891 "SELECT std::str_lower(<str>$0 if <bool>$1 else 'DE')",
2892 "SELECT std::len(<optional str>$0 ?? 'DE')",
2893 ] {
2894 let ast = parse::parse(query).unwrap();
2895 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2896 }
2897 }
2898
2899 #[test]
2904 fn a_stdlib_call_over_a_pass_through_expression_resolves_the_value_type() {
2905 let schema = make_schema();
2906 for query in [
2907 "SELECT (FOR code IN std::array_unpack(<array<str>>$0) UNION (SELECT std::str_lower(code)))",
2909 "WITH x := { a := 'DE' } SELECT std::str_lower(x.a)",
2911 "SELECT math::abs(-3)",
2913 "SELECT math::abs(-(<int64>$0))",
2914 "SELECT std::str_lower((<array<str>>$0)[0])",
2916 "SELECT std::str_title(std::str_split(<str>$0, '::')[0])",
2918 "SELECT std::duration_to_seconds(std::datetime_of_transaction() - <datetime>$0)",
2920 "SELECT std::duration_to_seconds(<duration>$0 + <duration>$0)",
2921 ] {
2922 let ast = parse::parse(query).unwrap();
2923 super::compile(&ast, &schema).unwrap_or_else(|e| panic!("{query} must compile: {e}"));
2924 }
2925 }
2926
2927 #[test]
2930 fn a_stdlib_call_over_a_coalesce_of_the_wrong_type_is_still_rejected() {
2931 let schema = make_schema();
2932 let ast = parse::parse("SELECT std::str_lower(<optional int64>$0 ?? 3)").unwrap();
2933 let Err(err) = super::compile(&ast, &schema) else {
2934 panic!("wrong argument type must not compile")
2935 };
2936 assert!(err.to_string().contains("no overload accepting (int8)"), "{err}");
2937 }
2938
2939 #[test]
2943 fn test_to_int16_and_to_int64_over_bytes_select_the_bytes_overload() {
2944 let schema = make_schema();
2945 for (query, expected) in [
2946 (
2947 "SELECT std::to_int16(std::to_bytes(<uuid>$0)[14:16], std::Endian.Big)",
2948 "to_int16_bytes",
2949 ),
2950 (
2951 "SELECT std::to_int64(std::to_bytes(<uuid>$0)[0:8], std::Endian.Little)",
2952 "to_int64_bytes",
2953 ),
2954 ] {
2955 let ast = parse::parse(query).unwrap();
2956 let ir = super::compile(&ast, &schema).expect("compile failed");
2957 let sql = crate::sql::emit(&ir).sql;
2958 assert!(sql.contains(expected), "expected {expected}, got: {sql}");
2959 }
2960 }
2961
2962 #[test]
2963 fn test_positional_param_names() {
2964 let schema = make_schema();
2965 let ast = parse::parse("SELECT Person FILTER .name = $0").unwrap();
2966 let ir = super::compile(&ast, &schema).expect("compile failed");
2967 assert_eq!(ir.params, vec!["0"]);
2968 }
2969
2970 #[test]
2971 fn test_multiple_positional_param_names_in_order() {
2972 let schema = make_schema();
2973 let ast = parse::parse("SELECT Person FILTER .name = $0 AND .age > $1").unwrap();
2974 let ir = super::compile(&ast, &schema).expect("compile failed");
2975 assert_eq!(ir.params, vec!["0", "1"]);
2976 }
2977
2978 #[test]
2979 fn test_repeated_positional_param_single_slot() {
2980 let schema = make_schema();
2981 let ast = parse::parse("SELECT Person FILTER .name = $0 OR .name = $0").unwrap();
2982 let ir = super::compile(&ast, &schema).expect("compile failed");
2983 assert_eq!(ir.params, vec!["0"], "repeated $0 must occupy a single slot");
2984 }
2985
2986 #[derive(Debug, PartialEq)]
2991 struct ParamTupleKeys {
2992 is_array: bool,
2993 type_name: Option<String>,
2994 keys: Vec<String>,
2995 }
2996
2997 fn param_tuple_keys(query: &str, schema: &SchemaDescriptor) -> Vec<Option<ParamTupleKeys>> {
2998 let ast = parse::parse(query).unwrap();
2999 let ir = super::compile(&ast, schema).expect("compile failed");
3000 ir.param_tuple_types
3001 .iter()
3002 .map(|plan| {
3003 plan.as_ref().map(|plan| ParamTupleKeys {
3004 is_array: plan.is_array,
3005 type_name: plan.type_name.clone(),
3006 keys: plan.members.iter().map(|m| m.key.clone().unwrap_or_default()).collect(),
3007 })
3008 })
3009 .collect()
3010 }
3011
3012 #[test]
3013 fn test_a_tuple_cast_records_its_member_names_against_the_parameter() {
3014 let schema = make_schema();
3015 assert_eq!(
3016 param_tuple_keys("SELECT <tuple<street: str, zip: str>>$address", &schema),
3017 vec![Some(ParamTupleKeys {
3018 is_array: false,
3019 type_name: None,
3020 keys: vec!["street".to_string(), "zip".to_string()],
3021 })]
3022 );
3023 }
3024
3025 #[test]
3026 fn test_an_array_of_tuples_cast_records_one_element_s_member_names() {
3027 let schema = make_schema();
3028 assert_eq!(
3029 param_tuple_keys("SELECT <array<tuple<name: str, value: str>>>$headers", &schema),
3030 vec![Some(ParamTupleKeys {
3031 is_array: true,
3032 type_name: None,
3033 keys: vec!["name".to_string(), "value".to_string()],
3034 })]
3035 );
3036 }
3037
3038 #[test]
3039 fn test_a_nominal_named_tuple_cast_records_its_type_name_too() {
3040 use crate::schema::{NamedTupleDescriptor, TupleMemberDescriptor, TupleMemberKind};
3041 let mut schema = make_schema();
3042 schema.named_tuples.push(NamedTupleDescriptor {
3043 name: "Point".into(),
3044 module: "default".into(),
3045 members: vec![
3046 TupleMemberDescriptor {
3047 name: Some("x".into()),
3048 kind: TupleMemberKind::Scalar {
3049 pg_type: "float8".into(),
3050 },
3051 },
3052 TupleMemberDescriptor {
3053 name: Some("y".into()),
3054 kind: TupleMemberKind::Scalar {
3055 pg_type: "float8".into(),
3056 },
3057 },
3058 ],
3059 });
3060 assert_eq!(
3061 param_tuple_keys("SELECT <array<default::Point>>$points", &schema),
3062 vec![Some(ParamTupleKeys {
3063 is_array: true,
3064 type_name: Some("default::Point".to_string()),
3065 keys: vec!["x".to_string(), "y".to_string()],
3066 })]
3067 );
3068 }
3069
3070 #[test]
3071 fn test_a_parameter_cast_to_a_plain_scalar_records_no_tuple_plan() {
3072 let schema = make_schema();
3073 assert_eq!(param_tuple_keys("SELECT <str>$name", &schema), vec![None]);
3074 }
3075
3076 #[test]
3077 fn test_a_tuple_cast_in_expression_position_records_its_member_names() {
3078 let schema = make_schema();
3082 assert_eq!(
3083 param_tuple_keys(
3084 "SELECT { name := <str>$name, address := <tuple<street: str, zip: str>>$address }",
3085 &schema
3086 ),
3087 vec![
3088 None,
3089 Some(ParamTupleKeys {
3090 is_array: false,
3091 type_name: None,
3092 keys: vec!["street".to_string(), "zip".to_string()],
3093 })
3094 ]
3095 );
3096 }
3097}