Skip to main content

reddb_rql/
sql.rs

1use crate::ast::{
2    AlterMetricQuery, AlterQueueQuery, AlterTableQuery, AlterUserStmt, ApplyMigrationQuery,
3    AskQuery, BinOp, CompareOp, ConfigCommand, CopyFormat, CopyFromQuery, CreateCollectionQuery,
4    CreateForeignTableQuery, CreateIndexQuery, CreateMetricQuery, CreateMigrationQuery,
5    CreatePolicyQuery, CreateQueueQuery, CreateSchemaQuery, CreateSequenceQuery, CreateServerQuery,
6    CreateSloQuery, CreateTableQuery, CreateTimeSeriesQuery, CreateTreeQuery, CreateUserStmt,
7    CreateVcsRefQuery, CreateVectorQuery, CreateViewQuery, DeleteQuery, DropCollectionQuery,
8    DropDocumentQuery, DropForeignTableQuery, DropGraphQuery, DropIndexQuery, DropKvQuery,
9    DropPolicyQuery, DropQueueQuery, DropSchemaQuery, DropSequenceQuery, DropServerQuery,
10    DropTableQuery, DropTimeSeriesQuery, DropTreeQuery, DropVcsRefQuery, DropVectorQuery,
11    DropViewQuery, EventsBackfillQuery, ExplainAlterQuery, ExplainMigrationQuery, Expr, FieldRef,
12    Filter, ForeignColumnDef, GrantStmt, GraphCommand, GraphQuery, HybridQuery, InsertQuery,
13    JoinQuery, KvCommand, MaintenanceCommand, PathQuery, PolicyAction, ProbabilisticCommand,
14    QueryExpr, QueueCommand, QueueSelectQuery, RankOfQuery, RankRangeQuery,
15    RefreshMaterializedViewQuery, RevokeStmt, RollbackMigrationQuery, SearchCommand, Span,
16    TableQuery, TreeCommand, TruncateQuery, TxnControl, UpdateQuery, VcsCommand,
17    VcsConflictResolution, VcsRefKind, VcsResetMode, VectorQuery,
18};
19use crate::lexer::Token;
20use crate::parser::{ParseError, Parser, SafeTokenDisplay};
21use crate::sql_lowering::filter_to_expr;
22use reddb_types::catalog::CollectionModel;
23use reddb_types::types::Value;
24
25/// Canonical SQL frontend command surface.
26///
27/// This is the single entrypoint for SQL/RQL-style commands before they are
28/// lowered into the broader multi-backend `QueryExpr` space.
29#[derive(Debug, Clone)]
30pub enum SqlStatement {
31    Query(SqlQuery),
32    Mutation(SqlMutation),
33    Schema(SqlSchemaCommand),
34    Admin(SqlAdminCommand),
35}
36
37#[derive(Debug, Clone)]
38#[allow(clippy::large_enum_variant)]
39pub enum FrontendStatement {
40    Sql(SqlStatement),
41    Graph(GraphQuery),
42    GraphCommand(GraphCommand),
43    Path(PathQuery),
44    Vector(VectorQuery),
45    Hybrid(HybridQuery),
46    Search(SearchCommand),
47    Ask(AskQuery),
48    QueueSelect(QueueSelectQuery),
49    QueueCommand(QueueCommand),
50    EventsBackfill(EventsBackfillQuery),
51    EventsBackfillStatus { collection: String },
52    TreeCommand(TreeCommand),
53    ProbabilisticCommand(ProbabilisticCommand),
54    KvCommand(KvCommand),
55    ConfigCommand(ConfigCommand),
56    Ranking(QueryExpr),
57}
58
59#[derive(Debug, Clone)]
60pub enum SqlCommand {
61    Select(TableQuery),
62    Join(JoinQuery),
63    Insert(InsertQuery),
64    Update(UpdateQuery),
65    Delete(DeleteQuery),
66    ExplainAlter(ExplainAlterQuery),
67    CreateTable(CreateTableQuery),
68    CreateCollection(CreateCollectionQuery),
69    CreateVector(CreateVectorQuery),
70    DropTable(DropTableQuery),
71    DropGraph(DropGraphQuery),
72    DropVector(DropVectorQuery),
73    DropDocument(DropDocumentQuery),
74    DropKv(DropKvQuery),
75    DropCollection(DropCollectionQuery),
76    Truncate(TruncateQuery),
77    AlterTable(AlterTableQuery),
78    CreateVcsRef(CreateVcsRefQuery),
79    DropVcsRef(DropVcsRefQuery),
80    CreateIndex(CreateIndexQuery),
81    DropIndex(DropIndexQuery),
82    CreateTimeSeries(CreateTimeSeriesQuery),
83    CreateMetric(CreateMetricQuery),
84    AlterMetric(AlterMetricQuery),
85    CreateSlo(CreateSloQuery),
86    DropTimeSeries(DropTimeSeriesQuery),
87    CreateQueue(CreateQueueQuery),
88    AlterQueue(AlterQueueQuery),
89    DropQueue(DropQueueQuery),
90    CreateTree(CreateTreeQuery),
91    DropTree(DropTreeQuery),
92    Probabilistic(ProbabilisticCommand),
93    SetConfig {
94        key: String,
95        value: Value,
96    },
97    ShowConfig {
98        prefix: Option<String>,
99        as_json: bool,
100    },
101    SetSecret {
102        key: String,
103        value: Value,
104    },
105    DeleteSecret {
106        key: String,
107    },
108    ShowSecrets {
109        prefix: Option<String>,
110    },
111    SetTenant(Option<String>),
112    ShowTenant,
113    TransactionControl(TxnControl),
114    Maintenance(MaintenanceCommand),
115    Vcs(VcsCommand),
116    CreateSchema(CreateSchemaQuery),
117    DropSchema(DropSchemaQuery),
118    CreateSequence(CreateSequenceQuery),
119    DropSequence(DropSequenceQuery),
120    CopyFrom(CopyFromQuery),
121    CreateView(CreateViewQuery),
122    DropView(DropViewQuery),
123    RefreshMaterializedView(RefreshMaterializedViewQuery),
124    CreatePolicy(CreatePolicyQuery),
125    DropPolicy(DropPolicyQuery),
126    CreateServer(CreateServerQuery),
127    DropServer(DropServerQuery),
128    CreateForeignTable(CreateForeignTableQuery),
129    DropForeignTable(DropForeignTableQuery),
130    /// `GRANT … ON … TO …`
131    Grant(GrantStmt),
132    /// `REVOKE … ON … FROM …`
133    Revoke(RevokeStmt),
134    /// `ALTER USER name <attrs>`
135    AlterUser(AlterUserStmt),
136    /// `CREATE USER name PASSWORD '...' [ROLE read|write|admin]`
137    CreateUser(CreateUserStmt),
138    /// IAM policy DDL (CREATE POLICY '...' AS '...', DROP POLICY '...',
139    /// ATTACH/DETACH POLICY, SHOW POLICIES, SIMULATE, SHOW EFFECTIVE
140    /// PERMISSIONS). Stored as a pre-built QueryExpr so the dispatcher
141    /// can route the multitude of shapes through a single arm.
142    IamPolicy(QueryExpr),
143    CreateMigration(CreateMigrationQuery),
144    ApplyMigration(ApplyMigrationQuery),
145    RollbackMigration(RollbackMigrationQuery),
146    ExplainMigration(ExplainMigrationQuery),
147}
148
149/// Issue #789 — Analytics v0 non-goal map for `CREATE …` forms.
150///
151/// PRD #782 ringfences Analytics v0 around a metric-centric catalog and
152/// explicitly excludes generic analytics objects, a separate event
153/// storage model, cohorts, funnels, SLA contracts, and adapter surfaces.
154/// When the parser sees one of these idents in the `CREATE` head, return
155/// a stable v0-scoped rejection message; otherwise return `None` and let
156/// the regular CREATE fallback handle the token.
157fn analytics_v0_non_goal_create(token: &Token) -> Option<String> {
158    let ident = match token {
159        Token::Ident(s) => s,
160        _ => return None,
161    };
162    let upper = ident.to_ascii_uppercase();
163    let message = match upper.as_str() {
164        "ANALYTICS" => {
165            "CREATE ANALYTICS is not supported in Analytics v0 — \
166             use CREATE METRIC <dotted.path> for the metric-centric \
167             catalog (PRD #782 non-goal)"
168        }
169        "EVENT" => {
170            "CREATE EVENT is not supported in Analytics v0 — \
171             event-shaped data lives in ordinary TABLE/DOCUMENT \
172             collections, not a new storage model (PRD #782 non-goal)"
173        }
174        "COHORT" => {
175            "CREATE COHORT is not supported in Analytics v0 — \
176             cohort surfaces are deferred (PRD #782 non-goal)"
177        }
178        "FUNNEL" => {
179            "CREATE FUNNEL is not supported in Analytics v0 — \
180             funnel surfaces are deferred (PRD #782 non-goal)"
181        }
182        "SLA" => {
183            "CREATE SLA is not supported in Analytics v0 — \
184             SLA/legal/commercial contract modeling is post-MVP \
185             (PRD #782 non-goal)"
186        }
187        "ADAPTER" => {
188            "CREATE ADAPTER is not supported in Analytics v0 — \
189             Prometheus/Grafana/Snowplow/Google Analytics adapters \
190             are deferred (PRD #782 non-goal)"
191        }
192        _ => return None,
193    };
194    Some(message.to_string())
195}
196
197fn collection_model_filter(model: &str) -> Filter {
198    Filter::Compare {
199        field: FieldRef::column("", "model"),
200        op: CompareOp::Eq,
201        value: Value::Text(model.to_string().into()),
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use reddb_types::catalog::CollectionModel;
209
210    fn frontend(input: &str) -> FrontendStatement {
211        parse_frontend(input)
212            .unwrap_or_else(|err| panic!("failed to parse frontend {input:?}: {err:?}"))
213    }
214
215    fn expr(input: &str) -> QueryExpr {
216        frontend(input).into_query_expr()
217    }
218
219    fn sql_command(input: &str) -> SqlCommand {
220        sql_command_result(input)
221            .unwrap_or_else(|err| panic!("failed to parse SQL command {input:?}: {err:?}"))
222    }
223
224    fn sql_command_result(input: &str) -> Result<SqlCommand, ParseError> {
225        let mut parser = Parser::new(input)?;
226        parser.parse_sql_command()
227    }
228
229    fn assert_text(value: &Value, expected: &str) {
230        match value {
231            Value::Text(text) => assert_eq!(text.as_ref(), expected),
232            other => panic!("expected text {expected:?}, got {other:?}"),
233        }
234    }
235
236    #[test]
237    fn parse_frontend_routes_core_sql_statements() {
238        let FrontendStatement::Sql(SqlStatement::Query(SqlQuery::Select(query))) =
239            frontend("SELECT * FROM users")
240        else {
241            panic!("SELECT should route to SqlStatement::Query::Select");
242        };
243        assert_eq!(query.table, "users");
244
245        let QueryExpr::Insert(query) = expr("INSERT INTO users (id, name) VALUES (1, 'ada')")
246        else {
247            panic!("INSERT should lower through the SQL frontend");
248        };
249        assert_eq!(query.table, "users");
250        assert_eq!(query.columns, vec!["id", "name"]);
251        assert_eq!(query.values.len(), 1);
252
253        let QueryExpr::Update(query) = expr("UPDATE users SET name = 'ada' WHERE id = 1") else {
254            panic!("UPDATE should lower through the SQL frontend");
255        };
256        assert_eq!(query.table, "users");
257        assert_eq!(query.assignments[0].0, "name");
258
259        let QueryExpr::Delete(query) = expr("DELETE FROM users WHERE id = 1") else {
260            panic!("DELETE should lower through the SQL frontend");
261        };
262        assert_eq!(query.table, "users");
263        assert!(query.filter.is_some());
264
265        let QueryExpr::CreateTable(query) = expr("CREATE TABLE users (id INT, name TEXT)") else {
266            panic!("CREATE TABLE should lower through the SQL frontend");
267        };
268        assert_eq!(query.collection_model, CollectionModel::Table);
269        assert_eq!(query.name, "users");
270        assert_eq!(query.columns[0].name, "id");
271
272        let QueryExpr::DropTable(query) = expr("DROP TABLE IF EXISTS users") else {
273            panic!("DROP TABLE should lower through the SQL frontend");
274        };
275        assert_eq!(query.name, "users");
276        assert!(query.if_exists);
277    }
278
279    #[test]
280    fn parse_frontend_routes_admin_and_catalog_sql() {
281        let QueryExpr::Table(query) = expr("SHOW COLLECTIONS") else {
282            panic!("SHOW COLLECTIONS should become a red.collections table query");
283        };
284        assert_eq!(query.table, "red.collections");
285        assert!(query.filter.is_some());
286
287        let QueryExpr::Table(query) = expr("SHOW TABLES LIMIT 5") else {
288            panic!("SHOW TABLES should become a filtered red.collections table query");
289        };
290        assert_eq!(query.table, "red.collections");
291        assert_eq!(query.limit, Some(5));
292        assert!(query.filter.is_some());
293
294        assert!(matches!(
295            expr("SHOW CONFIG durability.mode"),
296            QueryExpr::ShowConfig { prefix: Some(prefix), as_json: false } if prefix == "durability.mode"
297        ));
298        assert!(matches!(
299            expr("SHOW CONFIG"),
300            QueryExpr::ShowConfig {
301                prefix: None,
302                as_json: false
303            }
304        ));
305        assert!(matches!(
306            expr("SHOW CONFIG runtime.result_cache AS JSON"),
307            QueryExpr::ShowConfig { prefix: Some(prefix), as_json: true } if prefix == "runtime.result_cache"
308        ));
309        assert!(matches!(
310            expr("SHOW CONFIG FORMAT JSON"),
311            QueryExpr::ShowConfig {
312                prefix: None,
313                as_json: true
314            }
315        ));
316
317        let QueryExpr::SetConfig { key, value } = expr("SET CONFIG durability.mode = 'sync'")
318        else {
319            panic!("SET CONFIG should stay on the SQL admin surface");
320        };
321        assert_eq!(key, "durability.mode");
322        assert_text(&value, "sync");
323
324        let QueryExpr::SetSecret { key, value } = expr("SET SECRET provider.api_key = 'sk_test'")
325        else {
326            panic!("SET SECRET should stay on the SQL admin surface");
327        };
328        assert_eq!(key, "provider.api_key");
329        assert_text(&value, "sk_test");
330        assert!(matches!(
331            expr("SET SECRET red.secrets.provider.api_key = 'sk_test'"),
332            QueryExpr::SetSecret { key, .. } if key == "red.secret.provider.api_key"
333        ));
334
335        assert!(matches!(
336            expr("DELETE SECRET provider.api_key"),
337            QueryExpr::DeleteSecret { key } if key == "provider.api_key"
338        ));
339        assert!(matches!(
340            expr("DELETE SECRET red.secrets.provider.api_key"),
341            QueryExpr::DeleteSecret { key } if key == "red.secret.provider.api_key"
342        ));
343        assert!(matches!(
344            expr("SHOW SECRETS provider"),
345            QueryExpr::ShowSecrets { prefix: Some(prefix) } if prefix == "provider"
346        ));
347        assert!(matches!(
348            expr("SHOW SECRETS red.secrets.provider"),
349            QueryExpr::ShowSecrets { prefix: Some(prefix) } if prefix == "red.secret.provider"
350        ));
351        assert!(matches!(
352            expr("SET TENANT 'acme'"),
353            QueryExpr::SetTenant(Some(tenant)) if tenant == "acme"
354        ));
355        assert!(matches!(expr("RESET TENANT"), QueryExpr::SetTenant(None)));
356        assert!(matches!(expr("SHOW TENANT"), QueryExpr::ShowTenant));
357        assert!(matches!(
358            expr("BEGIN ISOLATION LEVEL SNAPSHOT"),
359            QueryExpr::TransactionControl(TxnControl::Begin)
360        ));
361        assert!(matches!(
362            expr("ROLLBACK TO SAVEPOINT sp1"),
363            QueryExpr::TransactionControl(TxnControl::RollbackToSavepoint(name)) if name == "sp1"
364        ));
365        assert!(matches!(
366            expr("VACUUM FULL users"),
367            QueryExpr::MaintenanceCommand(MaintenanceCommand::Vacuum {
368                target: Some(target),
369                full: true,
370            }) if target == "users"
371        ));
372    }
373
374    #[test]
375    fn parse_frontend_routes_extended_schema_sql() {
376        assert!(matches!(
377            expr("CREATE SCHEMA IF NOT EXISTS app"),
378            QueryExpr::CreateSchema(CreateSchemaQuery {
379                name,
380                if_not_exists: true,
381            }) if name == "app"
382        ));
383        assert!(matches!(
384            expr("DROP SCHEMA IF EXISTS app CASCADE"),
385            QueryExpr::DropSchema(DropSchemaQuery {
386                name,
387                if_exists: true,
388                cascade: true,
389            }) if name == "app"
390        ));
391        assert!(matches!(
392            expr("CREATE SEQUENCE IF NOT EXISTS seq START WITH 10 INCREMENT BY 2"),
393            QueryExpr::CreateSequence(CreateSequenceQuery {
394                name,
395                if_not_exists: true,
396                start: 10,
397                increment: 2,
398            }) if name == "seq"
399        ));
400        assert!(matches!(
401            expr("DROP SEQUENCE IF EXISTS seq"),
402            QueryExpr::DropSequence(DropSequenceQuery {
403                name,
404                if_exists: true,
405            }) if name == "seq"
406        ));
407
408        let QueryExpr::CopyFrom(copy) = expr(
409            "COPY users FROM '/tmp/u.csv' WITH (FORMAT = csv, HEADER = true, DELIMITER = ';')",
410        ) else {
411            panic!("COPY should lower through SQL frontend");
412        };
413        assert_eq!(copy.table, "users");
414        assert_eq!(copy.path, "/tmp/u.csv");
415        assert_eq!(copy.format, CopyFormat::Csv);
416        assert_eq!(copy.delimiter, Some(';'));
417        assert!(copy.has_header);
418
419        let QueryExpr::CreateView(view) = expr(
420            "CREATE MATERIALIZED VIEW IF NOT EXISTS mv WITH RETENTION 1 h \
421             AS SELECT id FROM users REFRESH EVERY 5 s",
422        ) else {
423            panic!("CREATE MATERIALIZED VIEW should lower through SQL frontend");
424        };
425        assert_eq!(view.name, "mv");
426        assert!(view.materialized);
427        assert!(view.if_not_exists);
428        assert_eq!(view.retention_duration_ms, Some(3_600_000));
429        assert_eq!(view.refresh_every_ms, Some(5_000));
430        assert!(matches!(*view.query, QueryExpr::Table(_)));
431
432        assert!(matches!(
433            expr("DROP MATERIALIZED VIEW IF EXISTS mv"),
434            QueryExpr::DropView(DropViewQuery {
435                name,
436                materialized: true,
437                if_exists: true,
438            }) if name == "mv"
439        ));
440        assert!(matches!(
441            expr("REFRESH MATERIALIZED VIEW mv"),
442            QueryExpr::RefreshMaterializedView(RefreshMaterializedViewQuery { name }) if name == "mv"
443        ));
444    }
445
446    #[test]
447    fn parse_frontend_routes_fdw_policy_auth_and_migrations() {
448        let QueryExpr::CreateServer(server) = expr(
449            "CREATE SERVER IF NOT EXISTS csvsrv FOREIGN DATA WRAPPER csv OPTIONS (path '/data')",
450        ) else {
451            panic!("CREATE SERVER should lower through SQL frontend");
452        };
453        assert_eq!(server.name, "csvsrv");
454        assert_eq!(server.wrapper, "csv");
455        assert!(server.if_not_exists);
456        assert_eq!(
457            server.options,
458            vec![("path".to_string(), "/data".to_string())]
459        );
460
461        let QueryExpr::CreateForeignTable(table) = expr(
462            "CREATE FOREIGN TABLE IF NOT EXISTS ext_users \
463             (id INT, name TEXT) SERVER csvsrv OPTIONS (file 'users.csv')",
464        ) else {
465            panic!("CREATE FOREIGN TABLE should lower through SQL frontend");
466        };
467        assert_eq!(table.name, "ext_users");
468        assert_eq!(table.server, "csvsrv");
469        assert!(table.if_not_exists);
470        assert_eq!(table.columns.len(), 2);
471        assert!(!table.columns[0].not_null);
472
473        assert!(matches!(
474            expr("DROP SERVER IF EXISTS csvsrv CASCADE"),
475            QueryExpr::DropServer(DropServerQuery {
476                name,
477                if_exists: true,
478                cascade: true,
479            }) if name == "csvsrv"
480        ));
481        assert!(matches!(
482            expr("DROP FOREIGN TABLE IF EXISTS ext_users"),
483            QueryExpr::DropForeignTable(DropForeignTableQuery {
484                name,
485                if_exists: true,
486            }) if name == "ext_users"
487        ));
488
489        let QueryExpr::CreatePolicy(policy) = expr(
490            "CREATE POLICY readonly ON NODES OF mygraph FOR SELECT TO analytics USING (public = 1)",
491        ) else {
492            panic!("CREATE POLICY should lower through SQL frontend");
493        };
494        assert_eq!(policy.name, "readonly");
495        assert_eq!(policy.table, "mygraph");
496        assert_eq!(policy.action, Some(PolicyAction::Select));
497        assert_eq!(policy.role.as_deref(), Some("analytics"));
498        assert_eq!(policy.target_kind.as_ident(), "nodes");
499
500        assert!(matches!(
501            expr("DROP POLICY IF EXISTS readonly ON mygraph"),
502            QueryExpr::DropPolicy(DropPolicyQuery {
503                name,
504                table,
505                if_exists: true,
506            }) if name == "readonly" && table == "mygraph"
507        ));
508
509        assert!(matches!(
510            expr("GRANT SELECT ON TABLE public.users TO tenant1.alice"),
511            QueryExpr::Grant(grant)
512                if grant.actions == vec!["SELECT"]
513                    && grant.objects[0].schema.as_deref() == Some("public")
514        ));
515        assert!(matches!(
516            expr("REVOKE GRANT OPTION FOR USAGE ON SCHEMA analytics FROM GROUP analysts"),
517            QueryExpr::Revoke(revoke) if revoke.grant_option_for && revoke.all == false
518        ));
519        assert!(matches!(
520            expr("ALTER USER bob ENABLE SET search_path TO 'public'"),
521            QueryExpr::AlterUser(user)
522                if user.username == "bob" && user.attributes.len() == 2
523        ));
524        assert!(matches!(
525            expr("CREATE USER tenant1.alice WITH PASSWORD 'pw' ROLE write"),
526            QueryExpr::CreateUser(user)
527                if user.tenant.as_deref() == Some("tenant1")
528                    && user.username == "alice"
529                    && user.password == "pw"
530                    && user.role == "write"
531        ));
532
533        assert!(matches!(
534            expr("CREATE POLICY 'readonly' AS '{\"Statement\":[]}'"),
535            QueryExpr::CreateIamPolicy { id, json }
536                if id == "readonly" && json == "{\"Statement\":[]}"
537        ));
538        assert!(matches!(
539            expr("DROP POLICY 'readonly'"),
540            QueryExpr::DropIamPolicy { id } if id == "readonly"
541        ));
542
543        assert!(matches!(
544            expr("CREATE MIGRATION m2 DEPENDS ON m0 BATCH 10 ROWS AS CREATE TABLE accounts (id INT)"),
545            QueryExpr::CreateMigration(migration)
546                if migration.name == "m2"
547                    && migration.depends_on == vec!["m0".to_string()]
548                    && migration.batch_size == Some(10)
549        ));
550        assert!(matches!(
551            expr("APPLY MIGRATION * FOR TENANT tenant1"),
552            QueryExpr::ApplyMigration(apply)
553                if apply.for_tenant.as_deref() == Some("tenant1")
554        ));
555        assert!(matches!(
556            expr("ROLLBACK MIGRATION m2"),
557            QueryExpr::RollbackMigration(RollbackMigrationQuery { name }) if name == "m2"
558        ));
559        assert!(matches!(
560            expr("EXPLAIN MIGRATION m2"),
561            QueryExpr::ExplainMigration(ExplainMigrationQuery { name }) if name == "m2"
562        ));
563    }
564
565    #[test]
566    fn parse_sql_statement_covers_statement_category_wrapping() {
567        enum Expected {
568            Select,
569            Insert,
570            CreateSchema,
571            SetTenant,
572        }
573
574        let cases = [
575            ("SELECT * FROM users", Expected::Select),
576            ("INSERT INTO users (id) VALUES (1)", Expected::Insert),
577            ("CREATE SCHEMA app", Expected::CreateSchema),
578            ("SET TENANT 'acme'", Expected::SetTenant),
579        ];
580
581        for (input, expected) in cases {
582            let mut parser = Parser::new(input).expect("lexer");
583            let statement = parser
584                .parse_sql_statement()
585                .unwrap_or_else(|err| panic!("failed to parse {input:?}: {err:?}"));
586            let matched = match expected {
587                Expected::Select => matches!(statement, SqlStatement::Query(SqlQuery::Select(_))),
588                Expected::Insert => {
589                    matches!(statement, SqlStatement::Mutation(SqlMutation::Insert(_)))
590                }
591                Expected::CreateSchema => matches!(
592                    statement,
593                    SqlStatement::Schema(SqlSchemaCommand::CreateSchema(_))
594                ),
595                Expected::SetTenant => {
596                    matches!(
597                        statement,
598                        SqlStatement::Admin(SqlAdminCommand::SetTenant(_))
599                    )
600                }
601            };
602            assert!(matched, "{input}");
603        }
604    }
605
606    #[test]
607    fn parse_frontend_routes_non_sql_frontends() {
608        let QueryExpr::KvCommand(KvCommand::Get {
609            model,
610            collection,
611            key,
612        }) = expr("KV GET settings.feature")
613        else {
614            panic!("KV GET should route to FrontendStatement::KvCommand");
615        };
616        assert_eq!(model, CollectionModel::Kv);
617        assert_eq!(collection, "settings");
618        assert_eq!(key, "feature");
619
620        let QueryExpr::ConfigCommand(ConfigCommand::Watch {
621            collection,
622            key,
623            prefix,
624            from_lsn,
625        }) = expr("WATCH CONFIG app PREFIX feature FROM LSN 7")
626        else {
627            panic!("WATCH CONFIG should route to FrontendStatement::ConfigCommand");
628        };
629        assert_eq!(collection, "app");
630        assert_eq!(key, "feature");
631        assert!(prefix);
632        assert_eq!(from_lsn, Some(7));
633
634        let QueryExpr::ConfigCommand(ConfigCommand::List {
635            collection,
636            prefix,
637            limit,
638            offset,
639        }) = expr("LIST CONFIG app PREFIX feature LIMIT 3 OFFSET 1")
640        else {
641            panic!("LIST CONFIG should route to FrontendStatement::ConfigCommand");
642        };
643        assert_eq!(collection, "app");
644        assert_eq!(prefix.as_deref(), Some("feature"));
645        assert_eq!(limit, Some(3));
646        assert_eq!(offset, 1);
647
648        let QueryExpr::KvCommand(KvCommand::List {
649            model,
650            collection,
651            prefix,
652            limit,
653            offset,
654            as_json,
655        }) = expr("KV LIST settings PREFIX 'feature.' LIMIT 10 OFFSET 2")
656        else {
657            panic!("KV LIST should route to FrontendStatement::KvCommand");
658        };
659        assert_eq!(model, CollectionModel::Kv);
660        assert_eq!(collection, "settings");
661        assert_eq!(prefix.as_deref(), Some("feature."));
662        assert_eq!(limit, Some(10));
663        assert_eq!(offset, 2);
664        assert!(!as_json);
665
666        let QueryExpr::KvCommand(KvCommand::List {
667            model,
668            collection,
669            prefix,
670            limit,
671            offset,
672            as_json,
673        }) = expr("LIST KV settings PREFIX feature LIMIT 10 OFFSET 2")
674        else {
675            panic!("LIST KV should route to FrontendStatement::KvCommand");
676        };
677        assert_eq!(model, CollectionModel::Kv);
678        assert_eq!(collection, "settings");
679        assert_eq!(prefix.as_deref(), Some("feature"));
680        assert_eq!(limit, Some(10));
681        assert_eq!(offset, 2);
682        assert!(!as_json);
683
684        let QueryExpr::KvCommand(KvCommand::List {
685            model,
686            collection,
687            prefix,
688            as_json,
689            ..
690        }) = expr("KV LIST settings PREFIX feature AS JSON")
691        else {
692            panic!("KV LIST AS JSON should route to FrontendStatement::KvCommand");
693        };
694        assert_eq!(model, CollectionModel::Kv);
695        assert_eq!(collection, "settings");
696        assert_eq!(prefix.as_deref(), Some("feature"));
697        assert!(as_json);
698
699        let QueryExpr::KvCommand(KvCommand::Watch {
700            model,
701            collection,
702            key,
703            prefix,
704            from_lsn,
705        }) = expr("WATCH sessions.user.* FROM LSN 3")
706        else {
707            panic!("bare WATCH should route to FrontendStatement::KvCommand");
708        };
709        assert_eq!(model, CollectionModel::Kv);
710        assert_eq!(collection, "sessions");
711        assert_eq!(key, "user");
712        assert!(prefix);
713        assert_eq!(from_lsn, Some(3));
714
715        let QueryExpr::KvCommand(KvCommand::Watch {
716            model,
717            collection,
718            key,
719            prefix,
720            from_lsn,
721        }) = expr("WATCH VAULT secrets PREFIX api FROM LSN 7")
722        else {
723            panic!("WATCH VAULT should route to FrontendStatement::KvCommand");
724        };
725        assert_eq!(model, CollectionModel::Vault);
726        assert_eq!(collection, "secrets");
727        assert_eq!(key, "api");
728        assert!(prefix);
729        assert_eq!(from_lsn, Some(7));
730
731        let QueryExpr::KvCommand(KvCommand::List {
732            model,
733            collection,
734            prefix,
735            limit,
736            offset,
737            as_json,
738        }) = expr("LIST VAULT secrets PREFIX api LIMIT 10 OFFSET 2")
739        else {
740            panic!("LIST VAULT should route to FrontendStatement::KvCommand");
741        };
742        assert_eq!(model, CollectionModel::Vault);
743        assert_eq!(collection, "secrets");
744        assert_eq!(prefix.as_deref(), Some("api"));
745        assert_eq!(limit, Some(10));
746        assert_eq!(offset, 2);
747        assert!(!as_json);
748
749        assert!(matches!(
750            expr("INVALIDATE CONFIG app feature_flag"),
751            QueryExpr::ConfigCommand(ConfigCommand::InvalidVolatileOperation {
752                operation,
753                collection,
754                key: Some(key),
755            }) if operation == "INVALIDATE" && collection == "app" && key == "feature_flag"
756        ));
757        assert!(matches!(
758            expr("INVALIDATE TAGS [user:42, org:7] FROM sessions"),
759            QueryExpr::KvCommand(KvCommand::InvalidateTags { collection, tags })
760                if collection == "sessions" && tags == vec!["user:42".to_string(), "org:7".to_string()]
761        ));
762
763        let QueryExpr::EventsBackfill(query) =
764            expr("EVENTS BACKFILL users WHERE status = 'active' TO audit LIMIT 10")
765        else {
766            panic!("EVENTS BACKFILL should route to FrontendStatement::EventsBackfill");
767        };
768        assert_eq!(query.collection, "users");
769        assert_eq!(query.where_filter.as_deref(), Some("status = 'active'"));
770        assert_eq!(query.target_queue, "audit");
771        assert_eq!(query.limit, Some(10));
772
773        let QueryExpr::Table(query) = expr("EVENTS STATUS users LIMIT 2") else {
774            panic!("EVENTS STATUS should route through the SQL select surface");
775        };
776        assert_eq!(query.table, "red.subscriptions");
777        assert_eq!(query.limit, Some(2));
778        assert!(query.filter.is_some());
779
780        assert!(matches!(
781            expr("EVENTS BACKFILL STATUS users"),
782            QueryExpr::EventsBackfillStatus { collection } if collection == "users"
783        ));
784        assert!(parse_frontend("LIST UNKNOWN").is_err());
785        assert!(parse_frontend("EVENTS UNKNOWN").is_err());
786    }
787
788    #[test]
789    fn parse_frontend_routes_ranking_reads() {
790        assert!(matches!(
791            expr("RANK OF 42 IN page_rank"),
792            QueryExpr::RankOf(RankOfQuery { ranking, entity_id })
793                if ranking == "page_rank" && entity_id == 42
794        ));
795        assert!(matches!(
796            expr("APPROX RANK OF 7 IN page_rank"),
797            QueryExpr::ApproxRankOf(RankOfQuery { ranking, entity_id })
798                if ranking == "page_rank" && entity_id == 7
799        ));
800        assert!(matches!(
801            expr("RANK RANGE 1 TO 3 IN page_rank"),
802            QueryExpr::RankRange(RankRangeQuery { ranking, lo, hi })
803                if ranking == "page_rank" && lo == 1 && hi == 3
804        ));
805        assert!(matches!(
806            expr("ZRANK page_rank 0"),
807            QueryExpr::RankOf(RankOfQuery { ranking, entity_id })
808                if ranking == "page_rank" && entity_id == 0
809        ));
810        assert!(matches!(
811            expr("ZRANGE page_rank 0 3 WITHSCORES"),
812            QueryExpr::RankRange(RankRangeQuery { ranking, lo, hi })
813                if ranking == "page_rank" && lo == 1 && hi == 4
814        ));
815        assert!(
816            parse_frontend("RANK RANGE 3 TO 1 IN page_rank").is_err(),
817            "rank range must reject reversed bounds"
818        );
819    }
820
821    #[test]
822    fn parse_frontend_covers_multimodel_command_routing() {
823        assert!(matches!(
824            expr("GRAPH CENTRALITY ALGORITHM pagerank LIMIT 5"),
825            QueryExpr::GraphCommand(GraphCommand::Centrality {
826                algorithm,
827                limit: Some(5),
828                ..
829            }) if algorithm == "pagerank"
830        ));
831        assert!(matches!(
832            expr("SEARCH TEXT 'login failure' COLLECTION incidents LIMIT 20 FUZZY"),
833            QueryExpr::SearchCommand(SearchCommand::Text {
834                query,
835                collection: Some(collection),
836                limit: 20,
837                fuzzy: true,
838                ..
839            }) if query == "login failure" && collection == "incidents"
840        ));
841        assert!(matches!(
842            expr("ASK 'why did login fail?' USING openai LIMIT 3"),
843            QueryExpr::Ask(query)
844                if query.question == "why did login fail?"
845                    && query.provider.as_deref() == Some("openai")
846                    && query.limit == Some(3)
847        ));
848        assert!(matches!(
849            expr("QUEUE LEN tasks"),
850            QueryExpr::QueueCommand(QueueCommand::Len { queue }) if queue == "tasks"
851        ));
852        assert!(matches!(
853            expr("TREE REBALANCE forest.org DRY RUN"),
854            QueryExpr::TreeCommand(TreeCommand::Rebalance {
855                collection,
856                tree_name,
857                dry_run: true,
858            }) if collection == "forest" && tree_name == "org"
859        ));
860        assert!(matches!(
861            expr("HLL COUNT visitors"),
862            QueryExpr::ProbabilisticCommand(ProbabilisticCommand::HllCount { names })
863                if names == vec!["visitors".to_string()]
864        ));
865    }
866
867    #[test]
868    fn sql_command_round_trips_multimodel_schema_variants() {
869        macro_rules! assert_command_round_trip {
870            ($input:expr, $pattern:pat) => {{
871                let command = sql_command($input);
872                assert!(matches!(command, $pattern), "unexpected command for {}", $input);
873
874                let statement = sql_command($input).into_statement();
875                let command = statement.into_command();
876                assert!(
877                    matches!(command, $pattern),
878                    "statement round trip changed command for {}",
879                    $input
880                );
881
882                let expr = sql_command($input).into_query_expr();
883                assert!(
884                    !matches!(expr, QueryExpr::Table(TableQuery { table, .. }) if table.is_empty()),
885                    "lowering produced an empty table placeholder for {}",
886                    $input
887                );
888            }};
889        }
890
891        assert_command_round_trip!(
892            "EXPLAIN ALTER FOR CREATE TABLE users (id INT) FORMAT JSON",
893            SqlCommand::ExplainAlter(_)
894        );
895        assert_command_round_trip!("CREATE TABLE users (id INT)", SqlCommand::CreateTable(_));
896        assert_command_round_trip!("DROP TABLE IF EXISTS users", SqlCommand::DropTable(_));
897        assert_command_round_trip!(
898            "ALTER TABLE users ADD COLUMN status TEXT",
899            SqlCommand::AlterTable(_)
900        );
901        assert_command_round_trip!(
902            "CREATE INDEX idx_email ON users (email) USING HASH",
903            SqlCommand::CreateIndex(_)
904        );
905        assert_command_round_trip!(
906            "DROP INDEX IF EXISTS idx_email ON users",
907            SqlCommand::DropIndex(_)
908        );
909        assert_command_round_trip!("CREATE GRAPH identity", SqlCommand::CreateTable(_));
910        assert_command_round_trip!("CREATE DOCUMENT docs", SqlCommand::CreateTable(_));
911        assert_command_round_trip!(
912            "CREATE VECTOR embeddings DIM 4",
913            SqlCommand::CreateVector(_)
914        );
915        assert_command_round_trip!(
916            "CREATE COLLECTION turbo KIND vector.turbo DIM 3",
917            SqlCommand::CreateCollection(_)
918        );
919        assert_command_round_trip!("CREATE KV settings", SqlCommand::CreateTable(_));
920        assert_command_round_trip!("CREATE CONFIG app", SqlCommand::CreateTable(_));
921        assert_command_round_trip!(
922            "CREATE VAULT secrets WITH OWN MASTER KEY",
923            SqlCommand::CreateTable(_)
924        );
925        assert_command_round_trip!(
926            "CREATE TIMESERIES metrics RETENTION 90 d",
927            SqlCommand::CreateTimeSeries(_)
928        );
929        assert_command_round_trip!(
930            "CREATE METRIC svc.latency TYPE gauge ROLE sli",
931            SqlCommand::CreateMetric(_)
932        );
933        assert_command_round_trip!(
934            "ALTER METRIC svc.latency SET ROLE internal",
935            SqlCommand::AlterMetric(_)
936        );
937        assert_command_round_trip!(
938            "CREATE SLO svc.availability ON svc.latency TARGET 99.9 WINDOW 5 m",
939            SqlCommand::CreateSlo(_)
940        );
941        assert_command_round_trip!(
942            "CREATE QUEUE tasks MAX_SIZE 100",
943            SqlCommand::CreateQueue(_)
944        );
945        assert_command_round_trip!(
946            "ALTER QUEUE tasks SET MODE FANOUT",
947            SqlCommand::AlterQueue(_)
948        );
949        assert_command_round_trip!(
950            "CREATE TREE org IN forest ROOT LABEL root MAX_CHILDREN 4",
951            SqlCommand::CreateTree(_)
952        );
953        assert_command_round_trip!(
954            "CREATE HLL visitors PRECISION 14",
955            SqlCommand::Probabilistic(_)
956        );
957        assert_command_round_trip!(
958            "CREATE SKETCH freqs WIDTH 512 DEPTH 3",
959            SqlCommand::Probabilistic(_)
960        );
961        assert_command_round_trip!(
962            "CREATE FILTER seen CAPACITY 1024",
963            SqlCommand::Probabilistic(_)
964        );
965        assert_command_round_trip!("COPY users FROM '/tmp/u.csv'", SqlCommand::CopyFrom(_));
966        assert_command_round_trip!(
967            "CREATE VIEW active_users AS SELECT * FROM users",
968            SqlCommand::CreateView(_)
969        );
970        assert_command_round_trip!("DROP VIEW active_users", SqlCommand::DropView(_));
971        assert_command_round_trip!(
972            "REFRESH MATERIALIZED VIEW active_users",
973            SqlCommand::RefreshMaterializedView(_)
974        );
975        assert_command_round_trip!(
976            "CREATE SERVER mycsv FOREIGN DATA WRAPPER csv OPTIONS (base_path '/data')",
977            SqlCommand::CreateServer(_)
978        );
979        assert_command_round_trip!(
980            "DROP SERVER IF EXISTS mycsv CASCADE",
981            SqlCommand::DropServer(_)
982        );
983        assert_command_round_trip!(
984            "CREATE FOREIGN TABLE ext_users (id INT, name TEXT) SERVER mycsv OPTIONS (path 'users.csv')",
985            SqlCommand::CreateForeignTable(_)
986        );
987        assert_command_round_trip!(
988            "DROP FOREIGN TABLE IF EXISTS ext_users",
989            SqlCommand::DropForeignTable(_)
990        );
991    }
992
993    #[test]
994    fn sql_command_round_trips_drop_truncate_and_maintenance_variants() {
995        macro_rules! assert_command_round_trip {
996            ($input:expr, $pattern:pat) => {{
997                let command = sql_command($input);
998                assert!(
999                    matches!(command, $pattern),
1000                    "unexpected command for {}",
1001                    $input
1002                );
1003                let statement = sql_command($input).into_statement();
1004                assert!(
1005                    matches!(statement.into_command(), $pattern),
1006                    "statement round trip changed command for {}",
1007                    $input
1008                );
1009            }};
1010        }
1011
1012        assert_command_round_trip!("DROP GRAPH IF EXISTS identity", SqlCommand::DropGraph(_));
1013        assert_command_round_trip!(
1014            "DROP VECTOR IF EXISTS embeddings",
1015            SqlCommand::DropVector(_)
1016        );
1017        assert_command_round_trip!("DROP DOCUMENT IF EXISTS docs", SqlCommand::DropDocument(_));
1018        assert_command_round_trip!("DROP KV IF EXISTS settings", SqlCommand::DropKv(_));
1019        assert_command_round_trip!("DROP CONFIG IF EXISTS app", SqlCommand::DropKv(_));
1020        assert_command_round_trip!("DROP VAULT IF EXISTS secrets", SqlCommand::DropKv(_));
1021        assert_command_round_trip!(
1022            "DROP COLLECTION IF EXISTS docs",
1023            SqlCommand::DropCollection(_)
1024        );
1025        assert_command_round_trip!(
1026            "DROP TIMESERIES IF EXISTS metrics",
1027            SqlCommand::DropTimeSeries(_)
1028        );
1029        assert_command_round_trip!(
1030            "DROP HYPERTABLE IF EXISTS metrics",
1031            SqlCommand::DropTimeSeries(_)
1032        );
1033        assert_command_round_trip!("DROP QUEUE IF EXISTS tasks", SqlCommand::DropQueue(_));
1034        assert_command_round_trip!("DROP TREE IF EXISTS org IN forest", SqlCommand::DropTree(_));
1035        assert_command_round_trip!("DROP HLL IF EXISTS visitors", SqlCommand::Probabilistic(_));
1036        assert_command_round_trip!("DROP SKETCH IF EXISTS freqs", SqlCommand::Probabilistic(_));
1037        assert_command_round_trip!("DROP FILTER IF EXISTS seen", SqlCommand::Probabilistic(_));
1038        assert_command_round_trip!(
1039            "TRUNCATE VECTOR IF EXISTS embeddings",
1040            SqlCommand::Truncate(_)
1041        );
1042        assert_command_round_trip!(
1043            "COMMIT WORK",
1044            SqlCommand::TransactionControl(TxnControl::Commit)
1045        );
1046        assert_command_round_trip!(
1047            "ROLLBACK",
1048            SqlCommand::TransactionControl(TxnControl::Rollback)
1049        );
1050        assert_command_round_trip!(
1051            "SAVEPOINT before_batch",
1052            SqlCommand::TransactionControl(TxnControl::Savepoint(_))
1053        );
1054        assert_command_round_trip!(
1055            "RELEASE SAVEPOINT before_batch",
1056            SqlCommand::TransactionControl(TxnControl::ReleaseSavepoint(_))
1057        );
1058        assert_command_round_trip!(
1059            "ANALYZE users",
1060            SqlCommand::Maintenance(MaintenanceCommand::Analyze { .. })
1061        );
1062        assert_command_round_trip!(
1063            "VACUUM",
1064            SqlCommand::Maintenance(MaintenanceCommand::Vacuum { .. })
1065        );
1066    }
1067
1068    #[test]
1069    fn parse_sql_command_covers_show_and_error_branches() {
1070        assert!(matches!(
1071            sql_command("SHOW CREATE TABLE public.users"),
1072            SqlCommand::Select(TableQuery { table, .. }) if table == "red.show_create"
1073        ));
1074        assert!(matches!(
1075            sql_command("SHOW COLLECTIONS INCLUDING INTERNAL LIMIT 2"),
1076            SqlCommand::Select(TableQuery { table, limit: Some(2), .. }) if table == "red.collections"
1077        ));
1078        assert!(matches!(
1079            sql_command("SHOW QUEUES INCLUDING INTERNAL"),
1080            SqlCommand::Select(TableQuery { table, filter: None, .. }) if table == "red.queues"
1081        ));
1082        assert!(matches!(
1083            sql_command("SHOW INDICES ON users"),
1084            SqlCommand::Select(TableQuery { table, filter: Some(_), .. }) if table == "red.show_indexes"
1085        ));
1086        assert!(matches!(
1087            sql_command("SHOW POLICIES ON users WHERE action = 'SELECT'"),
1088            SqlCommand::Select(TableQuery { table, filter: Some(_), .. }) if table == "red.policies"
1089        ));
1090        assert!(matches!(
1091            sql_command("SHOW STATS 'users' WHERE rows > 0"),
1092            SqlCommand::Select(TableQuery { table, filter: Some(_), .. }) if table == "red.stats"
1093        ));
1094        assert!(matches!(
1095            sql_command("SHOW SAMPLE users"),
1096            SqlCommand::Select(TableQuery { table, limit: Some(10), .. }) if table == "users"
1097        ));
1098        assert!(matches!(
1099            sql_command("DESC public.users"),
1100            SqlCommand::Select(TableQuery { table, filter: Some(_), .. }) if table == "red.describe"
1101        ));
1102        assert!(
1103            sql_command_result("CREATE VIEW v WITH RETENTION 1 h AS SELECT * FROM users").is_err()
1104        );
1105        assert!(sql_command_result("CREATE TABLE bad WITH ANALYTICS (centrality)").is_err());
1106        assert!(sql_command_result("BEGIN ISOLATION LEVEL SERIALIZABLE").is_err());
1107        assert!(sql_command_result("EVENTS BACKFILL STATUS users").is_err());
1108    }
1109
1110    #[test]
1111    fn parse_sql_command_covers_remaining_catalog_and_copy_shapes() {
1112        for input in [
1113            "SHOW VECTORS",
1114            "SHOW DOCUMENTS",
1115            "SHOW TIMESERIES",
1116            "SHOW GRAPHS",
1117            "SHOW CONFIGS",
1118            "SHOW VAULTS",
1119            "SHOW KV",
1120            "SHOW SCHEMA public.users",
1121        ] {
1122            assert!(
1123                matches!(sql_command(input), SqlCommand::Select(_)),
1124                "{input}"
1125            );
1126        }
1127
1128        for input in [
1129            "TRUNCATE TABLE users",
1130            "TRUNCATE GRAPH identity",
1131            "TRUNCATE DOCUMENT docs",
1132            "TRUNCATE TIMESERIES metrics",
1133            "TRUNCATE METRICS metrics",
1134            "TRUNCATE KV settings",
1135            "TRUNCATE QUEUE tasks",
1136            "TRUNCATE COLLECTION docs",
1137        ] {
1138            assert!(
1139                matches!(sql_command(input), SqlCommand::Truncate(_)),
1140                "{input}"
1141            );
1142        }
1143        assert!(sql_command_result("TRUNCATE UNKNOWN users").is_err());
1144
1145        let SqlCommand::CopyFrom(copy) = sql_command("COPY users FROM '/tmp/u.csv' WITH (HEADER)")
1146        else {
1147            panic!("expected COPY");
1148        };
1149        assert!(copy.has_header);
1150        assert_eq!(copy.delimiter, None);
1151
1152        let SqlCommand::CopyFrom(copy) =
1153            sql_command("COPY users FROM '/tmp/u.csv' WITH (HEADER = false)")
1154        else {
1155            panic!("expected COPY");
1156        };
1157        assert!(!copy.has_header);
1158
1159        let SqlCommand::CopyFrom(copy) =
1160            sql_command("COPY users FROM '/tmp/u.csv' DELIMITER '|' HEADER")
1161        else {
1162            panic!("expected COPY");
1163        };
1164        assert_eq!(copy.delimiter, Some('|'));
1165        assert!(copy.has_header);
1166    }
1167
1168    #[test]
1169    fn parse_sql_command_covers_remaining_ranking_and_event_errors() {
1170        assert!(matches!(
1171            expr("APPROXIMATE RANK OF 9 IN page_rank"),
1172            QueryExpr::ApproxRankOf(RankOfQuery { ranking, entity_id })
1173                if ranking == "page_rank" && entity_id == 9
1174        ));
1175        assert!(parse_frontend("APPROX OF 7 IN page_rank").is_err());
1176        assert!(parse_frontend("RANK 1 IN page_rank").is_err());
1177        assert!(parse_frontend("RANK RANGE 0 TO 3 IN page_rank").is_err());
1178        assert!(parse_frontend("ZRANK page_rank -1").is_err());
1179        assert!(parse_frontend("ZRANGE page_rank 3 1").is_err());
1180
1181        let QueryExpr::Table(query) = expr("EVENTS STATUS 'users' WHERE active = true") else {
1182            panic!("EVENTS STATUS should accept a quoted collection");
1183        };
1184        assert_eq!(query.table, "red.subscriptions");
1185        assert!(query.filter.is_some());
1186        assert!(query.where_expr.is_some());
1187
1188        let QueryExpr::EventsBackfill(query) = expr("EVENTS BACKFILL users TO audit") else {
1189            panic!("EVENTS BACKFILL should allow omitted filter and limit");
1190        };
1191        assert_eq!(query.collection, "users");
1192        assert_eq!(query.where_filter, None);
1193        assert_eq!(query.limit, None);
1194
1195        assert!(parse_frontend("EVENTS BACKFILL users WHERE TO audit").is_err());
1196    }
1197
1198    #[test]
1199    fn parse_sql_command_covers_analytics_non_goal_rejections() {
1200        for head in ["ANALYTICS", "EVENT", "COHORT", "FUNNEL", "SLA", "ADAPTER"] {
1201            let err = sql_command_result(&format!("CREATE {head} demo"))
1202                .expect_err("analytics v0 non-goal should be rejected");
1203            assert!(err.to_string().contains(&format!("CREATE {head}")), "{err}");
1204        }
1205    }
1206
1207    #[test]
1208    fn parse_sql_command_covers_transaction_isolation_edges() {
1209        for input in [
1210            "BEGIN ISOLATION LEVEL READ UNCOMMITTED",
1211            "BEGIN ISOLATION LEVEL READ COMMITTED",
1212            "BEGIN ISOLATION LEVEL REPEATABLE READ",
1213            "START TRANSACTION ISOLATION LEVEL SNAPSHOT",
1214        ] {
1215            assert!(
1216                matches!(
1217                    sql_command(input),
1218                    SqlCommand::TransactionControl(TxnControl::Begin)
1219                ),
1220                "{input}"
1221            );
1222        }
1223
1224        assert!(sql_command_result("BEGIN ISOLATION LEVEL READ").is_err());
1225        assert!(sql_command_result("BEGIN ISOLATION LEVEL REPEATABLE").is_err());
1226        assert!(sql_command_result("BEGIN ISOLATION LEVEL CHAOS").is_err());
1227    }
1228
1229    #[test]
1230    fn parse_sql_command_covers_vcs_working_set_verbs() {
1231        assert!(matches!(
1232            sql_command("CHECKPOINT 'initial import' AUTHOR 'Ada <ada@reddb.io>'"),
1233            SqlCommand::Vcs(VcsCommand::Checkpoint { .. })
1234        ));
1235        assert!(matches!(
1236            sql_command("CHECKOUT main"),
1237            SqlCommand::Vcs(VcsCommand::Checkout { .. })
1238        ));
1239        assert!(matches!(
1240            sql_command("RESET HARD TO abc123"),
1241            SqlCommand::Vcs(VcsCommand::Reset {
1242                mode: VcsResetMode::Hard,
1243                ..
1244            })
1245        ));
1246        assert!(matches!(
1247            sql_command("MERGE 'feature'"),
1248            SqlCommand::Vcs(VcsCommand::Merge { .. })
1249        ));
1250        assert!(matches!(
1251            sql_command("CHERRY PICK 'abc123'"),
1252            SqlCommand::Vcs(VcsCommand::CherryPick { .. })
1253        ));
1254        assert!(matches!(
1255            sql_command("REVERT 'abc123'"),
1256            SqlCommand::Vcs(VcsCommand::Revert { .. })
1257        ));
1258        assert!(matches!(
1259            sql_command("RESOLVE CONFLICT 'orders/1' USING THEIRS"),
1260            SqlCommand::Vcs(VcsCommand::ResolveConflict {
1261                resolution: VcsConflictResolution::Theirs,
1262                ..
1263            })
1264        ));
1265        assert!(matches!(
1266            sql_command("RESET TENANT"),
1267            SqlCommand::SetTenant(None)
1268        ));
1269    }
1270
1271    #[test]
1272    fn parse_sql_command_covers_iam_and_hypertable_dispatch_edges() {
1273        assert!(matches!(
1274            expr("CREATE HYPERTABLE metrics TIME_COLUMN ts CHUNK_INTERVAL '1d'"),
1275            QueryExpr::CreateTimeSeries(query)
1276                if query.name == "metrics" && query.hypertable.is_some()
1277        ));
1278        assert!(sql_command_result("CREATE OR TABLE bad (id INT)").is_err());
1279        assert!(sql_command_result("DROP MATERIALIZED TABLE bad").is_err());
1280
1281        assert!(matches!(
1282            expr("ATTACH POLICY 'readonly' TO USER tenant1.alice"),
1283            QueryExpr::AttachPolicy { policy_id, .. } if policy_id == "readonly"
1284        ));
1285        assert!(matches!(
1286            expr("DETACH POLICY 'readonly' FROM GROUP analysts"),
1287            QueryExpr::DetachPolicy { policy_id, .. } if policy_id == "readonly"
1288        ));
1289        assert!(matches!(
1290            expr("SHOW POLICIES FOR USER alice"),
1291            QueryExpr::ShowPolicies { filter: Some(_) }
1292        ));
1293        assert!(matches!(
1294            expr("SHOW EFFECTIVE PERMISSIONS FOR alice"),
1295            QueryExpr::ShowEffectivePermissions { resource: None, .. }
1296        ));
1297        assert!(matches!(
1298            expr("SIMULATE alice ACTION 'iam:PassRole' ON TABLE:public.orders"),
1299            QueryExpr::SimulatePolicy { action, .. } if action == "iam:PassRole"
1300        ));
1301        assert!(matches!(
1302            expr("LINT POLICY JSON '{\"Statement\":[]}'"),
1303            QueryExpr::LintPolicy { .. }
1304        ));
1305        assert!(matches!(
1306            expr("MIGRATE POLICY MODE TO 'policy_only' DRY RUN"),
1307            QueryExpr::MigratePolicyMode {
1308                target,
1309                dry_run: true,
1310            } if target == "policy_only"
1311        ));
1312        assert!(parse_frontend("MIGRATE OTHER").is_err());
1313    }
1314
1315    #[test]
1316    fn parse_frontend_rejects_trailing_tokens() {
1317        let err = parse_frontend("SET TENANT 'acme' junk")
1318            .expect_err("parse_frontend should reject trailing tokens");
1319        assert!(
1320            err.to_string().contains("Unexpected token after query"),
1321            "{err}"
1322        );
1323    }
1324}
1325
1326fn add_table_filter(query: &mut TableQuery, filter: Filter) {
1327    let combined = match query.filter.take() {
1328        Some(existing) => existing.and(filter),
1329        None => filter,
1330    };
1331    query.where_expr = Some(filter_to_expr(&combined));
1332    query.filter = Some(combined);
1333}
1334
1335fn parse_show_collections_by_model(
1336    parser: &mut Parser<'_>,
1337    model: &str,
1338) -> Result<TableQuery, ParseError> {
1339    let mut query = TableQuery::new("red.collections");
1340    parser.parse_table_clauses(&mut query)?;
1341    add_table_filter(&mut query, collection_model_filter(model));
1342    Ok(query)
1343}
1344
1345#[derive(Debug, Clone)]
1346#[allow(clippy::large_enum_variant)]
1347pub enum SqlQuery {
1348    Select(TableQuery),
1349    Join(JoinQuery),
1350}
1351
1352#[derive(Debug, Clone)]
1353pub enum SqlMutation {
1354    Insert(InsertQuery),
1355    Update(UpdateQuery),
1356    Delete(DeleteQuery),
1357}
1358
1359#[derive(Debug, Clone)]
1360pub enum SqlSchemaCommand {
1361    ExplainAlter(ExplainAlterQuery),
1362    CreateTable(CreateTableQuery),
1363    CreateCollection(CreateCollectionQuery),
1364    CreateVector(CreateVectorQuery),
1365    DropTable(DropTableQuery),
1366    DropGraph(DropGraphQuery),
1367    DropVector(DropVectorQuery),
1368    DropDocument(DropDocumentQuery),
1369    DropKv(DropKvQuery),
1370    DropCollection(DropCollectionQuery),
1371    Truncate(TruncateQuery),
1372    AlterTable(AlterTableQuery),
1373    CreateVcsRef(CreateVcsRefQuery),
1374    DropVcsRef(DropVcsRefQuery),
1375    CreateIndex(CreateIndexQuery),
1376    DropIndex(DropIndexQuery),
1377    CreateTimeSeries(CreateTimeSeriesQuery),
1378    CreateMetric(CreateMetricQuery),
1379    AlterMetric(AlterMetricQuery),
1380    CreateSlo(CreateSloQuery),
1381    DropTimeSeries(DropTimeSeriesQuery),
1382    CreateQueue(CreateQueueQuery),
1383    AlterQueue(AlterQueueQuery),
1384    DropQueue(DropQueueQuery),
1385    CreateTree(CreateTreeQuery),
1386    DropTree(DropTreeQuery),
1387    Probabilistic(ProbabilisticCommand),
1388    CreateSchema(CreateSchemaQuery),
1389    DropSchema(DropSchemaQuery),
1390    CreateSequence(CreateSequenceQuery),
1391    DropSequence(DropSequenceQuery),
1392    CopyFrom(CopyFromQuery),
1393    CreateView(CreateViewQuery),
1394    DropView(DropViewQuery),
1395    RefreshMaterializedView(RefreshMaterializedViewQuery),
1396    CreatePolicy(CreatePolicyQuery),
1397    DropPolicy(DropPolicyQuery),
1398    CreateServer(CreateServerQuery),
1399    DropServer(DropServerQuery),
1400    CreateForeignTable(CreateForeignTableQuery),
1401    DropForeignTable(DropForeignTableQuery),
1402    CreateMigration(CreateMigrationQuery),
1403    ApplyMigration(ApplyMigrationQuery),
1404    RollbackMigration(RollbackMigrationQuery),
1405    ExplainMigration(ExplainMigrationQuery),
1406}
1407
1408#[derive(Debug, Clone)]
1409#[allow(clippy::large_enum_variant)]
1410pub enum SqlAdminCommand {
1411    SetConfig {
1412        key: String,
1413        value: Value,
1414    },
1415    ShowConfig {
1416        prefix: Option<String>,
1417        as_json: bool,
1418    },
1419    SetSecret {
1420        key: String,
1421        value: Value,
1422    },
1423    DeleteSecret {
1424        key: String,
1425    },
1426    ShowSecrets {
1427        prefix: Option<String>,
1428    },
1429    SetTenant(Option<String>),
1430    ShowTenant,
1431    TransactionControl(TxnControl),
1432    Maintenance(MaintenanceCommand),
1433    Vcs(VcsCommand),
1434    Grant(GrantStmt),
1435    Revoke(RevokeStmt),
1436    AlterUser(AlterUserStmt),
1437    CreateUser(CreateUserStmt),
1438    IamPolicy(QueryExpr),
1439}
1440
1441impl SqlStatement {
1442    pub fn into_command(self) -> SqlCommand {
1443        match self {
1444            SqlStatement::Query(SqlQuery::Select(query)) => SqlCommand::Select(query),
1445            SqlStatement::Query(SqlQuery::Join(query)) => SqlCommand::Join(query),
1446            SqlStatement::Mutation(SqlMutation::Insert(query)) => SqlCommand::Insert(query),
1447            SqlStatement::Mutation(SqlMutation::Update(query)) => SqlCommand::Update(query),
1448            SqlStatement::Mutation(SqlMutation::Delete(query)) => SqlCommand::Delete(query),
1449            SqlStatement::Schema(SqlSchemaCommand::ExplainAlter(query)) => {
1450                SqlCommand::ExplainAlter(query)
1451            }
1452            SqlStatement::Schema(SqlSchemaCommand::CreateTable(query)) => {
1453                SqlCommand::CreateTable(query)
1454            }
1455            SqlStatement::Schema(SqlSchemaCommand::CreateCollection(query)) => {
1456                SqlCommand::CreateCollection(query)
1457            }
1458            SqlStatement::Schema(SqlSchemaCommand::CreateVector(query)) => {
1459                SqlCommand::CreateVector(query)
1460            }
1461            SqlStatement::Schema(SqlSchemaCommand::DropTable(query)) => {
1462                SqlCommand::DropTable(query)
1463            }
1464            SqlStatement::Schema(SqlSchemaCommand::DropGraph(query)) => {
1465                SqlCommand::DropGraph(query)
1466            }
1467            SqlStatement::Schema(SqlSchemaCommand::DropVector(query)) => {
1468                SqlCommand::DropVector(query)
1469            }
1470            SqlStatement::Schema(SqlSchemaCommand::DropDocument(query)) => {
1471                SqlCommand::DropDocument(query)
1472            }
1473            SqlStatement::Schema(SqlSchemaCommand::DropKv(query)) => SqlCommand::DropKv(query),
1474            SqlStatement::Schema(SqlSchemaCommand::DropCollection(query)) => {
1475                SqlCommand::DropCollection(query)
1476            }
1477            SqlStatement::Schema(SqlSchemaCommand::Truncate(query)) => SqlCommand::Truncate(query),
1478            SqlStatement::Schema(SqlSchemaCommand::AlterTable(query)) => {
1479                SqlCommand::AlterTable(query)
1480            }
1481            SqlStatement::Schema(SqlSchemaCommand::CreateVcsRef(query)) => {
1482                SqlCommand::CreateVcsRef(query)
1483            }
1484            SqlStatement::Schema(SqlSchemaCommand::DropVcsRef(query)) => {
1485                SqlCommand::DropVcsRef(query)
1486            }
1487            SqlStatement::Schema(SqlSchemaCommand::CreateIndex(query)) => {
1488                SqlCommand::CreateIndex(query)
1489            }
1490            SqlStatement::Schema(SqlSchemaCommand::DropIndex(query)) => {
1491                SqlCommand::DropIndex(query)
1492            }
1493            SqlStatement::Schema(SqlSchemaCommand::CreateTimeSeries(query)) => {
1494                SqlCommand::CreateTimeSeries(query)
1495            }
1496            SqlStatement::Schema(SqlSchemaCommand::CreateMetric(query)) => {
1497                SqlCommand::CreateMetric(query)
1498            }
1499            SqlStatement::Schema(SqlSchemaCommand::AlterMetric(query)) => {
1500                SqlCommand::AlterMetric(query)
1501            }
1502            SqlStatement::Schema(SqlSchemaCommand::CreateSlo(query)) => {
1503                SqlCommand::CreateSlo(query)
1504            }
1505            SqlStatement::Schema(SqlSchemaCommand::DropTimeSeries(query)) => {
1506                SqlCommand::DropTimeSeries(query)
1507            }
1508            SqlStatement::Schema(SqlSchemaCommand::CreateQueue(query)) => {
1509                SqlCommand::CreateQueue(query)
1510            }
1511            SqlStatement::Schema(SqlSchemaCommand::AlterQueue(query)) => {
1512                SqlCommand::AlterQueue(query)
1513            }
1514            SqlStatement::Schema(SqlSchemaCommand::DropQueue(query)) => {
1515                SqlCommand::DropQueue(query)
1516            }
1517            SqlStatement::Schema(SqlSchemaCommand::CreateTree(query)) => {
1518                SqlCommand::CreateTree(query)
1519            }
1520            SqlStatement::Schema(SqlSchemaCommand::DropTree(query)) => SqlCommand::DropTree(query),
1521            SqlStatement::Schema(SqlSchemaCommand::Probabilistic(command)) => {
1522                SqlCommand::Probabilistic(command)
1523            }
1524            SqlStatement::Admin(SqlAdminCommand::SetConfig { key, value }) => {
1525                SqlCommand::SetConfig { key, value }
1526            }
1527            SqlStatement::Admin(SqlAdminCommand::ShowConfig { prefix, as_json }) => {
1528                SqlCommand::ShowConfig { prefix, as_json }
1529            }
1530            SqlStatement::Admin(SqlAdminCommand::SetSecret { key, value }) => {
1531                SqlCommand::SetSecret { key, value }
1532            }
1533            SqlStatement::Admin(SqlAdminCommand::DeleteSecret { key }) => {
1534                SqlCommand::DeleteSecret { key }
1535            }
1536            SqlStatement::Admin(SqlAdminCommand::ShowSecrets { prefix }) => {
1537                SqlCommand::ShowSecrets { prefix }
1538            }
1539            SqlStatement::Admin(SqlAdminCommand::SetTenant(value)) => SqlCommand::SetTenant(value),
1540            SqlStatement::Admin(SqlAdminCommand::ShowTenant) => SqlCommand::ShowTenant,
1541            SqlStatement::Admin(SqlAdminCommand::TransactionControl(ctl)) => {
1542                SqlCommand::TransactionControl(ctl)
1543            }
1544            SqlStatement::Admin(SqlAdminCommand::Maintenance(cmd)) => SqlCommand::Maintenance(cmd),
1545            SqlStatement::Admin(SqlAdminCommand::Vcs(cmd)) => SqlCommand::Vcs(cmd),
1546            SqlStatement::Schema(SqlSchemaCommand::CreateSchema(q)) => SqlCommand::CreateSchema(q),
1547            SqlStatement::Schema(SqlSchemaCommand::DropSchema(q)) => SqlCommand::DropSchema(q),
1548            SqlStatement::Schema(SqlSchemaCommand::CreateSequence(q)) => {
1549                SqlCommand::CreateSequence(q)
1550            }
1551            SqlStatement::Schema(SqlSchemaCommand::DropSequence(q)) => SqlCommand::DropSequence(q),
1552            SqlStatement::Schema(SqlSchemaCommand::CopyFrom(q)) => SqlCommand::CopyFrom(q),
1553            SqlStatement::Schema(SqlSchemaCommand::CreateView(q)) => SqlCommand::CreateView(q),
1554            SqlStatement::Schema(SqlSchemaCommand::DropView(q)) => SqlCommand::DropView(q),
1555            SqlStatement::Schema(SqlSchemaCommand::RefreshMaterializedView(q)) => {
1556                SqlCommand::RefreshMaterializedView(q)
1557            }
1558            SqlStatement::Schema(SqlSchemaCommand::CreatePolicy(q)) => SqlCommand::CreatePolicy(q),
1559            SqlStatement::Schema(SqlSchemaCommand::DropPolicy(q)) => SqlCommand::DropPolicy(q),
1560            SqlStatement::Schema(SqlSchemaCommand::CreateServer(q)) => SqlCommand::CreateServer(q),
1561            SqlStatement::Schema(SqlSchemaCommand::DropServer(q)) => SqlCommand::DropServer(q),
1562            SqlStatement::Schema(SqlSchemaCommand::CreateForeignTable(q)) => {
1563                SqlCommand::CreateForeignTable(q)
1564            }
1565            SqlStatement::Schema(SqlSchemaCommand::DropForeignTable(q)) => {
1566                SqlCommand::DropForeignTable(q)
1567            }
1568            SqlStatement::Admin(SqlAdminCommand::Grant(s)) => SqlCommand::Grant(s),
1569            SqlStatement::Admin(SqlAdminCommand::Revoke(s)) => SqlCommand::Revoke(s),
1570            SqlStatement::Admin(SqlAdminCommand::AlterUser(s)) => SqlCommand::AlterUser(s),
1571            SqlStatement::Admin(SqlAdminCommand::CreateUser(s)) => SqlCommand::CreateUser(s),
1572            SqlStatement::Admin(SqlAdminCommand::IamPolicy(e)) => SqlCommand::IamPolicy(e),
1573            SqlStatement::Schema(SqlSchemaCommand::CreateMigration(q)) => {
1574                SqlCommand::CreateMigration(q)
1575            }
1576            SqlStatement::Schema(SqlSchemaCommand::ApplyMigration(q)) => {
1577                SqlCommand::ApplyMigration(q)
1578            }
1579            SqlStatement::Schema(SqlSchemaCommand::RollbackMigration(q)) => {
1580                SqlCommand::RollbackMigration(q)
1581            }
1582            SqlStatement::Schema(SqlSchemaCommand::ExplainMigration(q)) => {
1583                SqlCommand::ExplainMigration(q)
1584            }
1585        }
1586    }
1587
1588    pub fn into_query_expr(self) -> QueryExpr {
1589        self.into_command().into_query_expr()
1590    }
1591}
1592
1593impl FrontendStatement {
1594    pub fn into_query_expr(self) -> QueryExpr {
1595        match self {
1596            FrontendStatement::Sql(statement) => statement.into_query_expr(),
1597            FrontendStatement::Graph(query) => QueryExpr::Graph(query),
1598            FrontendStatement::GraphCommand(command) => QueryExpr::GraphCommand(command),
1599            FrontendStatement::Path(query) => QueryExpr::Path(query),
1600            FrontendStatement::Vector(query) => QueryExpr::Vector(query),
1601            FrontendStatement::Hybrid(query) => QueryExpr::Hybrid(query),
1602            FrontendStatement::Search(command) => QueryExpr::SearchCommand(command),
1603            FrontendStatement::Ask(query) => QueryExpr::Ask(query),
1604            FrontendStatement::QueueSelect(query) => QueryExpr::QueueSelect(query),
1605            FrontendStatement::QueueCommand(command) => QueryExpr::QueueCommand(command),
1606            FrontendStatement::EventsBackfill(query) => QueryExpr::EventsBackfill(query),
1607            FrontendStatement::EventsBackfillStatus { collection } => {
1608                QueryExpr::EventsBackfillStatus { collection }
1609            }
1610            FrontendStatement::TreeCommand(command) => QueryExpr::TreeCommand(command),
1611            FrontendStatement::ProbabilisticCommand(command) => {
1612                QueryExpr::ProbabilisticCommand(command)
1613            }
1614            FrontendStatement::KvCommand(command) => QueryExpr::KvCommand(command),
1615            FrontendStatement::ConfigCommand(command) => QueryExpr::ConfigCommand(command),
1616            FrontendStatement::Ranking(expr) => expr,
1617        }
1618    }
1619}
1620
1621pub fn parse_frontend(input: &str) -> Result<FrontendStatement, ParseError> {
1622    let mut parser = Parser::new(input)?;
1623    let statement = parser.parse_frontend_statement()?;
1624    if !parser.check(&Token::Eof) {
1625        return Err(ParseError::new(
1626            // F-05: `Token::Ident` / `Token::String` / `Token::JsonLiteral`
1627            // Display arms emit raw user bytes. Render via `{:?}` so
1628            // embedded CR/LF/NUL/quotes are escaped before the message
1629            // reaches downstream JSON / audit / log / gRPC sinks.
1630            format!("Unexpected token after query: {:?}", parser.current.token),
1631            parser.position(),
1632        ));
1633    }
1634    Ok(statement)
1635}
1636
1637fn is_vcs_command_head(name: &str) -> bool {
1638    name.eq_ignore_ascii_case("CHECKPOINT")
1639        || name.eq_ignore_ascii_case("CHECKOUT")
1640        || name.eq_ignore_ascii_case("MERGE")
1641        || name.eq_ignore_ascii_case("CHERRY")
1642        || name.eq_ignore_ascii_case("REVERT")
1643    // NOTE: RESOLVE is intentionally excluded here — it is shared with the
1644    // config surface (`RESOLVE CONFIG …`). The VCS `RESOLVE CONFLICT` shape is
1645    // dispatched by a dedicated arm that peeks for the CONFLICT keyword.
1646}
1647
1648impl SqlCommand {
1649    pub fn into_query_expr(self) -> QueryExpr {
1650        match self {
1651            SqlCommand::Select(query) => QueryExpr::Table(query),
1652            SqlCommand::Join(query) => QueryExpr::Join(query),
1653            SqlCommand::Insert(query) => QueryExpr::Insert(query),
1654            SqlCommand::Update(query) => QueryExpr::Update(query),
1655            SqlCommand::Delete(query) => QueryExpr::Delete(query),
1656            SqlCommand::ExplainAlter(query) => QueryExpr::ExplainAlter(query),
1657            SqlCommand::CreateTable(query) => QueryExpr::CreateTable(query),
1658            SqlCommand::CreateCollection(query) => QueryExpr::CreateCollection(query),
1659            SqlCommand::CreateVector(query) => QueryExpr::CreateVector(query),
1660            SqlCommand::DropTable(query) => QueryExpr::DropTable(query),
1661            SqlCommand::DropGraph(query) => QueryExpr::DropGraph(query),
1662            SqlCommand::DropVector(query) => QueryExpr::DropVector(query),
1663            SqlCommand::DropDocument(query) => QueryExpr::DropDocument(query),
1664            SqlCommand::DropKv(query) => QueryExpr::DropKv(query),
1665            SqlCommand::DropCollection(query) => QueryExpr::DropCollection(query),
1666            SqlCommand::Truncate(query) => QueryExpr::Truncate(query),
1667            SqlCommand::AlterTable(query) => QueryExpr::AlterTable(query),
1668            SqlCommand::CreateVcsRef(query) => QueryExpr::CreateVcsRef(query),
1669            SqlCommand::DropVcsRef(query) => QueryExpr::DropVcsRef(query),
1670            SqlCommand::CreateIndex(query) => QueryExpr::CreateIndex(query),
1671            SqlCommand::DropIndex(query) => QueryExpr::DropIndex(query),
1672            SqlCommand::CreateTimeSeries(query) => QueryExpr::CreateTimeSeries(query),
1673            SqlCommand::CreateMetric(query) => QueryExpr::CreateMetric(query),
1674            SqlCommand::AlterMetric(query) => QueryExpr::AlterMetric(query),
1675            SqlCommand::CreateSlo(query) => QueryExpr::CreateSlo(query),
1676            SqlCommand::DropTimeSeries(query) => QueryExpr::DropTimeSeries(query),
1677            SqlCommand::CreateQueue(query) => QueryExpr::CreateQueue(query),
1678            SqlCommand::AlterQueue(query) => QueryExpr::AlterQueue(query),
1679            SqlCommand::DropQueue(query) => QueryExpr::DropQueue(query),
1680            SqlCommand::CreateTree(query) => QueryExpr::CreateTree(query),
1681            SqlCommand::DropTree(query) => QueryExpr::DropTree(query),
1682            SqlCommand::Probabilistic(command) => QueryExpr::ProbabilisticCommand(command),
1683            SqlCommand::SetConfig { key, value } => QueryExpr::SetConfig { key, value },
1684            SqlCommand::ShowConfig { prefix, as_json } => QueryExpr::ShowConfig { prefix, as_json },
1685            SqlCommand::SetSecret { key, value } => QueryExpr::SetSecret { key, value },
1686            SqlCommand::DeleteSecret { key } => QueryExpr::DeleteSecret { key },
1687            SqlCommand::ShowSecrets { prefix } => QueryExpr::ShowSecrets { prefix },
1688            SqlCommand::SetTenant(value) => QueryExpr::SetTenant(value),
1689            SqlCommand::ShowTenant => QueryExpr::ShowTenant,
1690            SqlCommand::TransactionControl(ctl) => QueryExpr::TransactionControl(ctl),
1691            SqlCommand::Maintenance(cmd) => QueryExpr::MaintenanceCommand(cmd),
1692            SqlCommand::Vcs(cmd) => QueryExpr::VcsCommand(cmd),
1693            SqlCommand::CreateSchema(q) => QueryExpr::CreateSchema(q),
1694            SqlCommand::DropSchema(q) => QueryExpr::DropSchema(q),
1695            SqlCommand::CreateSequence(q) => QueryExpr::CreateSequence(q),
1696            SqlCommand::DropSequence(q) => QueryExpr::DropSequence(q),
1697            SqlCommand::CopyFrom(q) => QueryExpr::CopyFrom(q),
1698            SqlCommand::CreateView(q) => QueryExpr::CreateView(q),
1699            SqlCommand::DropView(q) => QueryExpr::DropView(q),
1700            SqlCommand::RefreshMaterializedView(q) => QueryExpr::RefreshMaterializedView(q),
1701            SqlCommand::CreatePolicy(q) => QueryExpr::CreatePolicy(q),
1702            SqlCommand::DropPolicy(q) => QueryExpr::DropPolicy(q),
1703            SqlCommand::CreateServer(q) => QueryExpr::CreateServer(q),
1704            SqlCommand::DropServer(q) => QueryExpr::DropServer(q),
1705            SqlCommand::CreateForeignTable(q) => QueryExpr::CreateForeignTable(q),
1706            SqlCommand::DropForeignTable(q) => QueryExpr::DropForeignTable(q),
1707            SqlCommand::Grant(s) => QueryExpr::Grant(s),
1708            SqlCommand::Revoke(s) => QueryExpr::Revoke(s),
1709            SqlCommand::AlterUser(s) => QueryExpr::AlterUser(s),
1710            SqlCommand::CreateUser(s) => QueryExpr::CreateUser(s),
1711            SqlCommand::IamPolicy(e) => e,
1712            SqlCommand::CreateMigration(q) => QueryExpr::CreateMigration(q),
1713            SqlCommand::ApplyMigration(q) => QueryExpr::ApplyMigration(q),
1714            SqlCommand::RollbackMigration(q) => QueryExpr::RollbackMigration(q),
1715            SqlCommand::ExplainMigration(q) => QueryExpr::ExplainMigration(q),
1716        }
1717    }
1718
1719    pub fn into_statement(self) -> SqlStatement {
1720        match self {
1721            SqlCommand::Select(query) => SqlStatement::Query(SqlQuery::Select(query)),
1722            SqlCommand::Join(query) => SqlStatement::Query(SqlQuery::Join(query)),
1723            SqlCommand::Insert(query) => SqlStatement::Mutation(SqlMutation::Insert(query)),
1724            SqlCommand::Update(query) => SqlStatement::Mutation(SqlMutation::Update(query)),
1725            SqlCommand::Delete(query) => SqlStatement::Mutation(SqlMutation::Delete(query)),
1726            SqlCommand::ExplainAlter(query) => {
1727                SqlStatement::Schema(SqlSchemaCommand::ExplainAlter(query))
1728            }
1729            SqlCommand::CreateTable(query) => {
1730                SqlStatement::Schema(SqlSchemaCommand::CreateTable(query))
1731            }
1732            SqlCommand::CreateCollection(query) => {
1733                SqlStatement::Schema(SqlSchemaCommand::CreateCollection(query))
1734            }
1735            SqlCommand::CreateVector(query) => {
1736                SqlStatement::Schema(SqlSchemaCommand::CreateVector(query))
1737            }
1738            SqlCommand::DropTable(query) => {
1739                SqlStatement::Schema(SqlSchemaCommand::DropTable(query))
1740            }
1741            SqlCommand::DropGraph(query) => {
1742                SqlStatement::Schema(SqlSchemaCommand::DropGraph(query))
1743            }
1744            SqlCommand::DropVector(query) => {
1745                SqlStatement::Schema(SqlSchemaCommand::DropVector(query))
1746            }
1747            SqlCommand::DropDocument(query) => {
1748                SqlStatement::Schema(SqlSchemaCommand::DropDocument(query))
1749            }
1750            SqlCommand::DropKv(query) => SqlStatement::Schema(SqlSchemaCommand::DropKv(query)),
1751            SqlCommand::DropCollection(query) => {
1752                SqlStatement::Schema(SqlSchemaCommand::DropCollection(query))
1753            }
1754            SqlCommand::Truncate(query) => SqlStatement::Schema(SqlSchemaCommand::Truncate(query)),
1755            SqlCommand::AlterTable(query) => {
1756                SqlStatement::Schema(SqlSchemaCommand::AlterTable(query))
1757            }
1758            SqlCommand::CreateVcsRef(query) => {
1759                SqlStatement::Schema(SqlSchemaCommand::CreateVcsRef(query))
1760            }
1761            SqlCommand::DropVcsRef(query) => {
1762                SqlStatement::Schema(SqlSchemaCommand::DropVcsRef(query))
1763            }
1764            SqlCommand::CreateIndex(query) => {
1765                SqlStatement::Schema(SqlSchemaCommand::CreateIndex(query))
1766            }
1767            SqlCommand::DropIndex(query) => {
1768                SqlStatement::Schema(SqlSchemaCommand::DropIndex(query))
1769            }
1770            SqlCommand::CreateTimeSeries(query) => {
1771                SqlStatement::Schema(SqlSchemaCommand::CreateTimeSeries(query))
1772            }
1773            SqlCommand::CreateMetric(query) => {
1774                SqlStatement::Schema(SqlSchemaCommand::CreateMetric(query))
1775            }
1776            SqlCommand::AlterMetric(query) => {
1777                SqlStatement::Schema(SqlSchemaCommand::AlterMetric(query))
1778            }
1779            SqlCommand::CreateSlo(query) => {
1780                SqlStatement::Schema(SqlSchemaCommand::CreateSlo(query))
1781            }
1782            SqlCommand::DropTimeSeries(query) => {
1783                SqlStatement::Schema(SqlSchemaCommand::DropTimeSeries(query))
1784            }
1785            SqlCommand::CreateQueue(query) => {
1786                SqlStatement::Schema(SqlSchemaCommand::CreateQueue(query))
1787            }
1788            SqlCommand::AlterQueue(query) => {
1789                SqlStatement::Schema(SqlSchemaCommand::AlterQueue(query))
1790            }
1791            SqlCommand::DropQueue(query) => {
1792                SqlStatement::Schema(SqlSchemaCommand::DropQueue(query))
1793            }
1794            SqlCommand::CreateTree(query) => {
1795                SqlStatement::Schema(SqlSchemaCommand::CreateTree(query))
1796            }
1797            SqlCommand::DropTree(query) => SqlStatement::Schema(SqlSchemaCommand::DropTree(query)),
1798            SqlCommand::Probabilistic(command) => {
1799                SqlStatement::Schema(SqlSchemaCommand::Probabilistic(command))
1800            }
1801            SqlCommand::SetConfig { key, value } => {
1802                SqlStatement::Admin(SqlAdminCommand::SetConfig { key, value })
1803            }
1804            SqlCommand::ShowConfig { prefix, as_json } => {
1805                SqlStatement::Admin(SqlAdminCommand::ShowConfig { prefix, as_json })
1806            }
1807            SqlCommand::SetSecret { key, value } => {
1808                SqlStatement::Admin(SqlAdminCommand::SetSecret { key, value })
1809            }
1810            SqlCommand::DeleteSecret { key } => {
1811                SqlStatement::Admin(SqlAdminCommand::DeleteSecret { key })
1812            }
1813            SqlCommand::ShowSecrets { prefix } => {
1814                SqlStatement::Admin(SqlAdminCommand::ShowSecrets { prefix })
1815            }
1816            SqlCommand::SetTenant(value) => SqlStatement::Admin(SqlAdminCommand::SetTenant(value)),
1817            SqlCommand::ShowTenant => SqlStatement::Admin(SqlAdminCommand::ShowTenant),
1818            SqlCommand::TransactionControl(ctl) => {
1819                SqlStatement::Admin(SqlAdminCommand::TransactionControl(ctl))
1820            }
1821            SqlCommand::Maintenance(cmd) => SqlStatement::Admin(SqlAdminCommand::Maintenance(cmd)),
1822            SqlCommand::Vcs(cmd) => SqlStatement::Admin(SqlAdminCommand::Vcs(cmd)),
1823            SqlCommand::CreateSchema(q) => SqlStatement::Schema(SqlSchemaCommand::CreateSchema(q)),
1824            SqlCommand::DropSchema(q) => SqlStatement::Schema(SqlSchemaCommand::DropSchema(q)),
1825            SqlCommand::CreateSequence(q) => {
1826                SqlStatement::Schema(SqlSchemaCommand::CreateSequence(q))
1827            }
1828            SqlCommand::DropSequence(q) => SqlStatement::Schema(SqlSchemaCommand::DropSequence(q)),
1829            SqlCommand::CopyFrom(q) => SqlStatement::Schema(SqlSchemaCommand::CopyFrom(q)),
1830            SqlCommand::CreateView(q) => SqlStatement::Schema(SqlSchemaCommand::CreateView(q)),
1831            SqlCommand::DropView(q) => SqlStatement::Schema(SqlSchemaCommand::DropView(q)),
1832            SqlCommand::RefreshMaterializedView(q) => {
1833                SqlStatement::Schema(SqlSchemaCommand::RefreshMaterializedView(q))
1834            }
1835            SqlCommand::CreatePolicy(q) => SqlStatement::Schema(SqlSchemaCommand::CreatePolicy(q)),
1836            SqlCommand::DropPolicy(q) => SqlStatement::Schema(SqlSchemaCommand::DropPolicy(q)),
1837            SqlCommand::CreateServer(q) => SqlStatement::Schema(SqlSchemaCommand::CreateServer(q)),
1838            SqlCommand::DropServer(q) => SqlStatement::Schema(SqlSchemaCommand::DropServer(q)),
1839            SqlCommand::CreateForeignTable(q) => {
1840                SqlStatement::Schema(SqlSchemaCommand::CreateForeignTable(q))
1841            }
1842            SqlCommand::DropForeignTable(q) => {
1843                SqlStatement::Schema(SqlSchemaCommand::DropForeignTable(q))
1844            }
1845            SqlCommand::Grant(s) => SqlStatement::Admin(SqlAdminCommand::Grant(s)),
1846            SqlCommand::Revoke(s) => SqlStatement::Admin(SqlAdminCommand::Revoke(s)),
1847            SqlCommand::AlterUser(s) => SqlStatement::Admin(SqlAdminCommand::AlterUser(s)),
1848            SqlCommand::CreateUser(s) => SqlStatement::Admin(SqlAdminCommand::CreateUser(s)),
1849            SqlCommand::IamPolicy(e) => SqlStatement::Admin(SqlAdminCommand::IamPolicy(e)),
1850            SqlCommand::CreateMigration(q) => {
1851                SqlStatement::Schema(SqlSchemaCommand::CreateMigration(q))
1852            }
1853            SqlCommand::ApplyMigration(q) => {
1854                SqlStatement::Schema(SqlSchemaCommand::ApplyMigration(q))
1855            }
1856            SqlCommand::RollbackMigration(q) => {
1857                SqlStatement::Schema(SqlSchemaCommand::RollbackMigration(q))
1858            }
1859            SqlCommand::ExplainMigration(q) => {
1860                SqlStatement::Schema(SqlSchemaCommand::ExplainMigration(q))
1861            }
1862        }
1863    }
1864}
1865
1866impl<'a> Parser<'a> {
1867    fn parse_events_command(&mut self) -> Result<QueryExpr, ParseError> {
1868        self.expect_ident()?; // EVENTS
1869        if self.consume_ident_ci("STATUS")? {
1870            let mut query = TableQuery::new("red.subscriptions");
1871            let collection = match self.peek().clone() {
1872                Token::Ident(name) => {
1873                    self.advance()?;
1874                    Some(name)
1875                }
1876                Token::String(name) => {
1877                    self.advance()?;
1878                    Some(name)
1879                }
1880                _ => None,
1881            };
1882            self.parse_table_clauses(&mut query)?;
1883            if let Some(collection) = collection {
1884                let filter = Filter::compare(
1885                    FieldRef::column("red.subscriptions", "collection"),
1886                    CompareOp::Eq,
1887                    Value::text(collection),
1888                );
1889                let expr = filter_to_expr(&filter);
1890                query.where_expr = Some(match query.where_expr.take() {
1891                    Some(existing) => Expr::binop(BinOp::And, existing, expr),
1892                    None => expr,
1893                });
1894                query.filter = Some(match query.filter.take() {
1895                    Some(existing) => existing.and(filter),
1896                    None => filter,
1897                });
1898            }
1899            return Ok(QueryExpr::Table(query));
1900        }
1901
1902        if !self.consume_ident_ci("BACKFILL")? {
1903            return Err(ParseError::expected(
1904                vec!["BACKFILL", "STATUS"],
1905                self.peek(),
1906                self.position(),
1907            ));
1908        }
1909
1910        if self.consume_ident_ci("STATUS")? {
1911            let collection = self.expect_ident()?;
1912            return Ok(QueryExpr::EventsBackfillStatus { collection });
1913        }
1914
1915        let collection = self.expect_ident()?;
1916        let where_filter = if self.consume(&Token::Where)? {
1917            let mut parts = Vec::new();
1918            while !self.check(&Token::Eof) && !self.check(&Token::To) {
1919                parts.push(self.peek().to_string());
1920                self.advance()?;
1921            }
1922            if parts.is_empty() {
1923                return Err(ParseError::expected(
1924                    vec!["predicate"],
1925                    self.peek(),
1926                    self.position(),
1927                ));
1928            }
1929            Some(parts.join(" "))
1930        } else {
1931            None
1932        };
1933
1934        self.expect(Token::To)?;
1935        let target_queue = self.expect_ident()?;
1936        let limit = if self.consume(&Token::Limit)? {
1937            Some(self.parse_positive_integer("LIMIT")? as u64)
1938        } else {
1939            None
1940        };
1941
1942        Ok(QueryExpr::EventsBackfill(EventsBackfillQuery {
1943            collection,
1944            where_filter,
1945            target_queue,
1946            limit,
1947        }))
1948    }
1949
1950    /// Parse an optional `OPTIONS (key 'value', key2 'value2', ...)` clause
1951    /// used by Phase 3.2 FDW DDL statements. Returns an empty vec when the
1952    /// clause is absent. Values are always single-quoted string literals —
1953    /// consistent with PG's generic-options model.
1954    pub(crate) fn parse_fdw_options_clause(&mut self) -> Result<Vec<(String, String)>, ParseError> {
1955        if !self.consume(&Token::Options)? {
1956            return Ok(Vec::new());
1957        }
1958        self.expect(Token::LParen)?;
1959        let mut out: Vec<(String, String)> = Vec::new();
1960        loop {
1961            // Option keys frequently collide with reserved words
1962            // (`path`, `format`, `delimiter`, `header`, …) — accept
1963            // the keyword form and lowercase it so downstream
1964            // option-name matching stays case-insensitive.
1965            let was_ident = matches!(self.peek(), Token::Ident(_));
1966            let raw = self.expect_ident_or_keyword()?;
1967            let key = if was_ident {
1968                raw
1969            } else {
1970                raw.to_ascii_lowercase()
1971            };
1972            // Value is a single-quoted string literal.
1973            let value = self.parse_string()?;
1974            out.push((key, value));
1975            if !self.consume(&Token::Comma)? {
1976                break;
1977            }
1978        }
1979        self.expect(Token::RParen)?;
1980        Ok(out)
1981    }
1982
1983    /// Parse any top-level frontend statement through a single shared surface.
1984    pub fn parse_frontend_statement(&mut self) -> Result<FrontendStatement, ParseError> {
1985        self.enter_depth()?;
1986        let result = self.parse_frontend_statement_inner();
1987        self.exit_depth();
1988        result
1989    }
1990
1991    fn parse_frontend_statement_inner(&mut self) -> Result<FrontendStatement, ParseError> {
1992        match self.peek() {
1993            Token::Select => match self.parse_select_query()? {
1994                QueryExpr::Table(query) => Ok(FrontendStatement::Sql(SqlStatement::Query(
1995                    SqlQuery::Select(query),
1996                ))),
1997                QueryExpr::Join(query) => Ok(FrontendStatement::Sql(SqlStatement::Query(
1998                    SqlQuery::Join(query),
1999                ))),
2000                QueryExpr::QueueSelect(query) => Ok(FrontendStatement::QueueSelect(query)),
2001                other => Err(ParseError::new(
2002                    format!("internal: SELECT produced unexpected query kind {other:?}"),
2003                    self.position(),
2004                )),
2005            },
2006            Token::From
2007            | Token::Insert
2008            | Token::Update
2009            | Token::Truncate
2010            | Token::Create
2011            | Token::Drop
2012            | Token::Alter
2013            | Token::Set
2014            | Token::Begin
2015            | Token::Commit
2016            | Token::Rollback
2017            | Token::Savepoint
2018            | Token::Release
2019            | Token::Start
2020            | Token::Vacuum
2021            | Token::Analyze
2022            | Token::Copy
2023            | Token::Refresh => self.parse_sql_statement().map(FrontendStatement::Sql),
2024            Token::Explain => {
2025                if matches!(
2026                    self.peek_next()?,
2027                    Token::Ident(name) if name.eq_ignore_ascii_case("ASK")
2028                ) {
2029                    match self.parse_explain_ask_query()? {
2030                        QueryExpr::Ask(query) => Ok(FrontendStatement::Ask(query)),
2031                        other => Err(ParseError::new(
2032                            format!(
2033                                "internal: EXPLAIN ASK produced unexpected query kind {other:?}"
2034                            ),
2035                            self.position(),
2036                        )),
2037                    }
2038                } else {
2039                    self.parse_sql_statement().map(FrontendStatement::Sql)
2040                }
2041            }
2042            Token::Ident(name) if name.eq_ignore_ascii_case("SHOW") => {
2043                self.parse_sql_statement().map(FrontendStatement::Sql)
2044            }
2045            Token::Ident(name) if name.eq_ignore_ascii_case("RESET") => {
2046                self.parse_sql_statement().map(FrontendStatement::Sql)
2047            }
2048            Token::Ident(name)
2049                if name.eq_ignore_ascii_case("RANK")
2050                    || name.eq_ignore_ascii_case("APPROX")
2051                    || name.eq_ignore_ascii_case("APPROXIMATE")
2052                    || name.eq_ignore_ascii_case("ZRANK")
2053                    || name.eq_ignore_ascii_case("ZRANGE") =>
2054            {
2055                self.parse_ranking_read().map(FrontendStatement::Ranking)
2056            }
2057            Token::Desc => self.parse_sql_statement().map(FrontendStatement::Sql),
2058            Token::Ident(name)
2059                if name.eq_ignore_ascii_case("DESCRIBE") || name.eq_ignore_ascii_case("DESC") =>
2060            {
2061                self.parse_sql_statement().map(FrontendStatement::Sql)
2062            }
2063            Token::Ident(name)
2064                if name.eq_ignore_ascii_case("GRANT")
2065                    || name.eq_ignore_ascii_case("REVOKE")
2066                    || name.eq_ignore_ascii_case("SIMULATE")
2067                    || name.eq_ignore_ascii_case("LINT")
2068                    || name.eq_ignore_ascii_case("MIGRATE")
2069                    || name.eq_ignore_ascii_case("APPLY") =>
2070            {
2071                self.parse_sql_statement().map(FrontendStatement::Sql)
2072            }
2073            Token::Ident(name) if name.eq_ignore_ascii_case("WATCH") => {
2074                self.advance()?;
2075                if matches!(
2076                    self.peek(),
2077                    Token::Ident(name) if name.eq_ignore_ascii_case("CONFIG")
2078                ) {
2079                    match self.parse_config_watch_after_watch()? {
2080                        QueryExpr::ConfigCommand(command) => {
2081                            Ok(FrontendStatement::ConfigCommand(command))
2082                        }
2083                        other => Err(ParseError::new(
2084                            format!(
2085                                "internal: WATCH CONFIG produced unexpected query kind {other:?}"
2086                            ),
2087                            self.position(),
2088                        )),
2089                    }
2090                } else if matches!(
2091                    self.peek(),
2092                    Token::Ident(name) if name.eq_ignore_ascii_case("VAULT")
2093                ) {
2094                    match self.parse_vault_watch_after_watch()? {
2095                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2096                        other => Err(ParseError::new(
2097                            format!(
2098                                "internal: WATCH VAULT produced unexpected query kind {other:?}"
2099                            ),
2100                            self.position(),
2101                        )),
2102                    }
2103                } else {
2104                    match self.parse_kv_watch(reddb_types::catalog::CollectionModel::Kv)? {
2105                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2106                        other => Err(ParseError::new(
2107                            format!("internal: WATCH produced unexpected query kind {other:?}"),
2108                            self.position(),
2109                        )),
2110                    }
2111                }
2112            }
2113            Token::List => {
2114                self.advance()?;
2115                if matches!(
2116                    self.peek(),
2117                    Token::Ident(name) if name.eq_ignore_ascii_case("CONFIG")
2118                ) {
2119                    match self.parse_config_list_after_list()? {
2120                        QueryExpr::ConfigCommand(command) => {
2121                            Ok(FrontendStatement::ConfigCommand(command))
2122                        }
2123                        other => Err(ParseError::new(
2124                            format!(
2125                                "internal: LIST CONFIG produced unexpected query kind {other:?}"
2126                            ),
2127                            self.position(),
2128                        )),
2129                    }
2130                } else if matches!(self.peek(), Token::Kv) {
2131                    match self.parse_kv_list_after_list()? {
2132                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2133                        other => Err(ParseError::new(
2134                            format!("internal: LIST KV produced unexpected query kind {other:?}"),
2135                            self.position(),
2136                        )),
2137                    }
2138                } else if matches!(
2139                    self.peek(),
2140                    Token::Ident(name) if name.eq_ignore_ascii_case("VAULT")
2141                ) {
2142                    match self.parse_vault_list_after_list()? {
2143                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2144                        other => Err(ParseError::new(
2145                            format!(
2146                                "internal: LIST VAULT produced unexpected query kind {other:?}"
2147                            ),
2148                            self.position(),
2149                        )),
2150                    }
2151                } else {
2152                    Err(ParseError::expected(
2153                        vec!["CONFIG", "KV", "VAULT"],
2154                        self.peek(),
2155                        self.position(),
2156                    ))
2157                }
2158            }
2159            Token::Ident(name) if name.eq_ignore_ascii_case("LIST") => {
2160                self.advance()?;
2161                if matches!(
2162                    self.peek(),
2163                    Token::Ident(name) if name.eq_ignore_ascii_case("CONFIG")
2164                ) {
2165                    match self.parse_config_list_after_list()? {
2166                        QueryExpr::ConfigCommand(command) => {
2167                            Ok(FrontendStatement::ConfigCommand(command))
2168                        }
2169                        other => Err(ParseError::new(
2170                            format!(
2171                                "internal: LIST CONFIG produced unexpected query kind {other:?}"
2172                            ),
2173                            self.position(),
2174                        )),
2175                    }
2176                } else if matches!(self.peek(), Token::Kv) {
2177                    match self.parse_kv_list_after_list()? {
2178                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2179                        other => Err(ParseError::new(
2180                            format!("internal: LIST KV produced unexpected query kind {other:?}"),
2181                            self.position(),
2182                        )),
2183                    }
2184                } else if matches!(
2185                    self.peek(),
2186                    Token::Ident(name) if name.eq_ignore_ascii_case("VAULT")
2187                ) {
2188                    match self.parse_vault_list_after_list()? {
2189                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2190                        other => Err(ParseError::new(
2191                            format!(
2192                                "internal: LIST VAULT produced unexpected query kind {other:?}"
2193                            ),
2194                            self.position(),
2195                        )),
2196                    }
2197                } else {
2198                    Err(ParseError::expected(
2199                        vec!["CONFIG", "KV", "VAULT"],
2200                        self.peek(),
2201                        self.position(),
2202                    ))
2203                }
2204            }
2205            Token::Ident(name) if name.eq_ignore_ascii_case("INVALIDATE") => {
2206                if matches!(
2207                    self.peek_next()?,
2208                    Token::Ident(next) if next.eq_ignore_ascii_case("CONFIG")
2209                ) {
2210                    match self.parse_config_command()? {
2211                        QueryExpr::ConfigCommand(command) => {
2212                            Ok(FrontendStatement::ConfigCommand(command))
2213                        }
2214                        other => Err(ParseError::new(
2215                            format!("internal: CONFIG produced unexpected query kind {other:?}"),
2216                            self.position(),
2217                        )),
2218                    }
2219                } else {
2220                    self.advance()?;
2221                    match self.parse_kv_invalidate_tags_after_invalidate()? {
2222                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2223                        other => Err(ParseError::new(
2224                            format!(
2225                                "internal: INVALIDATE produced unexpected query kind {other:?}"
2226                            ),
2227                            self.position(),
2228                        )),
2229                    }
2230                }
2231            }
2232            Token::Attach | Token::Detach => self.parse_sql_statement().map(FrontendStatement::Sql),
2233            Token::Match => match self.parse_match_query()? {
2234                QueryExpr::Graph(query) => Ok(FrontendStatement::Graph(query)),
2235                other => Err(ParseError::new(
2236                    format!("internal: MATCH produced unexpected query kind {other:?}"),
2237                    self.position(),
2238                )),
2239            },
2240            Token::Path => match self.parse_path_query()? {
2241                QueryExpr::Path(query) => Ok(FrontendStatement::Path(query)),
2242                other => Err(ParseError::new(
2243                    format!("internal: PATH produced unexpected query kind {other:?}"),
2244                    self.position(),
2245                )),
2246            },
2247            Token::Vector => match self.parse_vector_query()? {
2248                QueryExpr::Vector(query) => Ok(FrontendStatement::Vector(query)),
2249                other => Err(ParseError::new(
2250                    format!("internal: VECTOR produced unexpected query kind {other:?}"),
2251                    self.position(),
2252                )),
2253            },
2254            Token::Hybrid => match self.parse_hybrid_query()? {
2255                QueryExpr::Hybrid(query) => Ok(FrontendStatement::Hybrid(query)),
2256                other => Err(ParseError::new(
2257                    format!("internal: HYBRID produced unexpected query kind {other:?}"),
2258                    self.position(),
2259                )),
2260            },
2261            Token::Graph => match self.parse_graph_command()? {
2262                QueryExpr::GraphCommand(command) => Ok(FrontendStatement::GraphCommand(command)),
2263                other => Err(ParseError::new(
2264                    format!("internal: GRAPH produced unexpected query kind {other:?}"),
2265                    self.position(),
2266                )),
2267            },
2268            Token::Search => match self.parse_search_command()? {
2269                QueryExpr::SearchCommand(command) => Ok(FrontendStatement::Search(command)),
2270                other => Err(ParseError::new(
2271                    format!("internal: SEARCH produced unexpected query kind {other:?}"),
2272                    self.position(),
2273                )),
2274            },
2275            Token::Ident(name) if name.eq_ignore_ascii_case("ASK") => {
2276                match self.parse_ask_query()? {
2277                    QueryExpr::Ask(query) => Ok(FrontendStatement::Ask(query)),
2278                    other => Err(ParseError::new(
2279                        format!("internal: ASK produced unexpected query kind {other:?}"),
2280                        self.position(),
2281                    )),
2282                }
2283            }
2284            Token::Ident(name) if name.eq_ignore_ascii_case("UNSEAL") => {
2285                match self.parse_unseal_vault_command()? {
2286                    QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2287                    other => Err(ParseError::new(
2288                        format!("internal: UNSEAL VAULT produced unexpected query kind {other:?}"),
2289                        self.position(),
2290                    )),
2291                }
2292            }
2293            Token::Queue => match self.parse_queue_command()? {
2294                QueryExpr::QueueCommand(command) => Ok(FrontendStatement::QueueCommand(command)),
2295                other => Err(ParseError::new(
2296                    format!("internal: QUEUE produced unexpected query kind {other:?}"),
2297                    self.position(),
2298                )),
2299            },
2300            Token::Ident(name) if name.eq_ignore_ascii_case("EVENTS") => {
2301                match self.parse_events_command()? {
2302                    QueryExpr::Table(query) => Ok(FrontendStatement::Sql(SqlStatement::Query(
2303                        SqlQuery::Select(query),
2304                    ))),
2305                    QueryExpr::EventsBackfill(query) => {
2306                        Ok(FrontendStatement::EventsBackfill(query))
2307                    }
2308                    QueryExpr::EventsBackfillStatus { collection } => {
2309                        Ok(FrontendStatement::EventsBackfillStatus { collection })
2310                    }
2311                    other => Err(ParseError::new(
2312                        format!("internal: EVENTS produced unexpected query kind {other:?}"),
2313                        self.position(),
2314                    )),
2315                }
2316            }
2317            Token::Kv => match self.parse_kv_command()? {
2318                QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2319                other => Err(ParseError::new(
2320                    format!("internal: KV produced unexpected query kind {other:?}"),
2321                    self.position(),
2322                )),
2323            },
2324            Token::Delete => {
2325                if matches!(
2326                    self.peek_next()?,
2327                    Token::Ident(name) if name.eq_ignore_ascii_case("CONFIG")
2328                ) {
2329                    match self.parse_config_command()? {
2330                        QueryExpr::ConfigCommand(command) => {
2331                            Ok(FrontendStatement::ConfigCommand(command))
2332                        }
2333                        other => Err(ParseError::new(
2334                            format!("internal: CONFIG produced unexpected query kind {other:?}"),
2335                            self.position(),
2336                        )),
2337                    }
2338                } else if matches!(
2339                    self.peek_next()?,
2340                    Token::Ident(name) if name.eq_ignore_ascii_case("VAULT")
2341                ) {
2342                    match self.parse_vault_lifecycle_command()? {
2343                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2344                        other => Err(ParseError::new(
2345                            format!("internal: VAULT produced unexpected query kind {other:?}"),
2346                            self.position(),
2347                        )),
2348                    }
2349                } else {
2350                    self.parse_sql_statement().map(FrontendStatement::Sql)
2351                }
2352            }
2353            Token::Add => match self.parse_config_command()? {
2354                QueryExpr::ConfigCommand(command) => Ok(FrontendStatement::ConfigCommand(command)),
2355                other => Err(ParseError::new(
2356                    format!("internal: CONFIG produced unexpected query kind {other:?}"),
2357                    self.position(),
2358                )),
2359            },
2360            Token::Purge => match self.parse_vault_lifecycle_command()? {
2361                QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2362                other => Err(ParseError::new(
2363                    format!("internal: VAULT produced unexpected query kind {other:?}"),
2364                    self.position(),
2365                )),
2366            },
2367            Token::Ident(name)
2368                if name.eq_ignore_ascii_case("PUT")
2369                    || name.eq_ignore_ascii_case("GET")
2370                    || name.eq_ignore_ascii_case("RESOLVE")
2371                    || name.eq_ignore_ascii_case("ROTATE")
2372                    || name.eq_ignore_ascii_case("HISTORY")
2373                    || name.eq_ignore_ascii_case("PURGE")
2374                    || name.eq_ignore_ascii_case("INCR")
2375                    || name.eq_ignore_ascii_case("DECR")
2376                    || name.eq_ignore_ascii_case("INVALIDATE") =>
2377            {
2378                // `RESOLVE CONFLICT …` is the VCS working-set verb, not config
2379                // resolution — route it to the SQL/VCS command surface.
2380                if name.eq_ignore_ascii_case("RESOLVE")
2381                    && matches!(self.peek_next()?, Token::Ident(next) if next.eq_ignore_ascii_case("CONFLICT"))
2382                {
2383                    self.parse_sql_command()
2384                        .map(SqlCommand::into_statement)
2385                        .map(FrontendStatement::Sql)
2386                } else if matches!(
2387                    self.peek_next()?,
2388                    Token::Ident(next) if next.eq_ignore_ascii_case("VAULT")
2389                ) {
2390                    match self.parse_vault_lifecycle_command()? {
2391                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2392                        other => Err(ParseError::new(
2393                            format!("internal: VAULT produced unexpected query kind {other:?}"),
2394                            self.position(),
2395                        )),
2396                    }
2397                } else {
2398                    match self.parse_config_command()? {
2399                        QueryExpr::ConfigCommand(command) => {
2400                            Ok(FrontendStatement::ConfigCommand(command))
2401                        }
2402                        other => Err(ParseError::new(
2403                            format!("internal: CONFIG produced unexpected query kind {other:?}"),
2404                            self.position(),
2405                        )),
2406                    }
2407                }
2408            }
2409            Token::Ident(name) if name.eq_ignore_ascii_case("VAULT") => {
2410                match self.parse_vault_command()? {
2411                    QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2412                    other => Err(ParseError::new(
2413                        format!("internal: VAULT produced unexpected query kind {other:?}"),
2414                        self.position(),
2415                    )),
2416                }
2417            }
2418            Token::Tree => match self.parse_tree_command()? {
2419                QueryExpr::TreeCommand(command) => Ok(FrontendStatement::TreeCommand(command)),
2420                other => Err(ParseError::new(
2421                    format!("internal: TREE produced unexpected query kind {other:?}"),
2422                    self.position(),
2423                )),
2424            },
2425            Token::Ident(name) if name.eq_ignore_ascii_case("HLL") => {
2426                match self.parse_hll_command()? {
2427                    QueryExpr::ProbabilisticCommand(command) => {
2428                        Ok(FrontendStatement::ProbabilisticCommand(command))
2429                    }
2430                    other => Err(ParseError::new(
2431                        format!("internal: HLL produced unexpected query kind {other:?}"),
2432                        self.position(),
2433                    )),
2434                }
2435            }
2436            Token::Ident(name) if name.eq_ignore_ascii_case("SKETCH") => {
2437                match self.parse_sketch_command()? {
2438                    QueryExpr::ProbabilisticCommand(command) => {
2439                        Ok(FrontendStatement::ProbabilisticCommand(command))
2440                    }
2441                    other => Err(ParseError::new(
2442                        format!("internal: SKETCH produced unexpected query kind {other:?}"),
2443                        self.position(),
2444                    )),
2445                }
2446            }
2447            Token::Ident(name) if name.eq_ignore_ascii_case("FILTER") => {
2448                match self.parse_filter_command()? {
2449                    QueryExpr::ProbabilisticCommand(command) => {
2450                        Ok(FrontendStatement::ProbabilisticCommand(command))
2451                    }
2452                    other => Err(ParseError::new(
2453                        format!("internal: FILTER produced unexpected query kind {other:?}"),
2454                        self.position(),
2455                    )),
2456                }
2457            }
2458            Token::Ident(name) if name.eq_ignore_ascii_case("EVENTS") => self
2459                .parse_sql_command()
2460                .map(SqlCommand::into_statement)
2461                .map(FrontendStatement::Sql),
2462            Token::Ident(name)
2463                if is_vcs_command_head(name) || name.eq_ignore_ascii_case("RESET") =>
2464            {
2465                self.parse_sql_command()
2466                    .map(SqlCommand::into_statement)
2467                    .map(FrontendStatement::Sql)
2468            }
2469            other => Err(ParseError::expected(
2470                vec![
2471                    "SELECT",
2472                    "MATCH",
2473                    "PATH",
2474                    "FROM",
2475                    "VECTOR",
2476                    "HYBRID",
2477                    "INSERT",
2478                    "UPDATE",
2479                    "DELETE",
2480                    "TRUNCATE",
2481                    "CREATE",
2482                    "DROP",
2483                    "ALTER",
2484                    "GRAPH",
2485                    "SEARCH",
2486                    "ASK",
2487                    "QUEUE",
2488                    "EVENTS",
2489                    "KV",
2490                    "HLL",
2491                    "TREE",
2492                    "SKETCH",
2493                    "FILTER",
2494                    "SET",
2495                    "SHOW",
2496                    "RESET",
2497                    "CHECKPOINT",
2498                    "CHECKOUT",
2499                    "MERGE",
2500                    "CHERRY",
2501                    "REVERT",
2502                    "RESOLVE",
2503                    "DESCRIBE",
2504                    "DESC",
2505                    "RANK",
2506                    "ZRANK",
2507                    "ZRANGE",
2508                ],
2509                other,
2510                self.position(),
2511            )),
2512        }
2513    }
2514
2515    fn parse_ranking_read(&mut self) -> Result<QueryExpr, ParseError> {
2516        let head = self.expect_ident()?;
2517        if head.eq_ignore_ascii_case("RANK") {
2518            return self.parse_rank_after_rank(false);
2519        }
2520        if head.eq_ignore_ascii_case("APPROX") || head.eq_ignore_ascii_case("APPROXIMATE") {
2521            if !self.consume_ident_ci("RANK")? {
2522                return Err(ParseError::expected(
2523                    vec!["RANK"],
2524                    self.peek(),
2525                    self.position(),
2526                ));
2527            }
2528            return self.parse_rank_after_rank(true);
2529        }
2530        if head.eq_ignore_ascii_case("ZRANK") {
2531            return self.parse_zrank();
2532        }
2533        if head.eq_ignore_ascii_case("ZRANGE") {
2534            return self.parse_zrange();
2535        }
2536        Err(ParseError::expected(
2537            vec!["RANK", "APPROX RANK", "ZRANK", "ZRANGE"],
2538            self.peek(),
2539            self.position(),
2540        ))
2541    }
2542
2543    fn parse_rank_after_rank(&mut self, approximate: bool) -> Result<QueryExpr, ParseError> {
2544        if self.consume(&Token::Of)? {
2545            let entity_id = self.parse_u64_slot("rank entity id")?;
2546            self.expect(Token::In)?;
2547            let ranking = self.expect_ident()?;
2548            let query = RankOfQuery { ranking, entity_id };
2549            return Ok(if approximate {
2550                QueryExpr::ApproxRankOf(query)
2551            } else {
2552                QueryExpr::RankOf(query)
2553            });
2554        }
2555
2556        if !approximate && self.consume(&Token::Range)? {
2557            let lo = self.parse_positive_u64_slot("rank range lower bound")?;
2558            self.expect(Token::To)?;
2559            let hi = self.parse_positive_u64_slot("rank range upper bound")?;
2560            if hi < lo {
2561                return Err(ParseError::value_out_of_range(
2562                    "rank range upper bound",
2563                    "must be greater than or equal to the lower bound",
2564                    self.position(),
2565                ));
2566            }
2567            self.expect(Token::In)?;
2568            let ranking = self.expect_ident()?;
2569            return Ok(QueryExpr::RankRange(RankRangeQuery { ranking, lo, hi }));
2570        }
2571
2572        Err(ParseError::expected(
2573            if approximate {
2574                vec!["OF"]
2575            } else {
2576                vec!["OF", "RANGE"]
2577            },
2578            self.peek(),
2579            self.position(),
2580        ))
2581    }
2582
2583    fn parse_zrank(&mut self) -> Result<QueryExpr, ParseError> {
2584        let ranking = self.expect_ident()?;
2585        let entity_id = self.parse_u64_slot("ZRANK entity id")?;
2586        Ok(QueryExpr::RankOf(RankOfQuery { ranking, entity_id }))
2587    }
2588
2589    fn parse_zrange(&mut self) -> Result<QueryExpr, ParseError> {
2590        let ranking = self.expect_ident()?;
2591        let start = self.parse_u64_slot("ZRANGE start")?;
2592        let stop = self.parse_u64_slot("ZRANGE stop")?;
2593        if stop < start {
2594            return Err(ParseError::value_out_of_range(
2595                "ZRANGE stop",
2596                "must be greater than or equal to start",
2597                self.position(),
2598            ));
2599        }
2600        let _with_scores = self.consume_ident_ci("WITHSCORES")?;
2601        Ok(QueryExpr::RankRange(RankRangeQuery {
2602            ranking,
2603            lo: start + 1,
2604            hi: stop + 1,
2605        }))
2606    }
2607
2608    fn parse_positive_u64_slot(&mut self, field: &'static str) -> Result<u64, ParseError> {
2609        let value = self.parse_u64_slot(field)?;
2610        if value == 0 {
2611            return Err(ParseError::value_out_of_range(
2612                field,
2613                "must be a positive integer",
2614                self.position(),
2615            ));
2616        }
2617        Ok(value)
2618    }
2619
2620    fn parse_u64_slot(&mut self, field: &'static str) -> Result<u64, ParseError> {
2621        let pos = self.position();
2622        if matches!(self.peek(), Token::Minus | Token::Dash) {
2623            return Err(ParseError::value_out_of_range(
2624                field,
2625                "must be an unsigned integer",
2626                pos,
2627            ));
2628        }
2629        let raw = self.parse_integer()?;
2630        u64::try_from(raw)
2631            .map_err(|_| ParseError::value_out_of_range(field, "must be an unsigned integer", pos))
2632    }
2633
2634    #[inline(never)]
2635    fn parse_vcs_atom(&mut self, field: &'static str) -> Result<String, ParseError> {
2636        match self.peek().clone() {
2637            Token::Ident(value) | Token::String(value) => {
2638                self.advance()?;
2639                Ok(value)
2640            }
2641            Token::Integer(value) => {
2642                self.advance()?;
2643                Ok(value.to_string())
2644            }
2645            other => Err(ParseError::expected(vec![field], &other, self.position())),
2646        }
2647    }
2648
2649    #[inline(never)]
2650    fn parse_vcs_command(&mut self) -> Result<VcsCommand, ParseError> {
2651        let head = self.expect_ident()?;
2652        if head.eq_ignore_ascii_case("CHECKPOINT") {
2653            let message = self.parse_string()?;
2654            let author = if self.consume_ident_ci("AUTHOR")? {
2655                Some(self.parse_string()?)
2656            } else {
2657                None
2658            };
2659            return Ok(VcsCommand::Checkpoint { message, author });
2660        }
2661        if head.eq_ignore_ascii_case("CHECKOUT") {
2662            return Ok(VcsCommand::Checkout {
2663                target: self.parse_vcs_atom("commitish")?,
2664            });
2665        }
2666        if head.eq_ignore_ascii_case("RESET") {
2667            let mode = if self.consume_ident_ci("HARD")? {
2668                VcsResetMode::Hard
2669            } else if self.consume_ident_ci("SOFT")? {
2670                VcsResetMode::Soft
2671            } else {
2672                // MIXED is the default; consume the optional explicit keyword.
2673                let _ = self.consume_ident_ci("MIXED")?;
2674                VcsResetMode::Mixed
2675            };
2676            self.expect(Token::To)?;
2677            return Ok(VcsCommand::Reset {
2678                mode,
2679                target: self.parse_vcs_atom("commitish")?,
2680            });
2681        }
2682        if head.eq_ignore_ascii_case("MERGE") {
2683            return Ok(VcsCommand::Merge {
2684                branch: self.parse_vcs_atom("branch")?,
2685            });
2686        }
2687        if head.eq_ignore_ascii_case("CHERRY") {
2688            if !self.consume_ident_ci("PICK")? {
2689                return Err(ParseError::expected(
2690                    vec!["PICK"],
2691                    self.peek(),
2692                    self.position(),
2693                ));
2694            }
2695            return Ok(VcsCommand::CherryPick {
2696                commit: self.parse_vcs_atom("commit")?,
2697            });
2698        }
2699        if head.eq_ignore_ascii_case("REVERT") {
2700            return Ok(VcsCommand::Revert {
2701                commit: self.parse_vcs_atom("commit")?,
2702            });
2703        }
2704        if head.eq_ignore_ascii_case("RESOLVE") {
2705            if !self.consume_ident_ci("CONFLICT")? {
2706                return Err(ParseError::expected(
2707                    vec!["CONFLICT"],
2708                    self.peek(),
2709                    self.position(),
2710                ));
2711            }
2712            let key = self.parse_string()?;
2713            if !self.consume(&Token::Using)? {
2714                return Err(ParseError::expected(
2715                    vec!["USING"],
2716                    self.peek(),
2717                    self.position(),
2718                ));
2719            }
2720            let resolution = if self.consume_ident_ci("OURS")? {
2721                VcsConflictResolution::Ours
2722            } else if self.consume_ident_ci("THEIRS")? {
2723                VcsConflictResolution::Theirs
2724            } else {
2725                return Err(ParseError::expected(
2726                    vec!["OURS", "THEIRS"],
2727                    self.peek(),
2728                    self.position(),
2729                ));
2730            };
2731            return Ok(VcsCommand::ResolveConflict { key, resolution });
2732        }
2733        Err(ParseError::expected(
2734            vec![
2735                "CHECKPOINT",
2736                "CHECKOUT",
2737                "RESET",
2738                "MERGE",
2739                "CHERRY",
2740                "REVERT",
2741                "RESOLVE",
2742            ],
2743            self.peek(),
2744            self.position(),
2745        ))
2746    }
2747
2748    /// Parse any SQL/RQL-style command into the canonical SQL frontend IR.
2749    pub fn parse_sql_statement(&mut self) -> Result<SqlStatement, ParseError> {
2750        self.parse_sql_command().map(SqlCommand::into_statement)
2751    }
2752
2753    fn parse_dotted_admin_path(&mut self, lowercase: bool) -> Result<String, ParseError> {
2754        let mut path = self.expect_ident()?;
2755        while self.consume(&Token::Dot)? {
2756            let next = self.expect_ident_or_keyword()?;
2757            path = format!("{path}.{next}");
2758        }
2759        Ok(if lowercase {
2760            path.to_ascii_lowercase()
2761        } else {
2762            path
2763        })
2764    }
2765
2766    fn normalize_secret_admin_path(path: String) -> String {
2767        if let Some(rest) = path.strip_prefix("red.secrets.") {
2768            format!("red.secret.{rest}")
2769        } else if path == "red.secrets" {
2770            "red.secret".to_string()
2771        } else {
2772            path
2773        }
2774    }
2775
2776    /// Parse any SQL/RQL-style command through a single frontend module.
2777    /// Parse a `CREATE ...` statement. Split out of
2778    /// [`parse_sql_command`] so its very large per-arm locals
2779    /// (every CREATE variant's query struct) live in their own
2780    /// stack frame instead of inflating the dispatcher's frame.
2781    /// `parse_sql_command` recurses (CREATE VIEW ... AS <stmt>,
2782    /// nested subqueries), so a fat dispatcher frame stacked on
2783    /// itself overflowed small (2 MiB) worker-thread stacks (#635).
2784    #[inline(never)]
2785    fn parse_create_command(&mut self) -> Result<SqlCommand, ParseError> {
2786        let pos = self.position();
2787        self.advance()?;
2788
2789        // CREATE [OR REPLACE] [MATERIALIZED] VIEW [IF NOT EXISTS] name AS <select>
2790        // Detect the VIEW path early so OR REPLACE / MATERIALIZED modifiers
2791        // don't collide with other CREATE variants (TABLE, INDEX, etc.).
2792        let mut or_replace = false;
2793        if self.consume(&Token::Or)? || self.consume_ident_ci("OR")? {
2794            let _ = self.consume_ident_ci("REPLACE")?;
2795            or_replace = true;
2796        }
2797        let materialized = self.consume(&Token::Materialized)?;
2798        if self.check(&Token::View) {
2799            self.advance()?;
2800            let if_not_exists = self.match_if_not_exists()?;
2801            let name = self.expect_ident()?;
2802            // Issue #584 slice 12 — `WITH RETENTION <duration>`
2803            // on CREATE MATERIALIZED VIEW. Parsed before `AS`
2804            // so the SELECT body parser cannot consume the
2805            // trailing `WITH` for its own (TTL / METADATA /
2806            // …) clauses. Persisted on the view definition;
2807            // the physical sweep against view-backing rows
2808            // activates with the slice-9 row-storage follow-up.
2809            let mut retention_duration_ms: Option<u64> = None;
2810            if self.check(&Token::With) {
2811                self.advance()?;
2812                if !self.consume(&Token::Retention)? && !self.consume_ident_ci("RETENTION")? {
2813                    return Err(ParseError::expected(
2814                        vec!["RETENTION"],
2815                        self.peek(),
2816                        self.position(),
2817                    ));
2818                }
2819                if !materialized {
2820                    return Err(ParseError::new(
2821                        "WITH RETENTION is only valid on \
2822                                 CREATE MATERIALIZED VIEW"
2823                            .to_string(),
2824                        self.position(),
2825                    ));
2826                }
2827                let value = self.parse_float()?;
2828                let unit_mult = self.parse_duration_unit()?;
2829                retention_duration_ms = Some((value * unit_mult).round() as u64);
2830            }
2831            // Accept `AS` — the lexer promotes it to `Token::As`
2832            // (keyword) but some paths still see it as an ident.
2833            if !self.consume(&Token::As)? && !self.consume_ident_ci("AS")? {
2834                return Err(ParseError::expected(
2835                    vec!["AS"],
2836                    self.peek(),
2837                    self.position(),
2838                ));
2839            }
2840            // Recursive parse of the body. Any QueryExpr that the
2841            // rest of the grammar accepts is valid (Select, Join, etc.).
2842            let body = self.parse_sql_command()?.into_query_expr();
2843            // Optional `REFRESH EVERY <duration>` clause on
2844            // materialized views (issue #583 slice 10). The
2845            // background scheduler reads this off the view
2846            // descriptor and ticks the view on its cadence.
2847            let mut refresh_every_ms: Option<u64> = None;
2848            if self.check(&Token::Refresh) {
2849                if !materialized {
2850                    return Err(ParseError::new(
2851                        "REFRESH EVERY is only valid on \
2852                                 CREATE MATERIALIZED VIEW"
2853                            .to_string(),
2854                        self.position(),
2855                    ));
2856                }
2857                self.advance()?;
2858                if !self.consume_ident_ci("EVERY")? {
2859                    return Err(ParseError::expected(
2860                        vec!["EVERY"],
2861                        self.peek(),
2862                        self.position(),
2863                    ));
2864                }
2865                let value = self.parse_float()?;
2866                let unit_mult = self.parse_duration_unit()?;
2867                refresh_every_ms = Some((value * unit_mult).round() as u64);
2868            }
2869            return Ok(SqlCommand::CreateView(CreateViewQuery {
2870                name,
2871                query: Box::new(body),
2872                materialized,
2873                if_not_exists,
2874                or_replace,
2875                refresh_every_ms,
2876                retention_duration_ms,
2877            }));
2878        }
2879        // If OR REPLACE / MATERIALIZED was consumed but VIEW was not,
2880        // bail out — no other CREATE form accepts those modifiers.
2881        if or_replace || materialized {
2882            return Err(ParseError::expected(
2883                vec!["VIEW"],
2884                self.peek(),
2885                self.position(),
2886            ));
2887        }
2888
2889        if matches!(self.peek(), Token::Ident(name) if name.eq_ignore_ascii_case("USER")) {
2890            let stmt = self.parse_create_user_statement()?;
2891            Ok(SqlCommand::CreateUser(stmt))
2892        } else if self.consume_ident_ci("BRANCH")? {
2893            self.parse_create_vcs_ref(VcsRefKind::Branch)
2894        } else if self.consume_ident_ci("TAG")? {
2895            self.parse_create_vcs_ref(VcsRefKind::Tag)
2896        } else if self.check(&Token::Index) || self.check(&Token::Unique) {
2897            match self.parse_create_index_query()? {
2898                QueryExpr::CreateIndex(query) => Ok(SqlCommand::CreateIndex(query)),
2899                other => Err(ParseError::new(
2900                    format!("internal: CREATE INDEX produced unexpected kind {other:?}"),
2901                    self.position(),
2902                )),
2903            }
2904        } else if self.check(&Token::Table) {
2905            self.expect(Token::Table)?;
2906            match self.parse_create_table_body()? {
2907                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2908                other => Err(ParseError::new(
2909                    format!("internal: CREATE TABLE produced unexpected kind {other:?}"),
2910                    self.position(),
2911                )),
2912            }
2913        } else if self.check(&Token::Graph) {
2914            self.advance()?;
2915            match self.parse_create_collection_model_body(CollectionModel::Graph)? {
2916                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2917                other => Err(ParseError::new(
2918                    format!("internal: CREATE GRAPH produced unexpected kind {other:?}"),
2919                    self.position(),
2920                )),
2921            }
2922        } else if self.check(&Token::Document) {
2923            self.advance()?;
2924            match self.parse_create_collection_model_body(CollectionModel::Document)? {
2925                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2926                other => Err(ParseError::new(
2927                    format!("internal: CREATE DOCUMENT produced unexpected kind {other:?}"),
2928                    self.position(),
2929                )),
2930            }
2931        } else if self.check(&Token::Vector) {
2932            self.advance()?;
2933            match self.parse_create_vector_body()? {
2934                QueryExpr::CreateVector(query) => Ok(SqlCommand::CreateVector(query)),
2935                other => Err(ParseError::new(
2936                    format!("internal: CREATE VECTOR produced unexpected kind {other:?}"),
2937                    self.position(),
2938                )),
2939            }
2940        } else if self.check(&Token::Collection) {
2941            self.advance()?;
2942            match self.parse_create_collection_body()? {
2943                QueryExpr::CreateCollection(query) => Ok(SqlCommand::CreateCollection(query)),
2944                other => Err(ParseError::new(
2945                    format!("internal: CREATE COLLECTION produced unexpected kind {other:?}"),
2946                    self.position(),
2947                )),
2948            }
2949        } else if self.check(&Token::Kv) {
2950            self.advance()?;
2951            match self.parse_create_keyed_body(CollectionModel::Kv)? {
2952                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2953                other => Err(ParseError::new(
2954                    format!("internal: CREATE KV produced unexpected kind {other:?}"),
2955                    self.position(),
2956                )),
2957            }
2958        } else if self.consume_ident_ci("CONFIG")? {
2959            match self.parse_create_keyed_body(CollectionModel::Config)? {
2960                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2961                other => Err(ParseError::new(
2962                    format!("internal: CREATE CONFIG produced unexpected kind {other:?}"),
2963                    self.position(),
2964                )),
2965            }
2966        } else if self.consume_ident_ci("VAULT")? {
2967            match self.parse_create_keyed_body(CollectionModel::Vault)? {
2968                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2969                other => Err(ParseError::new(
2970                    format!("internal: CREATE VAULT produced unexpected kind {other:?}"),
2971                    self.position(),
2972                )),
2973            }
2974        } else if self.check(&Token::Timeseries) {
2975            self.advance()?;
2976            match self.parse_create_timeseries_body()? {
2977                QueryExpr::CreateTimeSeries(query) => Ok(SqlCommand::CreateTimeSeries(query)),
2978                other => Err(ParseError::new(
2979                    format!("internal: CREATE TIMESERIES produced unexpected kind {other:?}"),
2980                    self.position(),
2981                )),
2982            }
2983        } else if self.check(&Token::Metric) {
2984            self.advance()?;
2985            match self.parse_create_metric_body()? {
2986                QueryExpr::CreateMetric(query) => Ok(SqlCommand::CreateMetric(query)),
2987                other => Err(ParseError::new(
2988                    format!("internal: CREATE METRIC produced unexpected kind {other:?}"),
2989                    self.position(),
2990                )),
2991            }
2992        } else if self.consume_ident_ci("METRICS")? {
2993            match self.parse_create_metrics_body()? {
2994                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2995                other => Err(ParseError::new(
2996                    format!("internal: CREATE METRICS produced unexpected kind {other:?}"),
2997                    self.position(),
2998                )),
2999            }
3000        } else if self.consume_ident_ci("SLO")? {
3001            match self.parse_create_slo_body()? {
3002                QueryExpr::CreateSlo(query) => Ok(SqlCommand::CreateSlo(query)),
3003                other => Err(ParseError::new(
3004                    format!("internal: CREATE SLO produced unexpected kind {other:?}"),
3005                    self.position(),
3006                )),
3007            }
3008        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("HYPERTABLE")) {
3009            self.advance()?;
3010            match self.parse_create_hypertable_body()? {
3011                QueryExpr::CreateTimeSeries(query) => Ok(SqlCommand::CreateTimeSeries(query)),
3012                other => Err(ParseError::new(
3013                    format!("internal: CREATE HYPERTABLE produced unexpected kind {other:?}"),
3014                    self.position(),
3015                )),
3016            }
3017        } else if self.check(&Token::Queue) {
3018            self.advance()?;
3019            match self.parse_create_queue_body()? {
3020                QueryExpr::CreateQueue(query) => Ok(SqlCommand::CreateQueue(query)),
3021                other => Err(ParseError::new(
3022                    format!("internal: CREATE QUEUE produced unexpected kind {other:?}"),
3023                    self.position(),
3024                )),
3025            }
3026        } else if self.check(&Token::Tree) {
3027            self.advance()?;
3028            match self.parse_create_tree_body()? {
3029                QueryExpr::CreateTree(query) => Ok(SqlCommand::CreateTree(query)),
3030                other => Err(ParseError::new(
3031                    format!("internal: CREATE TREE produced unexpected kind {other:?}"),
3032                    self.position(),
3033                )),
3034            }
3035        } else if matches!(self.peek(), Token::Ident(n) if
3036                    n.eq_ignore_ascii_case("HLL") ||
3037                    n.eq_ignore_ascii_case("SKETCH") ||
3038                    n.eq_ignore_ascii_case("FILTER"))
3039        {
3040            match self.parse_create_probabilistic()? {
3041                QueryExpr::ProbabilisticCommand(command) => Ok(SqlCommand::Probabilistic(command)),
3042                other => Err(ParseError::new(
3043                    format!("internal: CREATE probabilistic produced unexpected kind {other:?}"),
3044                    self.position(),
3045                )),
3046            }
3047        } else if self.check(&Token::Schema) {
3048            // CREATE SCHEMA [IF NOT EXISTS] name
3049            self.advance()?;
3050            let if_not_exists = self.match_if_not_exists()?;
3051            let name = self.expect_ident()?;
3052            Ok(SqlCommand::CreateSchema(CreateSchemaQuery {
3053                name,
3054                if_not_exists,
3055            }))
3056        } else if self.check(&Token::Policy) {
3057            // Two forms share the leading `CREATE POLICY` tokens:
3058            //   * IAM:   CREATE POLICY '<id>' AS '<json>'          (string literal id)
3059            //   * RLS:   CREATE POLICY <name> ON <target> ...      (bare ident name)
3060            // Disambiguate by peeking the token after POLICY.
3061            self.advance()?;
3062            if matches!(self.peek(), Token::String(_)) {
3063                // IAM form — short-circuit out of the SQL command stack.
3064                let expr = self.parse_create_iam_policy_after_keywords()?;
3065                // Inline command-wrapping: produce a synthetic SqlCommand by
3066                // routing through a generic IAM admin holder. We don't
3067                // have a dedicated SqlCommand variant for IAM yet, so we
3068                // bounce through the existing Grant-shaped Admin slot
3069                // which expects no further tokens.
3070                return Ok(SqlCommand::IamPolicy(expr));
3071            }
3072            let name = self.expect_ident()?;
3073            self.expect(Token::On)?;
3074
3075            let (target_kind, table) = {
3076                use crate::ast::PolicyTargetKind;
3077                let kw = match self.peek() {
3078                    Token::Ident(s) => Some(s.to_ascii_uppercase()),
3079                    _ => None,
3080                };
3081                let kind = kw.as_deref().and_then(|k| match k {
3082                    "NODES" => Some(PolicyTargetKind::Nodes),
3083                    "EDGES" => Some(PolicyTargetKind::Edges),
3084                    "VECTORS" => Some(PolicyTargetKind::Vectors),
3085                    "MESSAGES" => Some(PolicyTargetKind::Messages),
3086                    "POINTS" => Some(PolicyTargetKind::Points),
3087                    "DOCUMENTS" => Some(PolicyTargetKind::Documents),
3088                    _ => None,
3089                });
3090                if let Some(k) = kind {
3091                    self.advance()?;
3092                    self.expect(Token::Of)?;
3093                    let coll = self.expect_ident()?;
3094                    (k, coll)
3095                } else {
3096                    let coll = self.expect_ident()?;
3097                    (PolicyTargetKind::Table, coll)
3098                }
3099            };
3100
3101            let action = if self.consume(&Token::For)? {
3102                let a = match self.peek() {
3103                    Token::Select => {
3104                        self.advance()?;
3105                        Some(PolicyAction::Select)
3106                    }
3107                    Token::Insert => {
3108                        self.advance()?;
3109                        Some(PolicyAction::Insert)
3110                    }
3111                    Token::Update => {
3112                        self.advance()?;
3113                        Some(PolicyAction::Update)
3114                    }
3115                    Token::Delete => {
3116                        self.advance()?;
3117                        Some(PolicyAction::Delete)
3118                    }
3119                    Token::All => {
3120                        self.advance()?;
3121                        None
3122                    }
3123                    _ => None,
3124                };
3125                a
3126            } else {
3127                None
3128            };
3129
3130            let role = if self.consume(&Token::To)? {
3131                Some(self.expect_ident()?)
3132            } else {
3133                None
3134            };
3135
3136            self.expect(Token::Using)?;
3137            self.expect(Token::LParen)?;
3138            let filter = self.parse_filter()?;
3139            self.expect(Token::RParen)?;
3140
3141            Ok(SqlCommand::CreatePolicy(CreatePolicyQuery {
3142                name,
3143                table,
3144                action,
3145                role,
3146                using: Box::new(filter),
3147                target_kind,
3148            }))
3149        } else if self.check(&Token::Server) {
3150            // CREATE SERVER [IF NOT EXISTS] name
3151            //   FOREIGN DATA WRAPPER kind
3152            //   [OPTIONS (key 'value', ...)]
3153            self.advance()?;
3154            let if_not_exists = self.match_if_not_exists()?;
3155            let name = self.expect_ident()?;
3156            self.expect(Token::Foreign)?;
3157            self.expect(Token::Data)?;
3158            self.expect(Token::Wrapper)?;
3159            let wrapper = self.expect_ident()?;
3160            let options = self.parse_fdw_options_clause()?;
3161            Ok(SqlCommand::CreateServer(CreateServerQuery {
3162                name,
3163                wrapper,
3164                options,
3165                if_not_exists,
3166            }))
3167        } else if self.check(&Token::Foreign) {
3168            // CREATE FOREIGN TABLE [IF NOT EXISTS] name (cols)
3169            //   SERVER server_name
3170            //   [OPTIONS (key 'value', ...)]
3171            self.advance()?;
3172            self.expect(Token::Table)?;
3173            let if_not_exists = self.match_if_not_exists()?;
3174            let name = self.expect_ident()?;
3175            self.expect(Token::LParen)?;
3176            let mut columns = Vec::new();
3177            loop {
3178                let col_name = self.expect_ident()?;
3179                let data_type = self.expect_ident_or_keyword()?;
3180                // Inline NOT NULL check — the CREATE TABLE path's helper is
3181                // private and coupling to it just for FDW columns isn't worth it.
3182                let mut not_null = false;
3183                if matches!(self.peek(), Token::Ident(n) if n.eq_ignore_ascii_case("NOT")) {
3184                    self.advance()?;
3185                    if matches!(self.peek(), Token::Ident(n) if n.eq_ignore_ascii_case("NULL")) {
3186                        self.advance()?;
3187                        not_null = true;
3188                    }
3189                }
3190                columns.push(ForeignColumnDef {
3191                    name: col_name,
3192                    data_type,
3193                    not_null,
3194                });
3195                if !self.consume(&Token::Comma)? {
3196                    break;
3197                }
3198            }
3199            self.expect(Token::RParen)?;
3200            self.expect(Token::Server)?;
3201            let server = self.expect_ident()?;
3202            let options = self.parse_fdw_options_clause()?;
3203            Ok(SqlCommand::CreateForeignTable(CreateForeignTableQuery {
3204                name,
3205                server,
3206                columns,
3207                options,
3208                if_not_exists,
3209            }))
3210        } else if self.check(&Token::Sequence) {
3211            // CREATE SEQUENCE [IF NOT EXISTS] name
3212            //   [START [WITH] n] [INCREMENT [BY] n]
3213            self.advance()?;
3214            let if_not_exists = self.match_if_not_exists()?;
3215            let name = self.expect_ident()?;
3216            let mut start: i64 = 1;
3217            let mut increment: i64 = 1;
3218            // Loop over optional clauses in any order.
3219            loop {
3220                if self.consume(&Token::Start)? {
3221                    // Accept `START 100` or `START WITH 100`.
3222                    let _ = self.consume(&Token::With)? || self.consume_ident_ci("WITH")?;
3223                    start = self.parse_integer()?;
3224                } else if self.consume(&Token::Increment)? {
3225                    // Accept `INCREMENT 5` or `INCREMENT BY 5`.
3226                    let _ = self.consume(&Token::By)? || self.consume_ident_ci("BY")?;
3227                    increment = self.parse_integer()?;
3228                } else {
3229                    break;
3230                }
3231            }
3232            Ok(SqlCommand::CreateSequence(CreateSequenceQuery {
3233                name,
3234                if_not_exists,
3235                start,
3236                increment,
3237            }))
3238        } else if matches!(self.peek(), Token::Ident(n) if n.eq_ignore_ascii_case("MIGRATION")) {
3239            self.advance()?; // consume MIGRATION
3240            match self.parse_create_migration_body()? {
3241                QueryExpr::CreateMigration(q) => Ok(SqlCommand::CreateMigration(q)),
3242                other => Err(ParseError::new(
3243                    format!("internal: CREATE MIGRATION produced unexpected kind {other:?}"),
3244                    self.position(),
3245                )),
3246            }
3247        } else if let Some(reason) = analytics_v0_non_goal_create(self.peek()) {
3248            // Issue #789 — enforce Analytics v0 non-goals at the parser
3249            // surface. The parent PRD (#782) explicitly excludes generic
3250            // analytics objects, a new event storage model, cohorts,
3251            // funnels, SLA contracts, and adapters from v0. Reject these
3252            // CREATE forms here with a stable, non-goal-specific message
3253            // so accidental use surfaces an obvious "out of scope for v0"
3254            // error rather than the generic CREATE fallback.
3255            Err(ParseError::new(reason, self.position()))
3256        } else if let Some(err) =
3257            ParseError::unsupported_recognized_token(self.peek(), self.position())
3258        {
3259            Err(err)
3260        } else {
3261            Err(ParseError::expected(
3262                vec![
3263                    "TABLE",
3264                    "GRAPH",
3265                    "VECTOR",
3266                    "DOCUMENT",
3267                    "KV",
3268                    "COLLECTION",
3269                    "INDEX",
3270                    "UNIQUE",
3271                    "METRIC",
3272                    "TIMESERIES",
3273                    "QUEUE",
3274                    "TREE",
3275                    "HLL",
3276                    "SKETCH",
3277                    "FILTER",
3278                    "SCHEMA",
3279                    "SEQUENCE",
3280                    "USER",
3281                    "BRANCH",
3282                    "TAG",
3283                    "MIGRATION",
3284                ],
3285                self.peek(),
3286                pos,
3287            ))
3288        }
3289    }
3290
3291    pub fn parse_sql_command(&mut self) -> Result<SqlCommand, ParseError> {
3292        self.enter_depth()?;
3293        let result = self.parse_sql_command_inner();
3294        self.exit_depth();
3295        result
3296    }
3297
3298    /// Desugar a `SHOW BRANCHES` / `SHOW TAGS` statement into a scan of the
3299    /// matching VCS virtual table. Kept out of `parse_sql_command_inner` and
3300    /// marked `#[inline(never)]` so its `TableQuery` locals do not inflate
3301    /// that already-huge match frame, which is paid twice on the nested
3302    /// `CREATE MATERIALIZED VIEW ... AS SELECT` parse path (issue #1569).
3303    #[inline(never)]
3304    fn parse_show_vcs_ref_table(&mut self, table: &str) -> Result<SqlCommand, ParseError> {
3305        let mut query = TableQuery::new(table);
3306        self.parse_table_clauses(&mut query)?;
3307        Ok(SqlCommand::Select(query))
3308    }
3309
3310    /// Parse the body of a `CREATE BRANCH` / `CREATE TAG` statement.
3311    /// Kept out of `parse_create_command`/`parse_sql_command_inner` and marked
3312    /// `#[inline(never)]` so its `QueryExpr`/`SqlCommand` locals do not inflate
3313    /// those already-huge match frames, which are paid twice on the nested
3314    /// `CREATE MATERIALIZED VIEW ... AS SELECT` parse path (issue #1570).
3315    #[inline(never)]
3316    fn parse_create_vcs_ref(&mut self, kind: VcsRefKind) -> Result<SqlCommand, ParseError> {
3317        match self.parse_create_vcs_ref_body(kind)? {
3318            QueryExpr::CreateVcsRef(query) => Ok(SqlCommand::CreateVcsRef(query)),
3319            other => {
3320                let noun = match kind {
3321                    VcsRefKind::Branch => "BRANCH",
3322                    VcsRefKind::Tag => "TAG",
3323                };
3324                Err(ParseError::new(
3325                    format!("internal: CREATE {noun} produced unexpected kind {other:?}"),
3326                    self.position(),
3327                ))
3328            }
3329        }
3330    }
3331
3332    /// Parse the body of a `DROP BRANCH` / `DROP TAG` statement.
3333    /// Kept out of `parse_sql_command_inner` and marked `#[inline(never)]` for
3334    /// the same stack-frame reason as [`Self::parse_create_vcs_ref`] (#1570).
3335    #[inline(never)]
3336    fn parse_drop_vcs_ref(&mut self, kind: VcsRefKind) -> Result<SqlCommand, ParseError> {
3337        match self.parse_drop_vcs_ref_body(kind)? {
3338            QueryExpr::DropVcsRef(query) => Ok(SqlCommand::DropVcsRef(query)),
3339            other => {
3340                let noun = match kind {
3341                    VcsRefKind::Branch => "BRANCH",
3342                    VcsRefKind::Tag => "TAG",
3343                };
3344                Err(ParseError::new(
3345                    format!("internal: DROP {noun} produced unexpected kind {other:?}"),
3346                    self.position(),
3347                ))
3348            }
3349        }
3350    }
3351
3352    fn parse_sql_command_inner(&mut self) -> Result<SqlCommand, ParseError> {
3353        match self.peek() {
3354            Token::Select => match self.parse_select_query()? {
3355                QueryExpr::Table(query) => Ok(SqlCommand::Select(query)),
3356                QueryExpr::Join(query) => Ok(SqlCommand::Join(query)),
3357                other => Err(ParseError::new(
3358                    format!("internal: SELECT produced unexpected query kind {other:?}"),
3359                    self.position(),
3360                )),
3361            },
3362            Token::From => match self.parse_from_query()? {
3363                QueryExpr::Table(query) => Ok(SqlCommand::Select(query)),
3364                QueryExpr::Join(query) => Ok(SqlCommand::Join(query)),
3365                other => Err(ParseError::new(
3366                    format!("internal: FROM produced unexpected query kind {other:?}"),
3367                    self.position(),
3368                )),
3369            },
3370            Token::Insert => match self.parse_insert_query()? {
3371                QueryExpr::Insert(query) => Ok(SqlCommand::Insert(query)),
3372                other => Err(ParseError::new(
3373                    format!("internal: INSERT produced unexpected query kind {other:?}"),
3374                    self.position(),
3375                )),
3376            },
3377            Token::Update => match self.parse_update_query()? {
3378                QueryExpr::Update(query) => Ok(SqlCommand::Update(query)),
3379                other => Err(ParseError::new(
3380                    format!("internal: UPDATE produced unexpected query kind {other:?}"),
3381                    self.position(),
3382                )),
3383            },
3384            Token::Delete => {
3385                if matches!(self.peek_next()?, Token::Ident(n) if n.eq_ignore_ascii_case("SECRET"))
3386                {
3387                    self.advance()?; // DELETE
3388                    self.advance()?; // SECRET
3389                    let key =
3390                        Self::normalize_secret_admin_path(self.parse_dotted_admin_path(true)?);
3391                    Ok(SqlCommand::DeleteSecret { key })
3392                } else {
3393                    match self.parse_delete_query()? {
3394                        QueryExpr::Delete(query) => Ok(SqlCommand::Delete(query)),
3395                        other => Err(ParseError::new(
3396                            format!("internal: DELETE produced unexpected query kind {other:?}"),
3397                            self.position(),
3398                        )),
3399                    }
3400                }
3401            }
3402            Token::Truncate => {
3403                self.advance()?;
3404                let model = if self.consume(&Token::Table)? {
3405                    Some(CollectionModel::Table)
3406                } else if self.consume(&Token::Graph)? {
3407                    Some(CollectionModel::Graph)
3408                } else if self.consume(&Token::Vector)? {
3409                    Some(CollectionModel::Vector)
3410                } else if self.consume(&Token::Document)? {
3411                    Some(CollectionModel::Document)
3412                } else if self.consume(&Token::Timeseries)? {
3413                    Some(CollectionModel::TimeSeries)
3414                } else if self.consume_ident_ci("METRICS")? {
3415                    Some(CollectionModel::Metrics)
3416                } else if self.consume(&Token::Kv)? {
3417                    Some(CollectionModel::Kv)
3418                } else if self.consume(&Token::Queue)? {
3419                    Some(CollectionModel::Queue)
3420                } else if self.consume(&Token::Collection)? {
3421                    None
3422                } else {
3423                    return Err(ParseError::expected(
3424                        vec![
3425                            "TABLE",
3426                            "GRAPH",
3427                            "VECTOR",
3428                            "DOCUMENT",
3429                            "TIMESERIES",
3430                            "METRICS",
3431                            "KV",
3432                            "QUEUE",
3433                            "COLLECTION",
3434                        ],
3435                        self.peek(),
3436                        self.position(),
3437                    ));
3438                };
3439                match self.parse_truncate_body(model)? {
3440                    QueryExpr::Truncate(query) => Ok(SqlCommand::Truncate(query)),
3441                    other => Err(ParseError::new(
3442                        format!("internal: TRUNCATE produced unexpected kind {other:?}"),
3443                        self.position(),
3444                    )),
3445                }
3446            }
3447            Token::Explain => {
3448                // Peek ahead: EXPLAIN MIGRATION name → ExplainMigration
3449                // EXPLAIN ALTER FOR ... → ExplainAlter (existing path)
3450                if matches!(self.peek_next()?, Token::Ident(n) if n.eq_ignore_ascii_case("MIGRATION"))
3451                {
3452                    self.advance()?; // consume EXPLAIN
3453                    match self.parse_explain_migration_after_keyword()? {
3454                        QueryExpr::ExplainMigration(q) => Ok(SqlCommand::ExplainMigration(q)),
3455                        other => Err(ParseError::new(
3456                            format!(
3457                                "internal: EXPLAIN MIGRATION produced unexpected kind {other:?}"
3458                            ),
3459                            self.position(),
3460                        )),
3461                    }
3462                } else {
3463                    match self.parse_explain_alter_query()? {
3464                        QueryExpr::ExplainAlter(query) => Ok(SqlCommand::ExplainAlter(query)),
3465                        other => Err(ParseError::new(
3466                            format!("internal: EXPLAIN produced unexpected query kind {other:?}"),
3467                            self.position(),
3468                        )),
3469                    }
3470                }
3471            }
3472            Token::Create => self.parse_create_command(),
3473            Token::Drop => {
3474                let pos = self.position();
3475                self.advance()?;
3476
3477                // DROP [MATERIALIZED] VIEW [IF EXISTS] name
3478                let materialized = self.consume(&Token::Materialized)?;
3479                if self.check(&Token::View) {
3480                    self.advance()?;
3481                    let if_exists = self.match_if_exists()?;
3482                    let name = self.expect_ident()?;
3483                    return Ok(SqlCommand::DropView(DropViewQuery {
3484                        name,
3485                        materialized,
3486                        if_exists,
3487                    }));
3488                }
3489                if materialized {
3490                    return Err(ParseError::expected(
3491                        vec!["VIEW"],
3492                        self.peek(),
3493                        self.position(),
3494                    ));
3495                }
3496
3497                if self.consume_ident_ci("BRANCH")? {
3498                    self.parse_drop_vcs_ref(VcsRefKind::Branch)
3499                } else if self.consume_ident_ci("TAG")? {
3500                    self.parse_drop_vcs_ref(VcsRefKind::Tag)
3501                } else if self.check(&Token::Index) {
3502                    match self.parse_drop_index_query()? {
3503                        QueryExpr::DropIndex(query) => Ok(SqlCommand::DropIndex(query)),
3504                        other => Err(ParseError::new(
3505                            format!("internal: DROP INDEX produced unexpected kind {other:?}"),
3506                            self.position(),
3507                        )),
3508                    }
3509                } else if self.check(&Token::Table) {
3510                    self.expect(Token::Table)?;
3511                    match self.parse_drop_table_body()? {
3512                        QueryExpr::DropTable(query) => Ok(SqlCommand::DropTable(query)),
3513                        other => Err(ParseError::new(
3514                            format!("internal: DROP TABLE produced unexpected kind {other:?}"),
3515                            self.position(),
3516                        )),
3517                    }
3518                } else if self.check(&Token::Graph) {
3519                    self.advance()?;
3520                    match self.parse_drop_graph_body()? {
3521                        QueryExpr::DropGraph(query) => Ok(SqlCommand::DropGraph(query)),
3522                        other => Err(ParseError::new(
3523                            format!("internal: DROP GRAPH produced unexpected kind {other:?}"),
3524                            self.position(),
3525                        )),
3526                    }
3527                } else if self.check(&Token::Vector) {
3528                    self.advance()?;
3529                    match self.parse_drop_vector_body()? {
3530                        QueryExpr::DropVector(query) => Ok(SqlCommand::DropVector(query)),
3531                        other => Err(ParseError::new(
3532                            format!("internal: DROP VECTOR produced unexpected kind {other:?}"),
3533                            self.position(),
3534                        )),
3535                    }
3536                } else if self.check(&Token::Document) {
3537                    self.advance()?;
3538                    match self.parse_drop_document_body()? {
3539                        QueryExpr::DropDocument(query) => Ok(SqlCommand::DropDocument(query)),
3540                        other => Err(ParseError::new(
3541                            format!("internal: DROP DOCUMENT produced unexpected kind {other:?}"),
3542                            self.position(),
3543                        )),
3544                    }
3545                } else if self.check(&Token::Kv) {
3546                    self.advance()?;
3547                    match self.parse_drop_kv_body()? {
3548                        QueryExpr::DropKv(query) => Ok(SqlCommand::DropKv(query)),
3549                        other => Err(ParseError::new(
3550                            format!("internal: DROP KV produced unexpected kind {other:?}"),
3551                            self.position(),
3552                        )),
3553                    }
3554                } else if self.consume_ident_ci("CONFIG")? {
3555                    match self.parse_drop_keyed_body(CollectionModel::Config)? {
3556                        QueryExpr::DropKv(query) => Ok(SqlCommand::DropKv(query)),
3557                        other => Err(ParseError::new(
3558                            format!("internal: DROP CONFIG produced unexpected kind {other:?}"),
3559                            self.position(),
3560                        )),
3561                    }
3562                } else if self.consume_ident_ci("VAULT")? {
3563                    match self.parse_drop_keyed_body(CollectionModel::Vault)? {
3564                        QueryExpr::DropKv(query) => Ok(SqlCommand::DropKv(query)),
3565                        other => Err(ParseError::new(
3566                            format!("internal: DROP VAULT produced unexpected kind {other:?}"),
3567                            self.position(),
3568                        )),
3569                    }
3570                } else if self.check(&Token::Collection) {
3571                    self.advance()?;
3572                    match self.parse_drop_collection_body()? {
3573                        QueryExpr::DropCollection(query) => Ok(SqlCommand::DropCollection(query)),
3574                        other => Err(ParseError::new(
3575                            format!("internal: DROP COLLECTION produced unexpected kind {other:?}"),
3576                            self.position(),
3577                        )),
3578                    }
3579                } else if self.check(&Token::Timeseries) {
3580                    self.advance()?;
3581                    match self.parse_drop_timeseries_body()? {
3582                        QueryExpr::DropTimeSeries(query) => Ok(SqlCommand::DropTimeSeries(query)),
3583                        other => Err(ParseError::new(
3584                            format!("internal: DROP TIMESERIES produced unexpected kind {other:?}"),
3585                            self.position(),
3586                        )),
3587                    }
3588                } else if self.consume_ident_ci("METRICS")? {
3589                    match self.parse_drop_collection_model_body(Some(CollectionModel::Metrics))? {
3590                        QueryExpr::DropCollection(query) => Ok(SqlCommand::DropCollection(query)),
3591                        other => Err(ParseError::new(
3592                            format!("internal: DROP METRICS produced unexpected kind {other:?}"),
3593                            self.position(),
3594                        )),
3595                    }
3596                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("HYPERTABLE"))
3597                {
3598                    // DROP HYPERTABLE name reuses the same AST as
3599                    // DROP TIMESERIES — runtime clears the registry
3600                    // entry *and* drops the backing collection.
3601                    self.advance()?;
3602                    match self.parse_drop_timeseries_body()? {
3603                        QueryExpr::DropTimeSeries(query) => Ok(SqlCommand::DropTimeSeries(query)),
3604                        other => Err(ParseError::new(
3605                            format!("internal: DROP HYPERTABLE produced unexpected kind {other:?}"),
3606                            self.position(),
3607                        )),
3608                    }
3609                } else if self.check(&Token::Queue) {
3610                    self.advance()?;
3611                    match self.parse_drop_queue_body()? {
3612                        QueryExpr::DropQueue(query) => Ok(SqlCommand::DropQueue(query)),
3613                        other => Err(ParseError::new(
3614                            format!("internal: DROP QUEUE produced unexpected kind {other:?}"),
3615                            self.position(),
3616                        )),
3617                    }
3618                } else if self.check(&Token::Tree) {
3619                    self.advance()?;
3620                    match self.parse_drop_tree_body()? {
3621                        QueryExpr::DropTree(query) => Ok(SqlCommand::DropTree(query)),
3622                        other => Err(ParseError::new(
3623                            format!("internal: DROP TREE produced unexpected kind {other:?}"),
3624                            self.position(),
3625                        )),
3626                    }
3627                } else if matches!(self.peek(), Token::Ident(n) if
3628                    n.eq_ignore_ascii_case("HLL") ||
3629                    n.eq_ignore_ascii_case("SKETCH") ||
3630                    n.eq_ignore_ascii_case("FILTER"))
3631                {
3632                    match self.parse_drop_probabilistic()? {
3633                        QueryExpr::ProbabilisticCommand(command) => {
3634                            Ok(SqlCommand::Probabilistic(command))
3635                        }
3636                        other => Err(ParseError::new(
3637                            format!(
3638                                "internal: DROP probabilistic produced unexpected kind {other:?}"
3639                            ),
3640                            self.position(),
3641                        )),
3642                    }
3643                } else if self.check(&Token::Schema) {
3644                    // DROP SCHEMA [IF EXISTS] name [CASCADE]
3645                    self.advance()?;
3646                    let if_exists = self.match_if_exists()?;
3647                    let name = self.expect_ident()?;
3648                    let cascade = self.consume(&Token::Cascade)?;
3649                    Ok(SqlCommand::DropSchema(DropSchemaQuery {
3650                        name,
3651                        if_exists,
3652                        cascade,
3653                    }))
3654                } else if self.check(&Token::Policy) {
3655                    // Two forms:
3656                    //   * IAM:   DROP POLICY '<id>'
3657                    //   * RLS:   DROP POLICY [IF EXISTS] name ON table
3658                    self.advance()?;
3659                    if matches!(self.peek(), Token::String(_)) {
3660                        let expr = self.parse_drop_iam_policy_after_keywords()?;
3661                        return Ok(SqlCommand::IamPolicy(expr));
3662                    }
3663                    let if_exists = self.match_if_exists()?;
3664                    let name = self.expect_ident()?;
3665                    self.expect(Token::On)?;
3666                    let table = self.expect_ident()?;
3667                    Ok(SqlCommand::DropPolicy(DropPolicyQuery {
3668                        name,
3669                        table,
3670                        if_exists,
3671                    }))
3672                } else if self.check(&Token::Server) {
3673                    // DROP SERVER [IF EXISTS] name [CASCADE]
3674                    self.advance()?;
3675                    let if_exists = self.match_if_exists()?;
3676                    let name = self.expect_ident()?;
3677                    let cascade = self.consume(&Token::Cascade)?;
3678                    Ok(SqlCommand::DropServer(DropServerQuery {
3679                        name,
3680                        if_exists,
3681                        cascade,
3682                    }))
3683                } else if self.check(&Token::Foreign) {
3684                    // DROP FOREIGN TABLE [IF EXISTS] name
3685                    self.advance()?;
3686                    self.expect(Token::Table)?;
3687                    let if_exists = self.match_if_exists()?;
3688                    let name = self.expect_ident()?;
3689                    Ok(SqlCommand::DropForeignTable(DropForeignTableQuery {
3690                        name,
3691                        if_exists,
3692                    }))
3693                } else if self.check(&Token::Sequence) {
3694                    // DROP SEQUENCE [IF EXISTS] name
3695                    self.advance()?;
3696                    let if_exists = self.match_if_exists()?;
3697                    let name = self.expect_ident()?;
3698                    Ok(SqlCommand::DropSequence(DropSequenceQuery {
3699                        name,
3700                        if_exists,
3701                    }))
3702                } else if let Some(err) =
3703                    ParseError::unsupported_recognized_token(self.peek(), self.position())
3704                {
3705                    Err(err)
3706                } else {
3707                    Err(ParseError::expected(
3708                        vec![
3709                            "TABLE",
3710                            "INDEX",
3711                            "TIMESERIES",
3712                            "QUEUE",
3713                            "TREE",
3714                            "HLL",
3715                            "SKETCH",
3716                            "FILTER",
3717                            "SCHEMA",
3718                            "SEQUENCE",
3719                            "BRANCH",
3720                            "TAG",
3721                        ],
3722                        self.peek(),
3723                        pos,
3724                    ))
3725                }
3726            }
3727            Token::Alter => {
3728                // Disambiguate ALTER USER / ALTER QUEUE / ALTER TABLE without
3729                // committing to a path until we've seen the target.
3730                // We peek the *next* token (without consuming) and
3731                // dispatch accordingly.
3732                let next = self.peek_next()?.clone();
3733                if matches!(next, Token::Ident(ref s) if s.eq_ignore_ascii_case("USER")) {
3734                    self.advance()?; // consume ALTER
3735                    let stmt = self.parse_alter_user_statement()?;
3736                    Ok(SqlCommand::AlterUser(stmt))
3737                } else if matches!(next, Token::Queue) {
3738                    self.advance()?; // consume ALTER
3739                    self.advance()?; // consume QUEUE
3740                    match self.parse_alter_queue_body()? {
3741                        QueryExpr::AlterQueue(query) => Ok(SqlCommand::AlterQueue(query)),
3742                        other => Err(ParseError::new(
3743                            format!("internal: ALTER QUEUE produced unexpected kind {other:?}"),
3744                            self.position(),
3745                        )),
3746                    }
3747                } else if matches!(next, Token::Metric) {
3748                    self.advance()?; // consume ALTER
3749                    self.advance()?; // consume METRIC
3750                    match self.parse_alter_metric_body()? {
3751                        QueryExpr::AlterMetric(query) => Ok(SqlCommand::AlterMetric(query)),
3752                        other => Err(ParseError::new(
3753                            format!("internal: ALTER METRIC produced unexpected kind {other:?}"),
3754                            self.position(),
3755                        )),
3756                    }
3757                } else if matches!(next, Token::Graph) {
3758                    // Issue #801 — `ALTER GRAPH name ADD|DROP ANALYTICS ...`
3759                    // shares the AlterTable AST so analytics-config lifecycle
3760                    // mutations dispatch through the existing executor path.
3761                    match self.parse_alter_graph_query()? {
3762                        QueryExpr::AlterTable(query) => Ok(SqlCommand::AlterTable(query)),
3763                        other => Err(ParseError::new(
3764                            format!(
3765                                "internal: ALTER GRAPH produced unexpected query kind {other:?}"
3766                            ),
3767                            self.position(),
3768                        )),
3769                    }
3770                } else if matches!(next, Token::Table)
3771                    || matches!(next, Token::Collection)
3772                    || matches!(next, Token::Ident(ref s) if s.eq_ignore_ascii_case("COLLECTION"))
3773                {
3774                    // Issue #522 — `ALTER COLLECTION` shares the AlterTable
3775                    // AST so signer-registry mutations dispatch through the
3776                    // existing executor. The DDL parser body accepts either
3777                    // keyword interchangeably for the open-vocabulary alters
3778                    // we own (currently `ADD|REVOKE SIGNER`).
3779                    match self.parse_alter_table_query()? {
3780                        QueryExpr::AlterTable(query) => Ok(SqlCommand::AlterTable(query)),
3781                        other => Err(ParseError::new(
3782                            format!(
3783                                "internal: ALTER TABLE produced unexpected query kind {other:?}"
3784                            ),
3785                            self.position(),
3786                        )),
3787                    }
3788                } else if let Some(err) =
3789                    ParseError::unsupported_recognized_token(&next, self.position())
3790                {
3791                    Err(err)
3792                } else {
3793                    match self.parse_alter_table_query()? {
3794                        QueryExpr::AlterTable(query) => Ok(SqlCommand::AlterTable(query)),
3795                        other => Err(ParseError::new(
3796                            format!("internal: ALTER produced unexpected query kind {other:?}"),
3797                            self.position(),
3798                        )),
3799                    }
3800                }
3801            }
3802            Token::Ident(name) if name.eq_ignore_ascii_case("GRANT") => {
3803                let stmt = self.parse_grant_statement()?;
3804                Ok(SqlCommand::Grant(stmt))
3805            }
3806            Token::Ident(name) if name.eq_ignore_ascii_case("REVOKE") => {
3807                let stmt = self.parse_revoke_statement()?;
3808                Ok(SqlCommand::Revoke(stmt))
3809            }
3810            Token::Ident(name) if name.eq_ignore_ascii_case("EVENTS") => {
3811                self.advance()?;
3812                if self.consume_ident_ci("BACKFILL")? {
3813                    return Err(ParseError::new(
3814                        "EVENTS BACKFILL STATUS is not implemented; EVENTS BACKFILL runtime is available but durable progress tracking is not"
3815                            .to_string(),
3816                        self.position(),
3817                    ));
3818                }
3819                if !self.consume_ident_ci("STATUS")? {
3820                    return Err(ParseError::expected(
3821                        vec!["STATUS"],
3822                        self.peek(),
3823                        self.position(),
3824                    ));
3825                }
3826
3827                let mut query = TableQuery::new("red.subscriptions");
3828                let collection = match self.peek().clone() {
3829                    Token::Ident(name) => {
3830                        self.advance()?;
3831                        Some(name)
3832                    }
3833                    Token::String(name) => {
3834                        self.advance()?;
3835                        Some(name)
3836                    }
3837                    _ => None,
3838                };
3839                self.parse_table_clauses(&mut query)?;
3840                if let Some(collection) = collection {
3841                    let filter = Filter::compare(
3842                        FieldRef::column("red.subscriptions", "collection"),
3843                        CompareOp::Eq,
3844                        Value::text(collection),
3845                    );
3846                    let expr = filter_to_expr(&filter);
3847                    query.where_expr = Some(match query.where_expr.take() {
3848                        Some(existing) => Expr::binop(BinOp::And, existing, expr),
3849                        None => expr,
3850                    });
3851                    query.filter = Some(match query.filter.take() {
3852                        Some(existing) => existing.and(filter),
3853                        None => filter,
3854                    });
3855                }
3856                Ok(SqlCommand::Select(query))
3857            }
3858            Token::Attach => {
3859                let expr = self.parse_attach_policy()?;
3860                Ok(SqlCommand::IamPolicy(expr))
3861            }
3862            Token::Detach => {
3863                let expr = self.parse_detach_policy()?;
3864                Ok(SqlCommand::IamPolicy(expr))
3865            }
3866            Token::Ident(name) if name.eq_ignore_ascii_case("SIMULATE") => {
3867                let expr = self.parse_simulate_policy()?;
3868                Ok(SqlCommand::IamPolicy(expr))
3869            }
3870            Token::Ident(name) if name.eq_ignore_ascii_case("LINT") => {
3871                let expr = self.parse_lint_policy()?;
3872                Ok(SqlCommand::IamPolicy(expr))
3873            }
3874            Token::Ident(name) if name.eq_ignore_ascii_case("MIGRATE") => {
3875                // `MIGRATE POLICY MODE TO ...` is the S5B (#714) path.
3876                // Lookahead one token because `MIGRATE` is otherwise
3877                // unused at this layer.
3878                let next = self.peek_next()?.clone();
3879                let is_policy_mode = matches!(&next, Token::Policy)
3880                    || matches!(&next, Token::Ident(name)
3881                        if name.eq_ignore_ascii_case("POLICY"));
3882                if is_policy_mode {
3883                    let expr = self.parse_migrate_policy_mode()?;
3884                    return Ok(SqlCommand::IamPolicy(expr));
3885                }
3886                Err(ParseError::expected(
3887                    vec!["POLICY"],
3888                    self.peek(),
3889                    self.position(),
3890                ))
3891            }
3892            Token::Ident(name) if is_vcs_command_head(name) => {
3893                self.parse_vcs_command().map(SqlCommand::Vcs)
3894            }
3895            // RESOLVE CONFLICT is the VCS working-set verb. The frontend only
3896            // routes RESOLVE here when CONFLICT follows; plain RESOLVE / RESOLVE
3897            // CONFIG stays on the config surface and never reaches this arm.
3898            Token::Ident(name) if name.eq_ignore_ascii_case("RESOLVE") => {
3899                self.parse_vcs_command().map(SqlCommand::Vcs)
3900            }
3901            Token::Set => {
3902                self.advance()?;
3903                if self.consume_ident_ci("CONFIG")? {
3904                    let full_key = self.parse_dotted_admin_path(true)?;
3905                    self.expect(Token::Eq)?;
3906                    let value = self.parse_literal_value()?;
3907                    Ok(SqlCommand::SetConfig {
3908                        key: full_key,
3909                        value,
3910                    })
3911                } else if self.consume_ident_ci("SECRET")? {
3912                    let key =
3913                        Self::normalize_secret_admin_path(self.parse_dotted_admin_path(true)?);
3914                    self.expect(Token::Eq)?;
3915                    let value = self.parse_literal_value()?;
3916                    Ok(SqlCommand::SetSecret { key, value })
3917                } else if self.consume_ident_ci("TENANT")? {
3918                    // SET TENANT 'id'  |  SET TENANT = 'id'  |
3919                    // SET TENANT NULL  |  SET TENANT = NULL
3920                    let _ = self.consume(&Token::Eq)?;
3921                    if self.consume_ident_ci("NULL")? {
3922                        Ok(SqlCommand::SetTenant(None))
3923                    } else {
3924                        let value = self.parse_literal_value()?;
3925                        match value {
3926                            Value::Text(s) => Ok(SqlCommand::SetTenant(Some(s.to_string()))),
3927                            Value::Null => Ok(SqlCommand::SetTenant(None)),
3928                            other => Err(ParseError::new(
3929                                format!("SET TENANT expects a text literal or NULL, got {other:?}"),
3930                                self.position(),
3931                            )),
3932                        }
3933                    }
3934                } else {
3935                    Err(ParseError::expected(
3936                        vec!["CONFIG", "SECRET", "TENANT"],
3937                        self.peek(),
3938                        self.position(),
3939                    ))
3940                }
3941            }
3942            Token::Ident(name) if name.eq_ignore_ascii_case("APPLY") => {
3943                self.advance()?;
3944                match self.parse_apply_migration()? {
3945                    QueryExpr::ApplyMigration(q) => Ok(SqlCommand::ApplyMigration(q)),
3946                    other => Err(ParseError::new(
3947                        format!("internal: APPLY MIGRATION produced unexpected kind {other:?}"),
3948                        self.position(),
3949                    )),
3950                }
3951            }
3952            Token::Ident(name) if name.eq_ignore_ascii_case("RESET") => {
3953                // RESET TENANT clears session-local tenant state; every
3954                // other RESET shape in this parser belongs to the VCS
3955                // working-set command (`RESET [HARD|SOFT|MIXED] TO ...`).
3956                if matches!(self.peek_next()?, Token::Ident(next) if next.eq_ignore_ascii_case("TENANT"))
3957                {
3958                    self.advance()?;
3959                    self.advance()?;
3960                    Ok(SqlCommand::SetTenant(None))
3961                } else {
3962                    self.parse_vcs_command().map(SqlCommand::Vcs)
3963                }
3964            }
3965            Token::Ident(name)
3966                if name.eq_ignore_ascii_case("DESCRIBE") || name.eq_ignore_ascii_case("DESC") =>
3967            {
3968                self.advance()?;
3969                let collection = self.parse_dotted_admin_path(false)?;
3970                let mut query = TableQuery::new("red.describe");
3971                query.filter = Some(Filter::compare(
3972                    FieldRef::column("", "collection"),
3973                    CompareOp::Eq,
3974                    Value::text(collection),
3975                ));
3976                Ok(SqlCommand::Select(query))
3977            }
3978            Token::Desc => {
3979                self.advance()?;
3980                let collection = self.parse_dotted_admin_path(false)?;
3981                let mut query = TableQuery::new("red.describe");
3982                query.filter = Some(Filter::compare(
3983                    FieldRef::column("", "collection"),
3984                    CompareOp::Eq,
3985                    Value::text(collection),
3986                ));
3987                Ok(SqlCommand::Select(query))
3988            }
3989            Token::Ident(name) if name.eq_ignore_ascii_case("SHOW") => {
3990                self.advance()?;
3991                if self.consume(&Token::Create)? || self.consume_ident_ci("CREATE")? {
3992                    if !(self.consume(&Token::Table)? || self.consume_ident_ci("TABLE")?) {
3993                        return Err(ParseError::expected(
3994                            vec!["TABLE"],
3995                            self.peek(),
3996                            self.position(),
3997                        ));
3998                    }
3999                    let collection = self.parse_dotted_admin_path(false)?;
4000                    let mut query = TableQuery::new("red.show_create");
4001                    query.filter = Some(Filter::compare(
4002                        FieldRef::column("", "collection"),
4003                        CompareOp::Eq,
4004                        Value::text(collection),
4005                    ));
4006                    Ok(SqlCommand::Select(query))
4007                } else if self.consume_ident_ci("CONFIG")? {
4008                    // Accept dotted prefixes the same way SET CONFIG does
4009                    // (`SHOW CONFIG durability.mode`), and empty prefix
4010                    // (`SHOW CONFIG`) for a catalog-wide listing.
4011                    let prefix = if !(self.check(&Token::Eof)
4012                        || self.check(&Token::As)
4013                        || self.check(&Token::Format))
4014                    {
4015                        let first = self.expect_ident()?;
4016                        let mut full = first;
4017                        while self.consume(&Token::Dot)? {
4018                            let next = self.expect_ident_or_keyword()?;
4019                            full = format!("{full}.{next}");
4020                        }
4021                        // Match SET CONFIG: lowercase so keyword segments
4022                        // come out consistent with the stored keys.
4023                        Some(full.to_ascii_lowercase())
4024                    } else {
4025                        None
4026                    };
4027                    let as_json = if self.consume(&Token::As)? || self.consume(&Token::Format)? {
4028                        if !self.consume(&Token::Json)? {
4029                            return Err(ParseError::expected(
4030                                vec!["JSON"],
4031                                self.peek(),
4032                                self.position(),
4033                            ));
4034                        }
4035                        true
4036                    } else {
4037                        false
4038                    };
4039                    Ok(SqlCommand::ShowConfig { prefix, as_json })
4040                } else if self.consume_ident_ci("COLLECTIONS")? {
4041                    let mut query = TableQuery::new("red.collections");
4042                    let include_internal = if self.consume_ident_ci("INCLUDING")? {
4043                        if !self.consume_ident_ci("INTERNAL")? {
4044                            return Err(ParseError::expected(
4045                                vec!["INTERNAL"],
4046                                self.peek(),
4047                                self.position(),
4048                            ));
4049                        }
4050                        true
4051                    } else {
4052                        false
4053                    };
4054                    self.parse_table_clauses(&mut query)?;
4055                    if !include_internal {
4056                        let user_filter = query.filter.take();
4057                        let hide_internal = crate::ast::Filter::Compare {
4058                            field: FieldRef::column("", "internal"),
4059                            op: CompareOp::Eq,
4060                            value: Value::Boolean(false),
4061                        };
4062                        query.filter = Some(match user_filter {
4063                            Some(filter) => filter.and(hide_internal),
4064                            None => hide_internal,
4065                        });
4066                    }
4067                    Ok(SqlCommand::Select(query))
4068                } else if self.consume_ident_ci("TABLES")? {
4069                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4070                        self, "table",
4071                    )?))
4072                } else if self.consume_ident_ci("QUEUES")? {
4073                    // Issue #535 — `SHOW QUEUES` desugars to the
4074                    // `red.queues` virtual table (queue-shaped
4075                    // columns), not the filtered `red.collections`
4076                    // view. `INCLUDING INTERNAL` mirrors the
4077                    // `SHOW COLLECTIONS` opt-in: without it, DLQ
4078                    // targets and other auto-created queues are
4079                    // hidden via the `internal = false` filter.
4080                    let mut query = TableQuery::new("red.queues");
4081                    let include_internal = if self.consume_ident_ci("INCLUDING")? {
4082                        if !self.consume_ident_ci("INTERNAL")? {
4083                            return Err(ParseError::expected(
4084                                vec!["INTERNAL"],
4085                                self.peek(),
4086                                self.position(),
4087                            ));
4088                        }
4089                        true
4090                    } else {
4091                        false
4092                    };
4093                    self.parse_table_clauses(&mut query)?;
4094                    if !include_internal {
4095                        let hide_internal = Filter::Compare {
4096                            field: FieldRef::column("", "internal"),
4097                            op: CompareOp::Eq,
4098                            value: Value::Boolean(false),
4099                        };
4100                        add_table_filter(&mut query, hide_internal);
4101                    }
4102                    Ok(SqlCommand::Select(query))
4103                } else if self.consume_ident_ci("BRANCHES")? {
4104                    self.parse_show_vcs_ref_table("red.branches")
4105                } else if self.consume_ident_ci("TAGS")? {
4106                    self.parse_show_vcs_ref_table("red.tags")
4107                } else if self.consume(&Token::Vectors)? || self.consume_ident_ci("VECTORS")? {
4108                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4109                        self, "vector",
4110                    )?))
4111                } else if self.consume_ident_ci("DOCUMENTS")? {
4112                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4113                        self, "document",
4114                    )?))
4115                } else if self.consume(&Token::Timeseries)?
4116                    || self.consume_ident_ci("TIMESERIES")?
4117                {
4118                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4119                        self,
4120                        "timeseries",
4121                    )?))
4122                } else if self.consume_ident_ci("GRAPHS")? {
4123                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4124                        self, "graph",
4125                    )?))
4126                } else if self.consume_ident_ci("CONFIGS")? {
4127                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4128                        self, "config",
4129                    )?))
4130                } else if self.consume_ident_ci("VAULTS")? {
4131                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4132                        self, "vault",
4133                    )?))
4134                } else if self.consume(&Token::Kv)?
4135                    || self.consume_ident_ci("KV")?
4136                    || self.consume_ident_ci("KVS")?
4137                {
4138                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4139                        self, "kv",
4140                    )?))
4141                } else if self.consume(&Token::Schema)? || self.consume_ident_ci("SCHEMA")? {
4142                    let collection = self.parse_dotted_admin_path(false)?;
4143                    let mut query = TableQuery::new("red.columns");
4144                    query.filter = Some(Filter::compare(
4145                        FieldRef::column("", "collection"),
4146                        CompareOp::Eq,
4147                        Value::text(collection),
4148                    ));
4149                    Ok(SqlCommand::Select(query))
4150                } else if self.consume_ident_ci("INDICES")? || self.consume_ident_ci("INDEXES")? {
4151                    let mut query = TableQuery::new("red.show_indexes");
4152                    if self.consume(&Token::On)? {
4153                        let collection = self.expect_ident_or_keyword()?;
4154                        let filter = Filter::Compare {
4155                            field: FieldRef::column("", "table"),
4156                            op: CompareOp::Eq,
4157                            value: Value::text(collection),
4158                        };
4159                        query.where_expr = Some(filter_to_expr(&filter));
4160                        query.filter = Some(filter);
4161                    }
4162                    self.parse_table_clauses(&mut query)?;
4163                    Ok(SqlCommand::Select(query))
4164                } else if self.consume_ident_ci("POLICIES")? {
4165                    if self.consume(&Token::For)? || self.consume_ident_ci("FOR")? {
4166                        let principal = self.parse_iam_principal_kind()?;
4167                        return Ok(SqlCommand::IamPolicy(QueryExpr::ShowPolicies {
4168                            filter: Some(principal),
4169                        }));
4170                    }
4171                    let mut query = TableQuery::new("red.policies");
4172                    let collection_filter =
4173                        if self.consume(&Token::On)? || self.consume_ident_ci("ON")? {
4174                            let collection = self.parse_dotted_admin_path(false)?;
4175                            Some(Filter::Compare {
4176                                field: FieldRef::TableColumn {
4177                                    table: String::new(),
4178                                    column: "collection".to_string(),
4179                                },
4180                                op: CompareOp::Eq,
4181                                value: Value::text(collection),
4182                            })
4183                        } else {
4184                            None
4185                        };
4186                    self.parse_table_clauses(&mut query)?;
4187                    if let Some(collection_filter) = collection_filter {
4188                        let combined = match query.filter.take() {
4189                            Some(existing) => {
4190                                Filter::And(Box::new(collection_filter), Box::new(existing))
4191                            }
4192                            None => collection_filter,
4193                        };
4194                        query.where_expr = Some(filter_to_expr(&combined));
4195                        query.filter = Some(combined);
4196                    }
4197                    Ok(SqlCommand::Select(query))
4198                } else if self.consume_ident_ci("STATS")? {
4199                    let mut query = TableQuery::new("red.stats");
4200                    let collection = match self.peek().clone() {
4201                        Token::Ident(name) => {
4202                            self.advance()?;
4203                            Some(name)
4204                        }
4205                        Token::String(name) => {
4206                            self.advance()?;
4207                            Some(name)
4208                        }
4209                        _ => None,
4210                    };
4211                    self.parse_table_clauses(&mut query)?;
4212                    if let Some(collection) = collection {
4213                        let filter = Filter::compare(
4214                            FieldRef::column("red.stats", "collection"),
4215                            CompareOp::Eq,
4216                            Value::text(collection),
4217                        );
4218                        let expr = filter_to_expr(&filter);
4219                        query.where_expr = Some(match query.where_expr.take() {
4220                            Some(existing) => Expr::binop(BinOp::And, existing, expr),
4221                            None => expr,
4222                        });
4223                        query.filter = Some(match query.filter.take() {
4224                            Some(existing) => existing.and(filter),
4225                            None => filter,
4226                        });
4227                    }
4228                    Ok(SqlCommand::Select(query))
4229                } else if self.consume_ident_ci("SAMPLE")? {
4230                    let mut query = TableQuery::new(&self.expect_ident()?);
4231                    query.limit = if self.consume(&Token::Limit)? {
4232                        Some(self.parse_integer()? as u64)
4233                    } else {
4234                        Some(10)
4235                    };
4236                    Ok(SqlCommand::Select(query))
4237                } else if self.consume_ident_ci("SECRET")? || self.consume_ident_ci("SECRETS")? {
4238                    let prefix = if !self.check(&Token::Eof) {
4239                        Some(Self::normalize_secret_admin_path(
4240                            self.parse_dotted_admin_path(true)?,
4241                        ))
4242                    } else {
4243                        None
4244                    };
4245                    Ok(SqlCommand::ShowSecrets { prefix })
4246                } else if self.consume_ident_ci("TENANT")? {
4247                    Ok(SqlCommand::ShowTenant)
4248                } else if let Some(expr) = self.parse_show_iam_after_show()? {
4249                    Ok(SqlCommand::IamPolicy(expr))
4250                } else {
4251                    Err(ParseError::expected(
4252                        vec![
4253                            "CONFIG",
4254                            "SECRET",
4255                            "SECRETS",
4256                            "COLLECTIONS",
4257                            "TABLES",
4258                            "QUEUES",
4259                            "VECTORS",
4260                            "DOCUMENTS",
4261                            "TIMESERIES",
4262                            "GRAPHS",
4263                            "KV",
4264                            "SCHEMA",
4265                            "INDICES",
4266                            "INDEXES",
4267                            "SAMPLE",
4268                            "POLICIES",
4269                            "STATS",
4270                            "TENANT",
4271                            "EFFECTIVE",
4272                        ],
4273                        self.peek(),
4274                        self.position(),
4275                    ))
4276                }
4277            }
4278            // Transaction control statements (Phase 1.1 PG parity).
4279            // BEGIN [WORK | TRANSACTION] [ISOLATION LEVEL <mode>]
4280            // START TRANSACTION [ISOLATION LEVEL <mode>]
4281            //
4282            // We only implement SNAPSHOT ISOLATION (our default). We
4283            // accept READ UNCOMMITTED / READ COMMITTED / REPEATABLE
4284            // READ / SNAPSHOT as PG-compatible no-ops, but reject
4285            // SERIALIZABLE outright — the previous behaviour of
4286            // silently degrading to snapshot made the parser
4287            // dishonest. Real SSI (Serializable Snapshot Isolation)
4288            // is tracked as a future milestone.
4289            Token::Begin | Token::Start => {
4290                self.advance()?;
4291                let _ = self.consume(&Token::Work)? || self.consume(&Token::Transaction)?;
4292                // Optional ISOLATION LEVEL clause.
4293                if self.consume_ident_ci("ISOLATION")? {
4294                    self.expect(Token::Level)?;
4295                    // The level identifier can span multiple words
4296                    // (READ UNCOMMITTED / READ COMMITTED / REPEATABLE
4297                    // READ). Collect them case-insensitively.
4298                    let mut parts: Vec<String> = Vec::new();
4299                    if self.consume_ident_ci("READ")? {
4300                        parts.push("READ".to_string());
4301                        if self.consume_ident_ci("UNCOMMITTED")? {
4302                            parts.push("UNCOMMITTED".to_string());
4303                        } else if self.consume_ident_ci("COMMITTED")? {
4304                            parts.push("COMMITTED".to_string());
4305                        } else {
4306                            return Err(ParseError::expected(
4307                                vec!["UNCOMMITTED", "COMMITTED"],
4308                                self.peek(),
4309                                self.position(),
4310                            ));
4311                        }
4312                    } else if self.consume_ident_ci("REPEATABLE")? {
4313                        parts.push("REPEATABLE".to_string());
4314                        if !self.consume_ident_ci("READ")? {
4315                            return Err(ParseError::expected(
4316                                vec!["READ"],
4317                                self.peek(),
4318                                self.position(),
4319                            ));
4320                        }
4321                        parts.push("READ".to_string());
4322                    } else if self.consume_ident_ci("SNAPSHOT")? {
4323                        parts.push("SNAPSHOT".to_string());
4324                    } else if self.consume_ident_ci("SERIALIZABLE")? {
4325                        return Err(ParseError::new(
4326                            "ISOLATION LEVEL SERIALIZABLE is not yet supported — reddb \
4327                             currently provides SNAPSHOT ISOLATION (which PG calls \
4328                             REPEATABLE READ). Use REPEATABLE READ / SNAPSHOT / \
4329                             READ COMMITTED, or omit ISOLATION LEVEL for the default."
4330                                .to_string(),
4331                            self.position(),
4332                        ));
4333                    } else {
4334                        return Err(ParseError::expected(
4335                            vec!["READ", "REPEATABLE", "SNAPSHOT", "SERIALIZABLE"],
4336                            self.peek(),
4337                            self.position(),
4338                        ));
4339                    }
4340                    // All accepted modes map to our snapshot engine today.
4341                    let _ = parts;
4342                }
4343                Ok(SqlCommand::TransactionControl(TxnControl::Begin))
4344            }
4345            // COMMIT [WORK | TRANSACTION]
4346            Token::Commit => {
4347                self.advance()?;
4348                let _ = self.consume(&Token::Work)? || self.consume(&Token::Transaction)?;
4349                Ok(SqlCommand::TransactionControl(TxnControl::Commit))
4350            }
4351            // ROLLBACK [WORK | TRANSACTION] [TO [SAVEPOINT] name]
4352            // ROLLBACK MIGRATION name
4353            Token::Rollback => {
4354                self.advance()?;
4355                if matches!(self.peek(), Token::Ident(n) if n.eq_ignore_ascii_case("MIGRATION")) {
4356                    match self.parse_rollback_migration_after_keyword()? {
4357                        QueryExpr::RollbackMigration(q) => Ok(SqlCommand::RollbackMigration(q)),
4358                        other => Err(ParseError::new(
4359                            format!(
4360                                "internal: ROLLBACK MIGRATION produced unexpected kind {other:?}"
4361                            ),
4362                            self.position(),
4363                        )),
4364                    }
4365                } else {
4366                    let _ = self.consume(&Token::Work)? || self.consume(&Token::Transaction)?;
4367                    if self.consume(&Token::To)? {
4368                        let _ = self.consume(&Token::Savepoint)?;
4369                        let name = self.expect_ident()?;
4370                        Ok(SqlCommand::TransactionControl(
4371                            TxnControl::RollbackToSavepoint(name),
4372                        ))
4373                    } else {
4374                        Ok(SqlCommand::TransactionControl(TxnControl::Rollback))
4375                    }
4376                }
4377            }
4378            // SAVEPOINT name
4379            Token::Savepoint => {
4380                self.advance()?;
4381                let name = self.expect_ident()?;
4382                Ok(SqlCommand::TransactionControl(TxnControl::Savepoint(name)))
4383            }
4384            // RELEASE [SAVEPOINT] name
4385            Token::Release => {
4386                self.advance()?;
4387                let _ = self.consume(&Token::Savepoint)?;
4388                let name = self.expect_ident()?;
4389                Ok(SqlCommand::TransactionControl(
4390                    TxnControl::ReleaseSavepoint(name),
4391                ))
4392            }
4393            // VACUUM [FULL] [table]
4394            Token::Vacuum => {
4395                self.advance()?;
4396                let full = self.consume(&Token::Full)?;
4397                let target = if self.check(&Token::Eof) {
4398                    None
4399                } else {
4400                    Some(self.expect_ident()?)
4401                };
4402                Ok(SqlCommand::Maintenance(MaintenanceCommand::Vacuum {
4403                    target,
4404                    full,
4405                }))
4406            }
4407            // REFRESH MATERIALIZED VIEW name
4408            Token::Refresh => {
4409                self.advance()?;
4410                self.expect(Token::Materialized)?;
4411                self.expect(Token::View)?;
4412                let name = self.expect_ident()?;
4413                Ok(SqlCommand::RefreshMaterializedView(
4414                    RefreshMaterializedViewQuery { name },
4415                ))
4416            }
4417            // ANALYZE [table]
4418            Token::Analyze => {
4419                self.advance()?;
4420                let target = if self.check(&Token::Eof) {
4421                    None
4422                } else {
4423                    Some(self.expect_ident()?)
4424                };
4425                Ok(SqlCommand::Maintenance(MaintenanceCommand::Analyze {
4426                    target,
4427                }))
4428            }
4429            // COPY table FROM 'path' [WITH (...)] [DELIMITER 'x'] [HEADER [true|false]]
4430            //
4431            // Accepts both PG-style `WITH (FORMAT csv, HEADER true)` and the
4432            // short-form `DELIMITER ',' HEADER`. The only supported format
4433            // today is CSV.
4434            Token::Copy => {
4435                self.advance()?;
4436                let table = self.expect_ident()?;
4437                self.expect(Token::From)?;
4438                let path = self.parse_string()?;
4439
4440                let mut delimiter: Option<char> = None;
4441                let mut has_header = false;
4442                let format = CopyFormat::Csv;
4443
4444                // Optional `WITH (FORMAT csv, HEADER true, DELIMITER ',')` block.
4445                // `WITH` is a reserved keyword token — accept both the keyword
4446                // form and the ident form that non-CTE callers sometimes emit.
4447                if self.consume(&Token::With)? || self.consume_ident_ci("WITH")? {
4448                    self.expect(Token::LParen)?;
4449                    loop {
4450                        if self.consume(&Token::Format)? || self.consume_ident_ci("FORMAT")? {
4451                            let _ = self.consume(&Token::Eq)?;
4452                            // Only CSV for now — accept the ident and move on.
4453                            let _ = self.expect_ident()?;
4454                        } else if self.consume(&Token::Header)? {
4455                            let _ = self.consume(&Token::Eq)?;
4456                            // Accept `HEADER`, `HEADER = true`, `HEADER = false`,
4457                            // or an ident spelling of true/false.
4458                            has_header = match self.peek().clone() {
4459                                Token::True => {
4460                                    self.advance()?;
4461                                    true
4462                                }
4463                                Token::False => {
4464                                    self.advance()?;
4465                                    false
4466                                }
4467                                Token::Ident(ref n) if n.eq_ignore_ascii_case("true") => {
4468                                    self.advance()?;
4469                                    true
4470                                }
4471                                Token::Ident(ref n) if n.eq_ignore_ascii_case("false") => {
4472                                    self.advance()?;
4473                                    false
4474                                }
4475                                _ => true,
4476                            };
4477                        } else if self.consume(&Token::Delimiter)? {
4478                            let _ = self.consume(&Token::Eq)?;
4479                            let s = self.parse_string()?;
4480                            delimiter = s.chars().next();
4481                        } else {
4482                            break;
4483                        }
4484                        if !self.consume(&Token::Comma)? {
4485                            break;
4486                        }
4487                    }
4488                    self.expect(Token::RParen)?;
4489                }
4490
4491                // Short form clauses outside WITH (in either order).
4492                loop {
4493                    if self.consume(&Token::Delimiter)? {
4494                        let s = self.parse_string()?;
4495                        delimiter = s.chars().next();
4496                    } else if self.consume(&Token::Header)? {
4497                        has_header = true;
4498                    } else {
4499                        break;
4500                    }
4501                }
4502
4503                Ok(SqlCommand::CopyFrom(CopyFromQuery {
4504                    table,
4505                    path,
4506                    format,
4507                    delimiter,
4508                    has_header,
4509                }))
4510            }
4511            other => Err(ParseError::expected(
4512                vec![
4513                    "SELECT",
4514                    "FROM",
4515                    "INSERT",
4516                    "UPDATE",
4517                    "DELETE",
4518                    "EXPLAIN",
4519                    "CREATE",
4520                    "DROP",
4521                    "ALTER",
4522                    "SET",
4523                    "SHOW",
4524                    "BEGIN",
4525                    "COMMIT",
4526                    "ROLLBACK",
4527                    "SAVEPOINT",
4528                    "RELEASE",
4529                    "START",
4530                    "VACUUM",
4531                    "ANALYZE",
4532                    "COPY",
4533                    "REFRESH",
4534                    "DESCRIBE",
4535                    "DESC",
4536                ],
4537                other,
4538                self.position(),
4539            )),
4540        }
4541    }
4542}