Skip to main content

reddb_rql/
core.rs

1use std::fmt;
2
3use super::builders::{GraphQueryBuilder, PathQueryBuilder, TableQueryBuilder};
4use reddb_types::catalog::CollectionModel;
5pub use reddb_types::distance::DistanceMetric;
6pub use reddb_types::queue_mode::QueueMode;
7use reddb_types::types::{SqlTypeName, Value};
8pub use reddb_types::vector_metadata::MetadataFilter;
9
10/// Root query expression
11#[derive(Debug, Clone)]
12#[allow(clippy::large_enum_variant)]
13pub enum QueryExpr {
14    /// Pure table query: SELECT ... FROM ...
15    Table(TableQuery),
16    /// Pure graph query: MATCH ... RETURN ...
17    Graph(GraphQuery),
18    /// Join between table and graph
19    Join(JoinQuery),
20    /// Path query: PATH FROM ... TO ...
21    Path(PathQuery),
22    /// Vector similarity search
23    Vector(VectorQuery),
24    /// Hybrid query combining structured and vector search
25    Hybrid(HybridQuery),
26    /// INSERT INTO table (cols) VALUES (vals)
27    Insert(InsertQuery),
28    /// UPDATE table SET col=val WHERE filter
29    Update(UpdateQuery),
30    /// DELETE FROM table WHERE filter
31    Delete(DeleteQuery),
32    /// CREATE TABLE name (columns)
33    CreateTable(CreateTableQuery),
34    /// CREATE COLLECTION name KIND kind
35    CreateCollection(CreateCollectionQuery),
36    /// CREATE VECTOR name DIM n [METRIC metric]
37    CreateVector(CreateVectorQuery),
38    /// DROP TABLE name
39    DropTable(DropTableQuery),
40    /// DROP GRAPH name
41    DropGraph(DropGraphQuery),
42    /// DROP VECTOR name
43    DropVector(DropVectorQuery),
44    /// DROP DOCUMENT name
45    DropDocument(DropDocumentQuery),
46    /// DROP KV name
47    DropKv(DropKvQuery),
48    /// DROP COLLECTION name
49    DropCollection(DropCollectionQuery),
50    /// TRUNCATE [model] name
51    Truncate(TruncateQuery),
52    /// ALTER TABLE name ADD/DROP/RENAME COLUMN
53    AlterTable(AlterTableQuery),
54    /// CREATE BRANCH/TAG ref DDL
55    CreateVcsRef(CreateVcsRefQuery),
56    /// DROP BRANCH/TAG ref DDL
57    DropVcsRef(DropVcsRefQuery),
58    /// FORK STORE AS name [AT LSN n]
59    ForkStore(ForkStoreQuery),
60    /// PROMOTE FORK name
61    PromoteFork(PromoteForkQuery),
62    /// DROP FORK name
63    DropFork(DropForkQuery),
64    /// GRAPH subcommand (NEIGHBORHOOD, SHORTEST_PATH, etc.)
65    GraphCommand(GraphCommand),
66    /// SEARCH subcommand (SIMILAR, TEXT, HYBRID)
67    SearchCommand(SearchCommand),
68    /// ASK 'question' — RAG query with LLM synthesis
69    Ask(AskQuery),
70    /// CREATE INDEX name ON table (columns) USING type
71    CreateIndex(CreateIndexQuery),
72    /// DROP INDEX name ON table
73    DropIndex(DropIndexQuery),
74    /// Probabilistic data structure commands (HLL, SKETCH, FILTER)
75    ProbabilisticCommand(ProbabilisticCommand),
76    /// CREATE TIMESERIES name [RETENTION duration] [CHUNK_SIZE n]
77    CreateTimeSeries(CreateTimeSeriesQuery),
78    /// CREATE METRIC path TYPE kind ROLE role
79    CreateMetric(CreateMetricQuery),
80    /// ALTER METRIC path SET <ROLE|KIND|TYPE|PATH> <value>
81    AlterMetric(AlterMetricQuery),
82    /// CREATE SLO path ON metric_path TARGET t WINDOW d UNIT
83    CreateSlo(CreateSloQuery),
84    /// DROP TIMESERIES name
85    DropTimeSeries(DropTimeSeriesQuery),
86    /// CREATE QUEUE name [WORK|STANDARD|FIFO|FANOUT] [MAX_SIZE n] [PRIORITY] [WITH TTL duration]
87    CreateQueue(CreateQueueQuery),
88    /// ALTER QUEUE name SET MODE [FANOUT|WORK|STANDARD|FIFO]
89    AlterQueue(AlterQueueQuery),
90    /// DROP QUEUE name
91    DropQueue(DropQueueQuery),
92    /// Read-only queue projection: SELECT ... FROM QUEUE name
93    QueueSelect(QueueSelectQuery),
94    /// QUEUE subcommand (PUSH, POP, PEEK, LEN, PURGE, GROUP, READ, ACK, NACK)
95    QueueCommand(QueueCommand),
96    /// KV subcommand (PUT, GET, DELETE)
97    KvCommand(KvCommand),
98    /// CONFIG keyed command (PUT, GET, ROTATE, DELETE, HISTORY)
99    ConfigCommand(ConfigCommand),
100    /// CREATE TREE name IN collection ROOT ... MAX_CHILDREN n
101    CreateTree(CreateTreeQuery),
102    /// DROP TREE name IN collection
103    DropTree(DropTreeQuery),
104    /// TREE subcommand (INSERT, MOVE, DELETE, VALIDATE, REBALANCE)
105    TreeCommand(TreeCommand),
106    /// SET CONFIG key = value
107    SetConfig { key: String, value: Value },
108    /// SHOW CONFIG [prefix] [AS JSON|FORMAT JSON]
109    ShowConfig {
110        prefix: Option<String>,
111        as_json: bool,
112    },
113    /// SET SECRET key = value
114    SetSecret { key: String, value: Value },
115    /// DELETE SECRET key
116    DeleteSecret { key: String },
117    /// SHOW SECRET[S] [prefix]
118    ShowSecrets { prefix: Option<String> },
119    /// SET KV key = value
120    ///
121    /// Stores a plain (non-encrypted) user key-value entry in the KV
122    /// store — a sibling to `SET SECRET`, but backed by an independent
123    /// flat-map with no vault encryption. Gated by `kv:write` (#1602).
124    SetKv { key: String, value: Value },
125    /// DELETE KV key — remove a plain KV entry. Gated by `kv:write` (#1602).
126    DeleteKv { key: String },
127    /// `SET TENANT 'id'` / `SET TENANT = 'id'` / `RESET TENANT`
128    ///
129    /// Session-scoped multi-tenancy handle. Populates a per-connection
130    /// thread-local that `CURRENT_TENANT()` reads and that RLS
131    /// policies combine with via `USING (tenant_id = CURRENT_TENANT())`.
132    /// `None` clears the current tenant (RESET TENANT or SET TENANT
133    /// NULL). Unlike `SetConfig` this is *not* persisted to red_config —
134    /// it lives for the connection's lifetime only.
135    SetTenant(Option<String>),
136    /// `SHOW TENANT` — returns the thread-local tenant id (or NULL).
137    ShowTenant,
138    /// EXPLAIN ALTER FOR CREATE TABLE name (...) [FORMAT JSON]
139    ///
140    /// Pure read command that diffs the embedded `CREATE TABLE`
141    /// statement against the live `CollectionContract` of the
142    /// table with the same name and returns the `ALTER TABLE`
143    /// operations that would close the gap. Never executes
144    /// anything — output is text (default) or JSON depending on
145    /// the optional `FORMAT JSON` suffix. Powers the Purple
146    /// framework's migration generator and any other client that
147    /// wants reddb to own the schema-diff rules.
148    ExplainAlter(ExplainAlterQuery),
149    /// CREATE MIGRATION name [DEPENDS ON dep1, dep2] [BATCH n ROWS] [NO ROLLBACK] body
150    CreateMigration(CreateMigrationQuery),
151    /// APPLY MIGRATION name | APPLY MIGRATION *  [FOR TENANT id]
152    ApplyMigration(ApplyMigrationQuery),
153    /// ROLLBACK MIGRATION name
154    RollbackMigration(RollbackMigrationQuery),
155    /// EXPLAIN MIGRATION name
156    ExplainMigration(ExplainMigrationQuery),
157    /// EXPLAIN <statement>
158    ///
159    /// Plan-only wrapper: the executor must inspect the wrapped statement
160    /// without executing it.
161    Explain(ExplainQuery),
162    /// `EVENTS BACKFILL collection [WHERE pred] TO queue [LIMIT n]`.
163    EventsBackfill(EventsBackfillQuery),
164    /// `EVENTS BACKFILL STATUS collection` placeholder for the status slice.
165    EventsBackfillStatus { collection: String },
166    /// Transaction control: BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE, ROLLBACK TO.
167    ///
168    /// Phase 1.1 (PG parity): parser + dispatch are wired so clients (psql, JDBC, etc.)
169    /// can issue these statements without errors. Real isolation/atomicity semantics
170    /// arrive with Phase 2.3 MVCC. Until then statements behave as autocommit (each
171    /// DML is its own transaction); BEGIN/COMMIT/ROLLBACK return success but do NOT
172    /// provide rollback-on-failure guarantees across multiple statements.
173    TransactionControl(TxnControl),
174    /// Maintenance commands: VACUUM [FULL] [table], ANALYZE [table].
175    ///
176    /// Phase 1.2 (PG parity): `VACUUM` triggers segment/page flush + planner stats
177    /// refresh. `ANALYZE` refreshes planner statistics (histograms, null counts,
178    /// distinct estimates). Both accept an optional table target; omitting the
179    /// target iterates every collection.
180    MaintenanceCommand(MaintenanceCommand),
181    /// VCS working-set commands: CHECKPOINT, CHECKOUT, RESET, MERGE,
182    /// CHERRY PICK, REVERT, RESOLVE CONFLICT.
183    VcsCommand(VcsCommand),
184    /// `CREATE SCHEMA [IF NOT EXISTS] name`
185    ///
186    /// Phase 1.3 (PG parity): schemas are logical namespaces stored in
187    /// `red_config` under the key `schema.{name}`. Tables created inside a
188    /// schema use `schema.table` qualified names (collection name = "schema.table").
189    CreateSchema(CreateSchemaQuery),
190    /// `DROP SCHEMA [IF EXISTS] name [CASCADE]`
191    DropSchema(DropSchemaQuery),
192    /// `CREATE SEQUENCE [IF NOT EXISTS] name [START [WITH] n] [INCREMENT [BY] n]`
193    ///
194    /// Phase 1.3 (PG parity): sequences are 64-bit monotonic counters persisted
195    /// in `red_config` under the key `sequence.{name}`. Values are produced by
196    /// the scalar functions `nextval('name')` and `currval('name')`.
197    CreateSequence(CreateSequenceQuery),
198    /// `DROP SEQUENCE [IF EXISTS] name`
199    DropSequence(DropSequenceQuery),
200    /// `COPY table FROM 'path' [WITH ...]` — CSV import (Phase 1.5 PG parity).
201    ///
202    /// Supported options: `DELIMITER c`, `HEADER [true|false]`. Rows stream
203    /// into the named collection via the `CsvImporter`.
204    CopyFrom(CopyFromQuery),
205    /// `CREATE [OR REPLACE] [MATERIALIZED] VIEW [IF NOT EXISTS] name AS SELECT ...`
206    ///
207    /// Phase 2.1 (PG parity): views are stored as `view.{name}` entries in
208    /// `red_config`. Materialized views additionally allocate a slot in the
209    /// shared `MaterializedViewCache`; `REFRESH MATERIALIZED VIEW` re-runs
210    /// the underlying query and repopulates the cache.
211    CreateView(CreateViewQuery),
212    /// `DROP [MATERIALIZED] VIEW [IF EXISTS] name`
213    DropView(DropViewQuery),
214    /// `REFRESH MATERIALIZED VIEW name`
215    ///
216    /// Re-executes the view's query and writes the result into the cache.
217    RefreshMaterializedView(RefreshMaterializedViewQuery),
218    /// `CREATE POLICY name ON table [FOR action] [TO role] USING (filter)`
219    ///
220    /// Phase 2.5 (PG parity): row-level security policy definition.
221    /// Evaluated at read time — when the table has RLS enabled, all
222    /// matching policies for the current role are combined with OR and
223    /// AND-ed into the query's WHERE clause.
224    CreatePolicy(CreatePolicyQuery),
225    /// `DROP POLICY [IF EXISTS] name ON table`
226    DropPolicy(DropPolicyQuery),
227    /// `CREATE SERVER name FOREIGN DATA WRAPPER kind OPTIONS (...)`
228    /// (Phase 3.2 PG parity). Registers a named foreign-data-wrapper
229    /// instance in the runtime's `ForeignTableRegistry`.
230    CreateServer(CreateServerQuery),
231    /// `DROP SERVER [IF EXISTS] name [CASCADE]`
232    DropServer(DropServerQuery),
233    /// `CREATE FOREIGN TABLE name (cols) SERVER srv OPTIONS (...)`
234    /// (Phase 3.2 PG parity). Makes `name` resolvable as a foreign table
235    /// via the parent server's `ForeignDataWrapper`.
236    CreateForeignTable(CreateForeignTableQuery),
237    /// `DROP FOREIGN TABLE [IF EXISTS] name`
238    DropForeignTable(DropForeignTableQuery),
239    /// `GRANT { actions | ALL [PRIVILEGES] }
240    ///   ON { TABLE | SCHEMA | DATABASE | FUNCTION } object_list
241    ///   TO grant_principal_list
242    ///   [WITH GRANT OPTION]`
243    ///
244    /// Granular RBAC primitive layered on top of the legacy 3-role model.
245    /// See `crate::auth::privileges` for the resolution algorithm.
246    Grant(GrantStmt),
247    /// `REVOKE [GRANT OPTION FOR] { actions | ALL } ON … FROM …`
248    Revoke(RevokeStmt),
249    /// `ALTER USER name [VALID UNTIL 'ts'] [CONNECTION LIMIT n]
250    ///   [ENABLE | DISABLE] [SET search_path = ...]`
251    AlterUser(AlterUserStmt),
252    /// `CREATE USER [tenant.]name [WITH] PASSWORD 'plaintext' [ROLE read|write|admin]`
253    CreateUser(CreateUserStmt),
254    // ----- IAM policy DDL (Agent #28 / IAM kernel integration) -----
255    /// `CREATE POLICY '<id>' AS '<json>'` — installs an IAM policy
256    /// document in the AuthStore. Distinct from the RLS-flavoured
257    /// `CreatePolicy(CreatePolicyQuery)` above (which uses
258    /// `CREATE POLICY name ON table ...`); the parser disambiguates
259    /// at parse time by inspecting the token after the policy name.
260    CreateIamPolicy { id: String, json: String },
261    /// `DROP POLICY '<id>'` — removes an IAM policy and its
262    /// attachments.
263    DropIamPolicy { id: String },
264    /// `ATTACH POLICY '<id>' TO USER <name>` /
265    /// `ATTACH POLICY '<id>' TO GROUP <name>`.
266    AttachPolicy {
267        policy_id: String,
268        principal: PolicyPrincipalRef,
269    },
270    /// `DETACH POLICY '<id>' FROM USER <name>` /
271    /// `DETACH POLICY '<id>' FROM GROUP <name>`.
272    DetachPolicy {
273        policy_id: String,
274        principal: PolicyPrincipalRef,
275    },
276    /// `SHOW POLICIES [FOR USER <name> | FOR GROUP <name>]`.
277    ShowPolicies { filter: Option<PolicyPrincipalRef> },
278    /// `SHOW EFFECTIVE PERMISSIONS FOR <name> [ON <kind>:<name>]`.
279    ShowEffectivePermissions {
280        user: PolicyUserRef,
281        resource: Option<PolicyResourceRef>,
282    },
283    /// Exact rank of one row in a declared leaderboard ranking.
284    RankOf(RankOfQuery),
285    /// Approximate tail rank of one row in a declared leaderboard ranking.
286    ApproxRankOf(RankOfQuery),
287    /// Exact rank-ordered range in a declared leaderboard ranking.
288    RankRange(RankRangeQuery),
289    /// `SIMULATE <name> ACTION <verb> ON <kind>:<name>`.
290    SimulatePolicy {
291        user: PolicyUserRef,
292        action: String,
293        resource: PolicyResourceRef,
294    },
295    /// `LINT POLICY '<id>'` — fetch a stored policy from the
296    /// AuthStore and run the [`PolicyLinter`](crate::auth::policy_linter)
297    /// against its serialized JSON.
298    ///
299    /// `LINT POLICY JSON '<json>'` — lint the supplied JSON document
300    /// directly without consulting the AuthStore. Issue #710.
301    LintPolicy { source: LintPolicySource },
302    /// `MIGRATE POLICY MODE TO '<target>' [DRY RUN]` — switch the
303    /// install from the legacy_rbac fallback to strict policy_only
304    /// after running the pre-flight delta simulator. With `DRY RUN`,
305    /// only the delta is returned. Without it, the migration refuses
306    /// if the delta is non-empty and otherwise mutates the
307    /// enforcement mode. Issue #714.
308    MigratePolicyMode { target: String, dry_run: bool },
309}
310
311/// Source of the policy document being linted.
312#[derive(Debug, Clone, PartialEq, Eq)]
313pub enum LintPolicySource {
314    /// Fetch the document from the AuthStore by id.
315    Id(String),
316    /// Use the supplied JSON literal verbatim.
317    Json(String),
318}
319
320/// Tenant-qualified user reference for IAM policy SQL DDL.
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct PolicyUserRef {
323    pub tenant: Option<String>,
324    pub username: String,
325}
326
327/// Resource reference (`<kind>:<name>`) used in `SIMULATE` /
328/// `SHOW EFFECTIVE PERMISSIONS`.
329#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct PolicyResourceRef {
331    pub kind: String,
332    pub name: String,
333}
334
335/// Principal target for ATTACH / DETACH / SHOW POLICIES filter.
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub enum PolicyPrincipalRef {
338    User(PolicyUserRef),
339    Group(String),
340}
341
342/// `RANK OF <entity_id> IN <ranking>` canonical leaderboard read.
343///
344/// Redis-flavor `ZRANK <ranking> <entity_id>` desugars to this exact same
345/// shape, so it carries no execution semantics that the canonical rank read
346/// lacks.
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct RankOfQuery {
349    pub ranking: String,
350    pub entity_id: u64,
351}
352
353/// `RANK RANGE <lo> TO <hi> IN <ranking>` canonical leaderboard read.
354///
355/// Redis-flavor `ZRANGE <ranking> <start> <stop> [WITHSCORES]` desugars by
356/// translating Redis' zero-based inclusive offsets to the canonical one-based
357/// rank bounds.
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub struct RankRangeQuery {
360    pub ranking: String,
361    pub lo: u64,
362    pub hi: u64,
363}
364
365// ---------------------------------------------------------------------------
366// GRANT / REVOKE / ALTER USER AST
367// ---------------------------------------------------------------------------
368
369/// Object class targeted by a GRANT/REVOKE.
370#[derive(Debug, Clone, PartialEq, Eq)]
371pub enum GrantObjectKind {
372    Table,
373    Schema,
374    Database,
375    Function,
376}
377
378/// One target object in a `GRANT ... ON ... <object_list>` clause.
379///
380/// `name` follows the parser's standard `[schema.]object` shape; the
381/// optional `schema` is only populated for `Table` / `Function` and
382/// stays `None` when the user wrote a bare identifier.
383#[derive(Debug, Clone)]
384pub struct GrantObject {
385    pub schema: Option<String>,
386    pub name: String,
387}
388
389/// Principal target of a GRANT (i.e. the recipient).
390#[derive(Debug, Clone)]
391pub enum GrantPrincipalRef {
392    /// `TO username` — username may include an `@tenant` suffix.
393    User {
394        tenant: Option<String>,
395        name: String,
396    },
397    /// `TO PUBLIC`.
398    Public,
399    /// `TO GROUP groupname` (parsed today, enforcement deferred).
400    Group(String),
401}
402
403/// `GRANT` statement AST.
404#[derive(Debug, Clone)]
405pub struct GrantStmt {
406    /// Privilege keywords as the user typed them, normalised to upper
407    /// case. Matches the `Action` set in `crate::auth::privileges`. An
408    /// empty list together with `all = true` represents `ALL [PRIVILEGES]`.
409    pub actions: Vec<String>,
410    /// Optional column list — populates the AST for column-level
411    /// grants but enforcement is deferred (stretch goal).
412    pub columns: Option<Vec<String>>,
413    pub object_kind: GrantObjectKind,
414    pub objects: Vec<GrantObject>,
415    pub principals: Vec<GrantPrincipalRef>,
416    pub with_grant_option: bool,
417    /// `true` when the privilege list was `ALL [PRIVILEGES]`.
418    pub all: bool,
419}
420
421/// `REVOKE` statement AST.
422#[derive(Debug, Clone)]
423pub struct RevokeStmt {
424    pub actions: Vec<String>,
425    pub columns: Option<Vec<String>>,
426    pub object_kind: GrantObjectKind,
427    pub objects: Vec<GrantObject>,
428    pub principals: Vec<GrantPrincipalRef>,
429    /// `REVOKE GRANT OPTION FOR ...` — strips just the grant option,
430    /// keeping the underlying privilege.
431    pub grant_option_for: bool,
432    pub all: bool,
433}
434
435/// One attribute setting under `ALTER USER`.
436#[derive(Debug, Clone)]
437pub enum AlterUserAttribute {
438    ValidUntil(String),
439    ConnectionLimit(i64),
440    Enable,
441    Disable,
442    SetSearchPath(String),
443    AddGroup(String),
444    DropGroup(String),
445    /// Reset password (carry the new plaintext until the runtime
446    /// hands it to AuthStore::change_password). Out of scope for the
447    /// initial milestone — present so the parser can accept the
448    /// keyword without a follow-up grammar change.
449    Password(String),
450}
451
452/// `ALTER USER` statement AST.
453#[derive(Debug, Clone)]
454pub struct AlterUserStmt {
455    pub tenant: Option<String>,
456    pub username: String,
457    pub attributes: Vec<AlterUserAttribute>,
458}
459
460/// `CREATE USER` statement AST.
461#[derive(Debug, Clone)]
462pub struct CreateUserStmt {
463    pub tenant: Option<String>,
464    pub username: String,
465    pub password: String,
466    pub role: String,
467}
468
469#[derive(Debug, Clone)]
470pub struct CreateServerQuery {
471    pub name: String,
472    /// Wrapper kind declared in `FOREIGN DATA WRAPPER <kind>`.
473    pub wrapper: String,
474    /// Generic `(key 'value', ...)` option bag.
475    pub options: Vec<(String, String)>,
476    pub if_not_exists: bool,
477}
478
479#[derive(Debug, Clone)]
480pub struct DropServerQuery {
481    pub name: String,
482    pub if_exists: bool,
483    pub cascade: bool,
484}
485
486#[derive(Debug, Clone)]
487pub struct CreateForeignTableQuery {
488    pub name: String,
489    pub server: String,
490    pub columns: Vec<ForeignColumnDef>,
491    pub options: Vec<(String, String)>,
492    pub if_not_exists: bool,
493}
494
495#[derive(Debug, Clone)]
496pub struct ForeignColumnDef {
497    pub name: String,
498    pub data_type: String,
499    pub not_null: bool,
500}
501
502#[derive(Debug, Clone)]
503pub struct DropForeignTableQuery {
504    pub name: String,
505    pub if_exists: bool,
506}
507
508/// Row-level security policy definition.
509#[derive(Debug, Clone)]
510pub struct CreatePolicyQuery {
511    pub name: String,
512    pub table: String,
513    /// Which action this policy gates. `None` = `ALL` (applies to all four).
514    pub action: Option<PolicyAction>,
515    /// Role the policy applies to. `None` = all roles.
516    pub role: Option<String>,
517    /// Boolean predicate the row must satisfy.
518    pub using: Box<Filter>,
519    /// Entity kind this policy targets (Phase 2.5.5 RLS universal).
520    /// `CREATE POLICY p ON t ...` defaults to `Table`; writing
521    /// `ON NODES OF g` / `ON VECTORS OF v` / `ON MESSAGES OF q` /
522    /// `ON POINTS OF ts` / `ON EDGES OF g` targets the matching
523    /// non-tabular kind. The evaluator filters polices by kind so
524    /// a graph policy only gates graph reads, vector policy only
525    /// gates vector reads, etc.
526    pub target_kind: PolicyTargetKind,
527}
528
529/// Which flavour of entity a policy governs (Phase 2.5.5).
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
531pub enum PolicyTargetKind {
532    Table,
533    Nodes,
534    Edges,
535    Vectors,
536    Messages,
537    Points,
538    Documents,
539}
540
541impl PolicyTargetKind {
542    /// Lowercase identifier for UX — used in messages and the
543    /// `red_config.rls.policies.*` persistence key.
544    pub fn as_ident(&self) -> &'static str {
545        match self {
546            Self::Table => "table",
547            Self::Nodes => "nodes",
548            Self::Edges => "edges",
549            Self::Vectors => "vectors",
550            Self::Messages => "messages",
551            Self::Points => "points",
552            Self::Documents => "documents",
553        }
554    }
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
558pub enum PolicyAction {
559    Select,
560    Insert,
561    Update,
562    Delete,
563}
564
565#[derive(Debug, Clone, PartialEq, Eq)]
566pub struct DropPolicyQuery {
567    pub name: String,
568    pub table: String,
569    pub if_exists: bool,
570}
571
572#[derive(Debug, Clone)]
573pub struct CreateViewQuery {
574    pub name: String,
575    /// Parsed `SELECT ...` body. Stored as a boxed `QueryExpr` so the
576    /// runtime can substitute the tree directly when a query references
577    /// this view (no re-parsing per read).
578    pub query: Box<QueryExpr>,
579    pub materialized: bool,
580    pub if_not_exists: bool,
581    /// `CREATE OR REPLACE VIEW` — overwrites any existing definition.
582    pub or_replace: bool,
583    /// `REFRESH EVERY <duration>` clause — only valid on materialized
584    /// views. When set, a background scheduler ticks the view at this
585    /// cadence. `None` means refresh-on-demand only (slice 9 behaviour).
586    /// Issue #583 slice 10.
587    pub refresh_every_ms: Option<u64>,
588    /// `WITH RETENTION <duration>` clause — only valid on materialized
589    /// views (issue #584 slice 12). Opts the view's backing storage
590    /// into a retention policy independent of the source. Persisted on
591    /// the view definition; the physical sweep is applied to the
592    /// MV's backing rows once slice-9's row-storage follow-up lands.
593    /// `None` means the view is unaffected by source retention.
594    pub retention_duration_ms: Option<u64>,
595}
596
597#[derive(Debug, Clone, PartialEq, Eq)]
598pub struct DropViewQuery {
599    pub name: String,
600    pub materialized: bool,
601    pub if_exists: bool,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq)]
605pub struct RefreshMaterializedViewQuery {
606    pub name: String,
607}
608
609#[derive(Debug, Clone, PartialEq, Eq)]
610pub struct CopyFromQuery {
611    pub table: String,
612    pub path: String,
613    pub format: CopyFormat,
614    pub delimiter: Option<char>,
615    pub has_header: bool,
616}
617
618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
619pub enum CopyFormat {
620    Csv,
621}
622
623#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct CreateSchemaQuery {
625    pub name: String,
626    pub if_not_exists: bool,
627}
628
629#[derive(Debug, Clone, PartialEq, Eq)]
630pub struct DropSchemaQuery {
631    pub name: String,
632    pub if_exists: bool,
633    pub cascade: bool,
634}
635
636#[derive(Debug, Clone, PartialEq, Eq)]
637pub struct CreateSequenceQuery {
638    pub name: String,
639    pub if_not_exists: bool,
640    /// First value produced by `nextval`. Default 1.
641    pub start: i64,
642    /// Added to the current value on each `nextval`. Default 1.
643    pub increment: i64,
644}
645
646#[derive(Debug, Clone, PartialEq, Eq)]
647pub struct DropSequenceQuery {
648    pub name: String,
649    pub if_exists: bool,
650}
651
652/// Transaction-control statement variants. See [`QueryExpr::TransactionControl`].
653#[derive(Debug, Clone, PartialEq, Eq)]
654pub enum TxnControl {
655    /// `BEGIN [WORK | TRANSACTION]`, `START TRANSACTION`
656    Begin(Option<IsolationLevel>),
657    /// `COMMIT [WORK | TRANSACTION]`, `END`
658    Commit,
659    /// `ROLLBACK [WORK | TRANSACTION]`
660    Rollback,
661    /// `SAVEPOINT name`
662    Savepoint(String),
663    /// `RELEASE [SAVEPOINT] name`
664    ReleaseSavepoint(String),
665    /// `ROLLBACK TO [SAVEPOINT] name`
666    RollbackToSavepoint(String),
667}
668
669/// Transaction isolation level requested by `BEGIN ISOLATION LEVEL ...`.
670#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
671pub enum IsolationLevel {
672    /// `READ UNCOMMITTED`
673    ReadUncommitted,
674    /// `READ COMMITTED`
675    ReadCommitted,
676    /// `REPEATABLE READ` / `SNAPSHOT`
677    #[default]
678    SnapshotIsolation,
679    /// `SERIALIZABLE`
680    Serializable,
681}
682
683/// Maintenance command variants. See [`QueryExpr::MaintenanceCommand`].
684#[derive(Debug, Clone, PartialEq, Eq)]
685pub enum MaintenanceCommand {
686    /// `VACUUM [FULL] [table]`
687    ///
688    /// Triggers segment compaction and planner stats refresh. `FULL` additionally
689    /// forces a full pager sync. Target `None` applies to every collection.
690    Vacuum { target: Option<String>, full: bool },
691    /// `ANALYZE [table]`
692    ///
693    /// Refreshes planner statistics (histogram, distinct estimates, null counts).
694    /// Target `None` re-analyzes every collection.
695    Analyze { target: Option<String> },
696}
697
698#[derive(Debug, Clone, PartialEq, Eq)]
699pub enum VcsCommand {
700    Checkpoint {
701        message: String,
702        author: Option<String>,
703    },
704    Checkout {
705        target: String,
706    },
707    Reset {
708        mode: VcsResetMode,
709        target: String,
710    },
711    Merge {
712        branch: String,
713    },
714    CherryPick {
715        commit: String,
716    },
717    Revert {
718        commit: String,
719    },
720    ResolveConflict {
721        key: String,
722        resolution: VcsConflictResolution,
723    },
724}
725
726#[derive(Debug, Clone, Copy, PartialEq, Eq)]
727pub enum VcsResetMode {
728    Hard,
729    Soft,
730    Mixed,
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
734pub enum VcsConflictResolution {
735    Ours,
736    Theirs,
737}
738
739/// AST node for `EXPLAIN ALTER FOR <CreateTableStmt> [FORMAT JSON]`.
740///
741/// `target` carries the CREATE TABLE structure exactly as the
742/// parser produces it for a regular CREATE — full reuse of
743/// `parse_create_table_body`. `format` determines whether the
744/// executor emits a `ALTER TABLE …;`-flavored text payload
745/// (the default — copy-paste friendly into the REPL) or a
746/// structured JSON object (machine-friendly).
747#[derive(Debug, Clone)]
748pub struct ExplainAlterQuery {
749    pub target: CreateTableQuery,
750    pub format: ExplainFormat,
751}
752
753/// Output format requested for an `EXPLAIN ALTER` command.
754#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
755pub enum ExplainFormat {
756    /// Plain SQL text — `ALTER TABLE …;` lines plus header
757    /// comments and rename hints. Default; copy-paste friendly.
758    #[default]
759    Sql,
760    /// Structured JSON object with `operations`,
761    /// `rename_candidates`, `summary`. Machine-friendly for
762    /// driver code (Purple migration generator, dashboards,
763    /// CLI tools).
764    Json,
765}
766
767#[derive(Debug, Clone, PartialEq)]
768pub struct CreateMigrationQuery {
769    pub name: String,
770    pub body: String,
771    pub depends_on: Vec<String>,
772    pub batch_size: Option<u64>,
773    pub no_rollback: bool,
774}
775
776#[derive(Debug, Clone, PartialEq)]
777pub struct ApplyMigrationQuery {
778    pub target: ApplyMigrationTarget,
779    pub for_tenant: Option<String>,
780}
781
782#[derive(Debug, Clone, PartialEq)]
783pub enum ApplyMigrationTarget {
784    Named(String),
785    All,
786}
787
788#[derive(Debug, Clone, PartialEq)]
789pub struct RollbackMigrationQuery {
790    pub name: String,
791}
792
793#[derive(Debug, Clone, PartialEq)]
794pub struct ExplainMigrationQuery {
795    pub name: String,
796}
797
798#[derive(Debug, Clone)]
799pub struct ExplainQuery {
800    pub inner: Box<QueryExpr>,
801}
802
803/// Probabilistic data structure commands
804#[derive(Debug, Clone)]
805pub enum ProbabilisticCommand {
806    // HyperLogLog
807    CreateHll {
808        name: String,
809        precision: u8,
810        if_not_exists: bool,
811    },
812    HllAdd {
813        name: String,
814        elements: Vec<String>,
815    },
816    HllCount {
817        names: Vec<String>,
818    },
819    HllMerge {
820        dest: String,
821        sources: Vec<String>,
822    },
823    HllInfo {
824        name: String,
825    },
826    DropHll {
827        name: String,
828        if_exists: bool,
829    },
830
831    // Count-Min Sketch (Fase 7)
832    CreateSketch {
833        name: String,
834        width: usize,
835        depth: usize,
836        if_not_exists: bool,
837    },
838    SketchAdd {
839        name: String,
840        element: String,
841        count: u64,
842    },
843    SketchCount {
844        name: String,
845        element: String,
846    },
847    SketchMerge {
848        dest: String,
849        sources: Vec<String>,
850    },
851    SketchInfo {
852        name: String,
853    },
854    DropSketch {
855        name: String,
856        if_exists: bool,
857    },
858
859    // Cuckoo Filter (Fase 8)
860    CreateFilter {
861        name: String,
862        capacity: usize,
863        if_not_exists: bool,
864    },
865    FilterAdd {
866        name: String,
867        element: String,
868    },
869    FilterCheck {
870        name: String,
871        element: String,
872    },
873    FilterDelete {
874        name: String,
875        element: String,
876    },
877    FilterCount {
878        name: String,
879    },
880    FilterInfo {
881        name: String,
882    },
883    DropFilter {
884        name: String,
885        if_exists: bool,
886    },
887}
888
889/// Default H3 resolution for `CREATE INDEX ... USING H3` when no
890/// explicit resolution is supplied. Resolution 9 ≈ ~150 m hex edge
891/// (PRD #1574 slice 2, #1576).
892pub const DEFAULT_H3_RESOLUTION: u8 = 9;
893
894/// Index type for CREATE INDEX ... USING <type>
895#[derive(Debug, Clone, PartialEq, Eq)]
896pub enum IndexMethod {
897    BTree,
898    Hash,
899    Bitmap,
900    /// Generic spatial index request (`USING SPATIAL`) — the engine picks
901    /// the default spatial backend. As of PRD #1574 slice 4 (#1578) that
902    /// default is the disk-resident H3 index (RAM stays O(working set)),
903    /// not the retired in-RAM R-tree.
904    Spatial,
905    /// H3 hexagonal spatial index: the `(lat, lon)` geo column is
906    /// encoded to a single H3 cell-id `u64` (at `resolution`) and stored
907    /// in the existing disk-paged B-tree (PRD #1574 slice 2, #1576).
908    H3 {
909        resolution: u8,
910    },
911}
912
913impl fmt::Display for IndexMethod {
914    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
915        match self {
916            Self::BTree => write!(f, "BTREE"),
917            Self::Hash => write!(f, "HASH"),
918            Self::Bitmap => write!(f, "BITMAP"),
919            Self::Spatial => write!(f, "SPATIAL"),
920            Self::H3 { .. } => write!(f, "H3"),
921        }
922    }
923}
924
925/// CREATE INDEX [UNIQUE] [IF NOT EXISTS] name ON table (col1, col2, ...) [USING method]
926#[derive(Debug, Clone)]
927pub struct CreateIndexQuery {
928    pub name: String,
929    pub table: String,
930    pub columns: Vec<String>,
931    pub method: IndexMethod,
932    pub unique: bool,
933    pub if_not_exists: bool,
934}
935
936/// DROP INDEX [IF EXISTS] name ON table
937#[derive(Debug, Clone)]
938pub struct DropIndexQuery {
939    pub name: String,
940    pub table: String,
941    pub if_exists: bool,
942}
943
944/// ASK 'question' [USING provider] [MODEL 'model'] [DEPTH n] [LIMIT n] [MIN_SCORE x]
945///                [COLLECTION col] [TEMPERATURE x] [SEED n] [STRICT ON|OFF] [STREAM]
946///                [CACHE TTL '5m' | NOCACHE] [AS RQL]
947///
948/// `temperature` and `seed` are per-query overrides resolved by the
949/// `DeterminismDecider` (issue #400). The parser merely surfaces the
950/// requested values; capability-based dropping happens at decide time.
951#[derive(Debug, Clone)]
952pub struct AskQuery {
953    /// `EXPLAIN ASK '...'` returns the retrieval/provider/cost plan
954    /// without making the LLM call.
955    pub explain: bool,
956    pub question: String,
957    /// Optional `$N` / `?` parameter slot for the question text.
958    pub question_param: Option<usize>,
959    pub provider: Option<String>,
960    pub model: Option<String>,
961    pub depth: Option<usize>,
962    pub limit: Option<usize>,
963    pub min_score: Option<f32>,
964    pub collection: Option<String>,
965    /// Per-query temperature override (`ASK '...' TEMPERATURE 0.7`).
966    /// `None` means fall back to `ask.default_temperature`.
967    pub temperature: Option<f32>,
968    /// Per-query seed override (`ASK '...' SEED 42`). `None` means the
969    /// decider derives one from `hash(question + sources_fingerprint)`.
970    pub seed: Option<u64>,
971    /// Strict citation validation is on by default. `STRICT OFF` keeps
972    /// citation diagnostics as warnings and skips retry/error handling.
973    pub strict: bool,
974    /// HTTP-only SSE response requested via `ASK '...' STREAM`.
975    pub stream: bool,
976    /// Per-query answer-cache override.
977    pub cache: AskCacheClause,
978    /// `ASK '...' PLAN` returns the typed plan (routed intent + candidate
979    /// query) without executing the candidate and without the synthesis
980    /// call (ADR 0068, #1751). The `EXECUTE` and `AS RQL` clauses were
981    /// removed in the same clean break: read-only candidates auto-execute
982    /// by default, and `PLAN` is the inspection-only surface.
983    pub plan_only: bool,
984    /// Per-query plan-step budget request (`ASK '...' STEPS N`). Clamped to
985    /// `red.config.ai.ask.max_plan_steps` at planning time and never exceeds
986    /// it; `None` falls back to the configured cap.
987    pub steps: Option<usize>,
988}
989
990#[derive(Debug, Clone, PartialEq, Eq, Default)]
991pub enum AskCacheClause {
992    #[default]
993    Default,
994    CacheTtl(String),
995    NoCache,
996}
997
998impl QueryExpr {
999    /// Create a table query
1000    pub fn table(name: &str) -> TableQueryBuilder {
1001        TableQueryBuilder::new(name)
1002    }
1003
1004    /// Create a graph query
1005    pub fn graph() -> GraphQueryBuilder {
1006        GraphQueryBuilder::new()
1007    }
1008
1009    /// Create a path query
1010    pub fn path(from: NodeSelector, to: NodeSelector) -> PathQueryBuilder {
1011        PathQueryBuilder::new(from, to)
1012    }
1013}
1014
1015// ============================================================================
1016// Table Query
1017// ============================================================================
1018
1019/// Table query: SELECT columns FROM table WHERE filter ORDER BY ... LIMIT ...
1020#[derive(Debug, Clone)]
1021pub struct TableQuery {
1022    /// Table name. Legacy slot — still populated even when `source`
1023    /// is set to a subquery so existing call sites that read
1024    /// `query.table.as_str()` keep compiling. When `source` is
1025    /// `Some(TableSource::Subquery(…))`, this field holds a synthetic
1026    /// sentinel name (`"__subq_NNNN"`) that runtime code must never
1027    /// resolve against the real schema registry.
1028    pub table: String,
1029    /// Fase 2 Week 3: structured table source. `None` means the
1030    /// legacy `table` field is authoritative. `Some(Name)` is the
1031    /// same information as `table` but in typed form. `Some(Subquery)`
1032    /// wires a `(SELECT …) AS alias` in a FROM position — the Fase
1033    /// 1.7 unlock.
1034    pub source: Option<TableSource>,
1035    /// Optional table alias
1036    pub alias: Option<String>,
1037    /// Canonical SQL select list.
1038    pub select_items: Vec<SelectItem>,
1039    /// Columns to select (empty = all)
1040    pub columns: Vec<Projection>,
1041    /// Canonical SQL WHERE clause.
1042    pub where_expr: Option<super::Expr>,
1043    /// Filter condition
1044    pub filter: Option<Filter>,
1045    /// Canonical SQL GROUP BY items.
1046    pub group_by_exprs: Vec<super::Expr>,
1047    /// GROUP BY fields
1048    pub group_by: Vec<String>,
1049    /// Canonical SQL HAVING clause.
1050    pub having_expr: Option<super::Expr>,
1051    /// HAVING filter (applied after grouping)
1052    pub having: Option<Filter>,
1053    /// Order by clauses
1054    pub order_by: Vec<OrderByClause>,
1055    /// Limit
1056    pub limit: Option<u64>,
1057    /// User-supplied-parameter slot for `LIMIT $N`. Set by the parser
1058    /// when the LIMIT clause references `$N`/`?` instead of a literal;
1059    /// cleared by the binder (`user_params::bind`) after substituting
1060    /// the parameter into `limit`. Mirrors the `limit_param` slot on
1061    /// `SearchCommand` variants — see #361 slice 11.
1062    pub limit_param: Option<usize>,
1063    /// Offset
1064    pub offset: Option<u64>,
1065    /// User-supplied-parameter slot for `OFFSET $N`. Same lifecycle as
1066    /// `limit_param`. See #361 slice 11.
1067    pub offset_param: Option<usize>,
1068    /// WITH EXPAND options (graph traversal, cross-ref following)
1069    pub expand: Option<ExpandOptions>,
1070    /// Time-travel anchor. When present the executor resolves this
1071    /// to an MVCC xid and evaluates the query against that snapshot
1072    /// instead of the current one. Mirrors git's `AS OF` semantics.
1073    pub as_of: Option<AsOfClause>,
1074    /// `SESSIONIZE BY <actor> GAP <duration> [ORDER BY <ts>]` operator
1075    /// (issue #585 slice 8). When present, the executor annotates each
1076    /// result row with a `session_id` column. `actor_col` / `gap_ms`
1077    /// may be `None` when the source collection's descriptor (slice 1
1078    /// `SESSION_KEY` / `SESSION_GAP`) supplies the defaults; one
1079    /// without the other resolved at execution time is the typed
1080    /// `MissingSessionKey` error.
1081    pub sessionize: Option<SessionizeClause>,
1082    /// `SELECT DISTINCT` projection quantifier. When `true` the executor
1083    /// deduplicates the projected output row-set (over the projected
1084    /// columns) before ORDER BY / LIMIT. `DISTINCT` inside an aggregate
1085    /// argument (`COUNT(DISTINCT x)`) is unrelated and lives on the
1086    /// aggregate call, not here.
1087    pub distinct: bool,
1088}
1089
1090/// `SESSIONIZE BY <actor_col> GAP <duration> [ORDER BY <ts_col>]`.
1091#[derive(Debug, Clone, Default)]
1092pub struct SessionizeClause {
1093    /// Explicit `BY <ident>`. `None` means "default from descriptor's
1094    /// `SESSION_KEY`" — resolved at execution time.
1095    pub actor_col: Option<String>,
1096    /// Explicit `GAP <duration>` in milliseconds. `None` means
1097    /// "default from descriptor's `SESSION_GAP`".
1098    pub gap_ms: Option<u64>,
1099    /// Explicit `ORDER BY <ident>` immediately after `GAP`. When
1100    /// `None` the executor falls back to the collection's timestamp
1101    /// column (the same resolution as `retention_filter`).
1102    pub order_col: Option<String>,
1103}
1104
1105/// Source spec for `AS OF` — parsed form sits in `TableQuery`, then
1106/// `vcs_resolve_as_of` turns it into an MVCC xid at execute time.
1107#[derive(Debug, Clone)]
1108pub enum AsOfClause {
1109    /// Explicit commit hash literal: `AS OF COMMIT '<hex>'`.
1110    Commit(String),
1111    /// Branch or ref: `AS OF BRANCH 'main'` or `AS OF 'refs/heads/main'`.
1112    Branch(String),
1113    /// Tag: `AS OF TAG 'v1.0'`.
1114    Tag(String),
1115    /// Unix epoch milliseconds: `AS OF TIMESTAMP 1710000000000`.
1116    TimestampMs(i64),
1117    /// Raw MVCC snapshot xid: `AS OF SNAPSHOT 12345`.
1118    Snapshot(u64),
1119}
1120
1121/// Structured FROM source for a `TableQuery`. Additive alongside the
1122/// legacy `TableQuery.table: String` slot — callers that understand
1123/// this type can branch on subqueries; callers that only read `table`
1124/// fall back to the synthetic sentinel name and, for subqueries,
1125/// produce an "unknown table" error until they migrate.
1126#[derive(Debug, Clone)]
1127pub enum TableSource {
1128    /// Plain table reference — equivalent to the legacy `String` form.
1129    Name(String),
1130    /// A subquery in FROM position: `FROM (SELECT …) AS alias`.
1131    Subquery(Box<QueryExpr>),
1132    /// A table-valued function call in FROM position, e.g.
1133    /// `FROM components(g)` (issue #795). `name` is the function
1134    /// identifier; `args` are its positional identifier arguments;
1135    /// `named_args` are `key => <f64>` named arguments such as
1136    /// `louvain(g, resolution => 0.5)` (issue #796), preserved in source
1137    /// order. Positional args always precede named args.
1138    Function {
1139        name: String,
1140        args: Vec<String>,
1141        named_args: Vec<(String, f64)>,
1142    },
1143    /// A graph-analytics table-valued function whose graph is supplied
1144    /// inline as two subqueries instead of a graph-collection reference
1145    /// (issue #799), e.g.
1146    /// `components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))`.
1147    ///
1148    /// Structurally distinct from `Function` so the executor can tell the
1149    /// inline form from the graph-collection form. `nodes`/`edges` are the
1150    /// two materialization subqueries (the first column of `nodes` is the
1151    /// node id; the first two-or-three columns of `edges` are
1152    /// `(source, target [, weight])`). `named_args` carries any remaining
1153    /// numeric named arguments (e.g. `resolution => 0.5`).
1154    InlineGraphFunction {
1155        name: String,
1156        nodes: Box<QueryExpr>,
1157        edges: Box<QueryExpr>,
1158        named_args: Vec<(String, f64)>,
1159    },
1160}
1161
1162/// Options for WITH EXPAND clause on SELECT queries.
1163#[derive(Debug, Clone, Default)]
1164pub struct ExpandOptions {
1165    /// Expand via graph edges (WITH EXPAND GRAPH)
1166    pub graph: bool,
1167    /// Graph expansion depth (DEPTH n)
1168    pub graph_depth: usize,
1169    /// Expand via cross-references (WITH EXPAND CROSS_REFS)
1170    pub cross_refs: bool,
1171    /// Index hint from the optimizer (which index to prefer for this query)
1172    pub index_hint: Option<reddb_types::index_hint::IndexHint>,
1173}
1174
1175impl TableQuery {
1176    /// Create a new table query
1177    pub fn new(table: &str) -> Self {
1178        Self {
1179            table: table.to_string(),
1180            source: None,
1181            alias: None,
1182            select_items: Vec::new(),
1183            columns: Vec::new(),
1184            where_expr: None,
1185            filter: None,
1186            group_by_exprs: Vec::new(),
1187            group_by: Vec::new(),
1188            having_expr: None,
1189            having: None,
1190            order_by: Vec::new(),
1191            limit: None,
1192            limit_param: None,
1193            offset: None,
1194            offset_param: None,
1195            expand: None,
1196            as_of: None,
1197            sessionize: None,
1198            distinct: false,
1199        }
1200    }
1201
1202    /// Create a TableQuery that wraps a subquery in FROM position.
1203    /// The legacy `table` slot holds a synthetic sentinel so code that
1204    /// only reads `table.as_str()` errors loudly with a
1205    /// recognisable marker instead of silently treating it as a
1206    /// real collection.
1207    pub fn from_subquery(subquery: QueryExpr, alias: Option<String>) -> Self {
1208        let sentinel = match &alias {
1209            Some(a) => format!("__subq_{a}"),
1210            None => "__subq_anon".to_string(),
1211        };
1212        Self {
1213            table: sentinel,
1214            source: Some(TableSource::Subquery(Box::new(subquery))),
1215            alias,
1216            select_items: Vec::new(),
1217            columns: Vec::new(),
1218            where_expr: None,
1219            filter: None,
1220            group_by_exprs: Vec::new(),
1221            group_by: Vec::new(),
1222            having_expr: None,
1223            having: None,
1224            order_by: Vec::new(),
1225            limit: None,
1226            limit_param: None,
1227            offset: None,
1228            offset_param: None,
1229            expand: None,
1230            as_of: None,
1231            sessionize: None,
1232            distinct: false,
1233        }
1234    }
1235}
1236
1237/// Canonical SQL select item for table queries.
1238#[derive(Debug, Clone, PartialEq)]
1239pub enum SelectItem {
1240    Wildcard,
1241    Expr {
1242        expr: super::Expr,
1243        alias: Option<String>,
1244    },
1245}
1246
1247// ============================================================================
1248// Graph Query
1249// ============================================================================
1250
1251/// Graph query: MATCH pattern WHERE filter RETURN projection
1252#[derive(Debug, Clone)]
1253pub struct GraphQuery {
1254    /// Optional outer alias when used as a join source
1255    pub alias: Option<String>,
1256    /// Graph pattern to match
1257    pub pattern: GraphPattern,
1258    /// Filter condition
1259    pub filter: Option<Filter>,
1260    /// Return projections
1261    pub return_: Vec<Projection>,
1262    /// Optional row limit
1263    pub limit: Option<u64>,
1264}
1265
1266impl GraphQuery {
1267    /// Create a new graph query
1268    pub fn new(pattern: GraphPattern) -> Self {
1269        Self {
1270            alias: None,
1271            pattern,
1272            filter: None,
1273            return_: Vec::new(),
1274            limit: None,
1275        }
1276    }
1277
1278    /// Set outer alias
1279    pub fn alias(mut self, alias: &str) -> Self {
1280        self.alias = Some(alias.to_string());
1281        self
1282    }
1283}
1284
1285/// Graph pattern: collection of node and edge patterns
1286#[derive(Debug, Clone, Default)]
1287pub struct GraphPattern {
1288    /// Node patterns
1289    pub nodes: Vec<NodePattern>,
1290    /// Edge patterns connecting nodes
1291    pub edges: Vec<EdgePattern>,
1292}
1293
1294impl GraphPattern {
1295    /// Create an empty pattern
1296    pub fn new() -> Self {
1297        Self::default()
1298    }
1299
1300    /// Add a node pattern
1301    pub fn node(mut self, pattern: NodePattern) -> Self {
1302        self.nodes.push(pattern);
1303        self
1304    }
1305
1306    /// Add an edge pattern
1307    pub fn edge(mut self, pattern: EdgePattern) -> Self {
1308        self.edges.push(pattern);
1309        self
1310    }
1311}
1312
1313/// Node pattern: (alias:Type {properties})
1314#[derive(Debug, Clone)]
1315pub struct NodePattern {
1316    /// Variable alias for this node
1317    pub alias: String,
1318    /// Optional label filter. Stored as the user-supplied label string so
1319    /// the parser is registry-free; executors resolve it against the live
1320    /// [`crate::storage::engine::graph_store::LabelRegistry`].
1321    pub node_label: Option<String>,
1322    /// Property filters
1323    pub properties: Vec<PropertyFilter>,
1324}
1325
1326impl NodePattern {
1327    /// Create a new node pattern
1328    pub fn new(alias: &str) -> Self {
1329        Self {
1330            alias: alias.to_string(),
1331            node_label: None,
1332            properties: Vec::new(),
1333        }
1334    }
1335
1336    /// Set the label filter (string form — preferred).
1337    pub fn of_label(mut self, label: impl Into<String>) -> Self {
1338        self.node_label = Some(label.into());
1339        self
1340    }
1341
1342    /// Add property filter
1343    pub fn with_property(mut self, name: &str, op: CompareOp, value: Value) -> Self {
1344        self.properties.push(PropertyFilter {
1345            name: name.to_string(),
1346            op,
1347            value,
1348        });
1349        self
1350    }
1351}
1352
1353/// Edge pattern: -[alias:Type*min..max]->
1354#[derive(Debug, Clone)]
1355pub struct EdgePattern {
1356    /// Optional alias for this edge
1357    pub alias: Option<String>,
1358    /// Source node alias
1359    pub from: String,
1360    /// Target node alias
1361    pub to: String,
1362    /// Optional label filter (user-supplied string).
1363    pub edge_label: Option<String>,
1364    /// Edge direction
1365    pub direction: EdgeDirection,
1366    /// Minimum hops (for variable-length patterns)
1367    pub min_hops: u32,
1368    /// Maximum hops (for variable-length patterns)
1369    pub max_hops: u32,
1370}
1371
1372impl EdgePattern {
1373    /// Create a new edge pattern
1374    pub fn new(from: &str, to: &str) -> Self {
1375        Self {
1376            alias: None,
1377            from: from.to_string(),
1378            to: to.to_string(),
1379            edge_label: None,
1380            direction: EdgeDirection::Outgoing,
1381            min_hops: 1,
1382            max_hops: 1,
1383        }
1384    }
1385
1386    /// Set label filter (string form — preferred).
1387    pub fn of_label(mut self, label: impl Into<String>) -> Self {
1388        self.edge_label = Some(label.into());
1389        self
1390    }
1391
1392    /// Set direction
1393    pub fn direction(mut self, dir: EdgeDirection) -> Self {
1394        self.direction = dir;
1395        self
1396    }
1397
1398    /// Set hop range for variable-length patterns
1399    pub fn hops(mut self, min: u32, max: u32) -> Self {
1400        self.min_hops = min;
1401        self.max_hops = max;
1402        self
1403    }
1404
1405    /// Set alias
1406    pub fn alias(mut self, alias: &str) -> Self {
1407        self.alias = Some(alias.to_string());
1408        self
1409    }
1410}
1411
1412/// Edge direction
1413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1414pub enum EdgeDirection {
1415    /// Outgoing: (a)-[r]->(b)
1416    Outgoing,
1417    /// Incoming: (a)<-[r]-(b)
1418    Incoming,
1419    /// Both: (a)-[r]-(b)
1420    Both,
1421}
1422
1423/// Property filter: name op value
1424#[derive(Debug, Clone)]
1425pub struct PropertyFilter {
1426    pub name: String,
1427    pub op: CompareOp,
1428    pub value: Value,
1429}
1430
1431// ============================================================================
1432// Join Query
1433// ============================================================================
1434
1435/// Join query: combines table and graph queries
1436#[derive(Debug, Clone)]
1437pub struct JoinQuery {
1438    /// Left side (typically table)
1439    pub left: Box<QueryExpr>,
1440    /// Right side (typically graph)
1441    pub right: Box<QueryExpr>,
1442    /// Join type
1443    pub join_type: JoinType,
1444    /// Join condition
1445    pub on: JoinCondition,
1446    /// Post-join filter condition
1447    pub filter: Option<Filter>,
1448    /// Post-join ordering
1449    pub order_by: Vec<OrderByClause>,
1450    /// Post-join limit
1451    pub limit: Option<u64>,
1452    /// Post-join offset
1453    pub offset: Option<u64>,
1454    /// Canonical SQL RETURN projection.
1455    pub return_items: Vec<SelectItem>,
1456    /// Post-join projection
1457    pub return_: Vec<Projection>,
1458}
1459
1460impl JoinQuery {
1461    /// Create a new join query
1462    pub fn new(left: QueryExpr, right: QueryExpr, on: JoinCondition) -> Self {
1463        Self {
1464            left: Box::new(left),
1465            right: Box::new(right),
1466            join_type: JoinType::Inner,
1467            on,
1468            filter: None,
1469            order_by: Vec::new(),
1470            limit: None,
1471            offset: None,
1472            return_items: Vec::new(),
1473            return_: Vec::new(),
1474        }
1475    }
1476
1477    /// Set join type
1478    pub fn join_type(mut self, jt: JoinType) -> Self {
1479        self.join_type = jt;
1480        self
1481    }
1482}
1483
1484/// Join type
1485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1486pub enum JoinType {
1487    /// Inner join — only matching pairs emitted
1488    Inner,
1489    /// Left outer join — every left row, matched or padded with nulls on the right
1490    LeftOuter,
1491    /// Right outer join — every right row, matched or padded with nulls on the left
1492    RightOuter,
1493    /// Full outer join — LeftOuter ∪ RightOuter, each unmatched side padded
1494    FullOuter,
1495    /// Cross join — Cartesian product, no predicate
1496    Cross,
1497}
1498
1499/// Join condition: how to match rows with nodes
1500#[derive(Debug, Clone)]
1501pub struct JoinCondition {
1502    /// Left field (table side)
1503    pub left_field: FieldRef,
1504    /// Right field (graph side)
1505    pub right_field: FieldRef,
1506}
1507
1508impl JoinCondition {
1509    /// Create a new join condition
1510    pub fn new(left: FieldRef, right: FieldRef) -> Self {
1511        Self {
1512            left_field: left,
1513            right_field: right,
1514        }
1515    }
1516}
1517
1518/// Reference to a field (table column, node property, or edge property)
1519#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1520pub enum FieldRef {
1521    /// Table column: table.column
1522    TableColumn { table: String, column: String },
1523    /// Node property: alias.property
1524    NodeProperty { alias: String, property: String },
1525    /// Edge property: alias.property
1526    EdgeProperty { alias: String, property: String },
1527    /// Node ID: alias.id
1528    NodeId { alias: String },
1529}
1530
1531impl FieldRef {
1532    /// Create a table column reference
1533    pub fn column(table: &str, column: &str) -> Self {
1534        Self::TableColumn {
1535            table: table.to_string(),
1536            column: column.to_string(),
1537        }
1538    }
1539
1540    /// Create a node property reference
1541    pub fn node_prop(alias: &str, property: &str) -> Self {
1542        Self::NodeProperty {
1543            alias: alias.to_string(),
1544            property: property.to_string(),
1545        }
1546    }
1547
1548    /// Create a node ID reference
1549    pub fn node_id(alias: &str) -> Self {
1550        Self::NodeId {
1551            alias: alias.to_string(),
1552        }
1553    }
1554
1555    /// Create an edge property reference
1556    pub fn edge_prop(alias: &str, property: &str) -> Self {
1557        Self::EdgeProperty {
1558            alias: alias.to_string(),
1559            property: property.to_string(),
1560        }
1561    }
1562}
1563
1564// ============================================================================
1565// Path Query
1566// ============================================================================
1567
1568/// Path query: find paths between nodes
1569#[derive(Debug, Clone)]
1570pub struct PathQuery {
1571    /// Optional outer alias when used as a join source
1572    pub alias: Option<String>,
1573    /// Source node selector
1574    pub from: NodeSelector,
1575    /// Target node selector
1576    pub to: NodeSelector,
1577    /// Edge labels to traverse (empty = any). Strings are resolved against
1578    /// the runtime registry by the executor.
1579    pub via: Vec<String>,
1580    /// Maximum path length
1581    pub max_length: u32,
1582    /// Filter on paths
1583    pub filter: Option<Filter>,
1584    /// Return projections
1585    pub return_: Vec<Projection>,
1586}
1587
1588impl PathQuery {
1589    /// Create a new path query
1590    pub fn new(from: NodeSelector, to: NodeSelector) -> Self {
1591        Self {
1592            alias: None,
1593            from,
1594            to,
1595            via: Vec::new(),
1596            max_length: 10,
1597            filter: None,
1598            return_: Vec::new(),
1599        }
1600    }
1601
1602    /// Set outer alias
1603    pub fn alias(mut self, alias: &str) -> Self {
1604        self.alias = Some(alias.to_string());
1605        self
1606    }
1607
1608    /// Add an edge label constraint to traverse (string form).
1609    pub fn via_label(mut self, label: impl Into<String>) -> Self {
1610        self.via.push(label.into());
1611        self
1612    }
1613}
1614
1615/// Node selector for path queries
1616#[derive(Debug, Clone)]
1617pub enum NodeSelector {
1618    /// By node ID
1619    ById(String),
1620    /// By node label and property
1621    ByType {
1622        node_label: String,
1623        filter: Option<PropertyFilter>,
1624    },
1625    /// By table row (linked node)
1626    ByRow { table: String, row_id: u64 },
1627}
1628
1629impl NodeSelector {
1630    /// Select by node ID
1631    pub fn by_id(id: &str) -> Self {
1632        Self::ById(id.to_string())
1633    }
1634
1635    /// Select by label string (preferred).
1636    pub fn by_label(label: impl Into<String>) -> Self {
1637        Self::ByType {
1638            node_label: label.into(),
1639            filter: None,
1640        }
1641    }
1642
1643    /// Select by table row
1644    pub fn by_row(table: &str, row_id: u64) -> Self {
1645        Self::ByRow {
1646            table: table.to_string(),
1647            row_id,
1648        }
1649    }
1650}
1651
1652// ============================================================================
1653// Vector Query
1654// ============================================================================
1655
1656/// Vector similarity search query
1657///
1658/// ```text
1659/// VECTOR SEARCH embeddings
1660/// SIMILAR TO [0.1, 0.2, ..., 0.5]
1661/// WHERE metadata.source = 'nmap'
1662/// LIMIT 10
1663/// ```
1664#[derive(Debug, Clone)]
1665pub struct VectorQuery {
1666    /// Optional outer alias when used as a join source
1667    pub alias: Option<String>,
1668    /// Collection name to search
1669    pub collection: String,
1670    /// Query vector (or reference to get vector from)
1671    pub query_vector: VectorSource,
1672    /// Number of results to return
1673    pub k: usize,
1674    /// Metadata filter
1675    pub filter: Option<MetadataFilter>,
1676    /// Distance metric to use (defaults to collection's metric)
1677    pub metric: Option<DistanceMetric>,
1678    /// Include vectors in results
1679    pub include_vectors: bool,
1680    /// Include metadata in results
1681    pub include_metadata: bool,
1682    /// Minimum similarity threshold (optional)
1683    pub threshold: Option<f32>,
1684}
1685
1686impl VectorQuery {
1687    /// Create a new vector query
1688    pub fn new(collection: &str, query: VectorSource) -> Self {
1689        Self {
1690            alias: None,
1691            collection: collection.to_string(),
1692            query_vector: query,
1693            k: 10,
1694            filter: None,
1695            metric: None,
1696            include_vectors: false,
1697            include_metadata: true,
1698            threshold: None,
1699        }
1700    }
1701
1702    /// Set the number of results
1703    pub fn limit(mut self, k: usize) -> Self {
1704        self.k = k;
1705        self
1706    }
1707
1708    /// Set metadata filter
1709    pub fn with_filter(mut self, filter: MetadataFilter) -> Self {
1710        self.filter = Some(filter);
1711        self
1712    }
1713
1714    /// Include vectors in results
1715    pub fn with_vectors(mut self) -> Self {
1716        self.include_vectors = true;
1717        self
1718    }
1719
1720    /// Set similarity threshold
1721    pub fn min_similarity(mut self, threshold: f32) -> Self {
1722        self.threshold = Some(threshold);
1723        self
1724    }
1725
1726    /// Set outer alias
1727    pub fn alias(mut self, alias: &str) -> Self {
1728        self.alias = Some(alias.to_string());
1729        self
1730    }
1731}
1732
1733/// Source of query vector
1734#[derive(Debug, Clone)]
1735pub enum VectorSource {
1736    /// Literal vector values
1737    Literal(Vec<f32>),
1738    /// Text to embed (requires embedding function)
1739    Text(String),
1740    /// Reference to another vector by ID
1741    Reference { collection: String, vector_id: u64 },
1742    /// From a subquery result
1743    Subquery(Box<QueryExpr>),
1744}
1745
1746impl VectorSource {
1747    /// Create from literal vector
1748    pub fn literal(values: Vec<f32>) -> Self {
1749        Self::Literal(values)
1750    }
1751
1752    /// Create from text (to be embedded)
1753    pub fn text(s: &str) -> Self {
1754        Self::Text(s.to_string())
1755    }
1756
1757    /// Reference another vector
1758    pub fn reference(collection: &str, vector_id: u64) -> Self {
1759        Self::Reference {
1760            collection: collection.to_string(),
1761            vector_id,
1762        }
1763    }
1764}
1765
1766// ============================================================================
1767// Hybrid Query
1768// ============================================================================
1769
1770/// Hybrid query combining structured (table/graph) and vector search
1771///
1772/// ```text
1773/// FROM hosts h
1774/// JOIN VECTOR embeddings e ON h.id = e.metadata.host_id
1775/// SIMILAR TO 'ssh vulnerability'
1776/// WHERE h.os = 'Linux'
1777/// RETURN h.*, e.distance
1778/// ```
1779#[derive(Debug, Clone)]
1780pub struct HybridQuery {
1781    /// Optional outer alias when used as a join source
1782    pub alias: Option<String>,
1783    /// Structured query part (table/graph)
1784    pub structured: Box<QueryExpr>,
1785    /// Vector search part
1786    pub vector: VectorQuery,
1787    /// How to combine results
1788    pub fusion: FusionStrategy,
1789    /// Final result limit
1790    pub limit: Option<usize>,
1791}
1792
1793impl HybridQuery {
1794    /// Create a new hybrid query
1795    pub fn new(structured: QueryExpr, vector: VectorQuery) -> Self {
1796        Self {
1797            alias: None,
1798            structured: Box::new(structured),
1799            vector,
1800            fusion: FusionStrategy::Rerank { weight: 0.5 },
1801            limit: None,
1802        }
1803    }
1804
1805    /// Set fusion strategy
1806    pub fn with_fusion(mut self, fusion: FusionStrategy) -> Self {
1807        self.fusion = fusion;
1808        self
1809    }
1810
1811    /// Set result limit
1812    pub fn limit(mut self, limit: usize) -> Self {
1813        self.limit = Some(limit);
1814        self
1815    }
1816
1817    /// Set outer alias
1818    pub fn alias(mut self, alias: &str) -> Self {
1819        self.alias = Some(alias.to_string());
1820        self
1821    }
1822}
1823
1824/// Strategy for combining structured and vector search results
1825#[derive(Debug, Clone)]
1826pub enum FusionStrategy {
1827    /// Vector similarity re-ranks structured results
1828    /// weight: 0.0 = pure structured, 1.0 = pure vector
1829    Rerank { weight: f32 },
1830    /// Filter with structured query, then search vectors among filtered
1831    FilterThenSearch,
1832    /// Search vectors first, then filter with structured query
1833    SearchThenFilter,
1834    /// Reciprocal Rank Fusion
1835    /// k: RRF constant (typically 60)
1836    RRF { k: u32 },
1837    /// Intersection: only return results that match both
1838    Intersection,
1839    /// Union: return results from either (with combined scores)
1840    Union {
1841        structured_weight: f32,
1842        vector_weight: f32,
1843    },
1844}
1845
1846impl Default for FusionStrategy {
1847    fn default() -> Self {
1848        Self::Rerank { weight: 0.5 }
1849    }
1850}
1851
1852// ============================================================================
1853// DML/DDL Query Types
1854// ============================================================================
1855
1856/// Entity type qualifier for INSERT statements
1857#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1858pub enum InsertEntityType {
1859    /// Default: plain row
1860    #[default]
1861    Row,
1862    /// INSERT INTO t NODE (...)
1863    Node,
1864    /// INSERT INTO t EDGE (...)
1865    Edge,
1866    /// INSERT INTO t VECTOR (...)
1867    Vector,
1868    /// INSERT INTO t DOCUMENT (...)
1869    Document,
1870    /// INSERT INTO t KV (...)
1871    Kv,
1872}
1873
1874/// Item-kind qualifier for UPDATE statements.
1875///
1876/// The `DOCUMENTS` / `ROWS` / `KV` model markers were removed (ADR 0067,
1877/// #1711): the catalog knows every existing collection's model, so an unmarked
1878/// UPDATE parses to `Rows` and the runtime resolves `Documents` / `Kv`
1879/// semantics from the catalog. Only `NODES` / `EDGES` remain parse-time markers
1880/// — a graph collection holds both record kinds, so only the user can say which.
1881#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1882pub enum UpdateTarget {
1883    /// Unmarked UPDATE: parses as row-shaped, catalog-resolved at runtime.
1884    #[default]
1885    Rows,
1886    /// Catalog-inferred document collection (no parse-time marker).
1887    Documents,
1888    /// Catalog-inferred KV collection (no parse-time marker).
1889    Kv,
1890    /// UPDATE t NODES SET ...
1891    Nodes,
1892    /// UPDATE t EDGES SET ...
1893    Edges,
1894}
1895
1896/// An item in a RETURNING clause: either `*` (all columns) or a named column.
1897#[derive(Debug, Clone, PartialEq)]
1898pub enum ReturningItem {
1899    /// RETURNING *
1900    All,
1901    /// RETURNING col
1902    Column(String),
1903}
1904
1905/// INSERT INTO table (columns) VALUES (row1), (row2), ... [WITH TTL duration] [WITH METADATA (k=v)]
1906#[derive(Debug, Clone)]
1907pub struct InsertQuery {
1908    /// Target table name
1909    pub table: String,
1910    /// Entity type qualifier
1911    pub entity_type: InsertEntityType,
1912    /// Column names
1913    pub columns: Vec<String>,
1914    /// Canonical SQL rows of expressions.
1915    pub value_exprs: Vec<Vec<super::Expr>>,
1916    /// Rows of values (each inner Vec is one row)
1917    pub values: Vec<Vec<Value>>,
1918    /// Optional RETURNING clause items.
1919    pub returning: Option<Vec<ReturningItem>>,
1920    /// Optional TTL in milliseconds (from WITH TTL clause)
1921    pub ttl_ms: Option<u64>,
1922    /// Optional absolute expiration (from WITH EXPIRES AT clause)
1923    pub expires_at_ms: Option<u64>,
1924    /// Optional metadata key-value pairs (from WITH METADATA clause)
1925    pub with_metadata: Vec<(String, Value)>,
1926    /// Auto-embed fields on insert (from WITH AUTO EMBED clause)
1927    pub auto_embed: Option<AutoEmbedConfig>,
1928    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
1929    pub suppress_events: bool,
1930}
1931
1932/// Configuration for automatic embedding generation on INSERT.
1933#[derive(Debug, Clone)]
1934pub struct AutoEmbedConfig {
1935    /// Fields to extract text from for embedding
1936    pub fields: Vec<String>,
1937    /// AI provider from an explicit `USING <provider>`. Empty when none was
1938    /// given — the runtime then resolves it via the embeddings task pointer
1939    /// `red.config.ai.embeddings.provider` (ADR-0068 §5).
1940    pub provider: String,
1941    /// Optional model override
1942    pub model: Option<String>,
1943}
1944
1945/// EVENTS BACKFILL collection [WHERE pred] TO queue [LIMIT n]
1946#[derive(Debug, Clone)]
1947pub struct EventsBackfillQuery {
1948    pub collection: String,
1949    pub where_filter: Option<String>,
1950    pub target_queue: String,
1951    pub limit: Option<u64>,
1952}
1953
1954/// UPDATE table SET col=val, ... WHERE filter [WITH TTL duration] [WITH METADATA (...)]
1955#[derive(Debug, Clone)]
1956pub struct UpdateQuery {
1957    /// Target table name
1958    pub table: String,
1959    /// Explicit item-kind target. Omitted targets default to rows.
1960    pub target: UpdateTarget,
1961    /// Canonical SQL assignments.
1962    pub assignment_exprs: Vec<(String, super::Expr)>,
1963    /// Per-assignment compound operator for `SET col += expr` forms.
1964    /// `None` means ordinary `SET col = expr`.
1965    pub compound_assignment_ops: Vec<Option<super::BinOp>>,
1966    /// Best-effort literal-only cache of assignments. Non-foldable expressions
1967    /// are preserved exclusively in `assignment_exprs` and evaluated later
1968    /// against the row pre-image by the runtime.
1969    pub assignments: Vec<(String, Value)>,
1970    /// Canonical SQL WHERE clause.
1971    pub where_expr: Option<super::Expr>,
1972    /// Optional WHERE filter
1973    pub filter: Option<Filter>,
1974    /// Optional TTL in milliseconds (from WITH TTL clause)
1975    pub ttl_ms: Option<u64>,
1976    /// Optional absolute expiration (from WITH EXPIRES AT clause)
1977    pub expires_at_ms: Option<u64>,
1978    /// Optional metadata key-value pairs (from WITH METADATA clause)
1979    pub with_metadata: Vec<(String, Value)>,
1980    /// Optional RETURNING clause items.
1981    pub returning: Option<Vec<ReturningItem>>,
1982    /// Optional Concurrent claim cardinality.
1983    pub claim_limit: Option<u64>,
1984    /// Require the claim cardinality to be fully satisfied before mutating.
1985    pub claim_exact: bool,
1986    /// Optional deterministic target ordering for limited UPDATE batches.
1987    pub order_by: Vec<OrderByClause>,
1988    /// Optional `LIMIT N` cap. Caps the number of targets the executor
1989    /// will mutate in a single statement. Required by `BATCH N ROWS`
1990    /// data migrations (#37) which run the same UPDATE body in a
1991    /// loop, advancing a checkpoint between batches.
1992    pub limit: Option<u64>,
1993    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
1994    pub suppress_events: bool,
1995}
1996
1997/// DELETE FROM table WHERE filter
1998#[derive(Debug, Clone)]
1999pub struct DeleteQuery {
2000    /// Target table name
2001    pub table: String,
2002    /// Canonical SQL WHERE clause.
2003    pub where_expr: Option<super::Expr>,
2004    /// Optional WHERE filter
2005    pub filter: Option<Filter>,
2006    /// Optional RETURNING clause items.
2007    pub returning: Option<Vec<ReturningItem>>,
2008    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
2009    pub suppress_events: bool,
2010}
2011
2012/// CREATE TABLE name (columns) or CREATE {KV|CONFIG|VAULT} name
2013#[derive(Debug, Clone)]
2014pub struct CreateTableQuery {
2015    /// Declared collection model. Defaults to Table for CREATE TABLE.
2016    pub collection_model: CollectionModel,
2017    /// Table name
2018    pub name: String,
2019    /// Column definitions
2020    pub columns: Vec<CreateColumnDef>,
2021    /// IF NOT EXISTS flag
2022    pub if_not_exists: bool,
2023    /// Optional default TTL applied to newly inserted items in this collection.
2024    pub default_ttl_ms: Option<u64>,
2025    /// Metrics rollup tiers declared by `CREATE METRICS ... DOWNSAMPLE`.
2026    /// Uses the existing time-series policy spelling: target:source:aggregation.
2027    pub metrics_rollup_policies: Vec<String>,
2028    /// Fields to prioritize in the context index (WITH CONTEXT INDEX ON (f1, f2))
2029    pub context_index_fields: Vec<String>,
2030    /// Enables the global context index for this table
2031    /// (`WITH context_index = true`). Default false — pure OLTP tables
2032    /// skip the tokenisation / 3-way RwLock write storm on every insert.
2033    /// Having `context_index_fields` non-empty also enables it implicitly.
2034    pub context_index_enabled: bool,
2035    /// When true, CREATE TABLE implicitly adds two user-visible columns
2036    /// `created_at` and `updated_at` (BIGINT unix-ms). The runtime
2037    /// populates them from `UnifiedEntity::created_at/updated_at` on
2038    /// every write; `created_at` is immutable after insert.
2039    /// Enabled via `WITH timestamps = true` in the DDL.
2040    pub timestamps: bool,
2041    /// Partitioning spec (Phase 2.2 PG parity).
2042    ///
2043    /// When present the table is the *parent* of a partition tree — every
2044    /// child partition is registered via `ALTER TABLE ... ATTACH PARTITION`.
2045    /// Phase 2.2 stops at registry-only: queries against a partitioned
2046    /// parent don't auto-rewrite as UNION yet (Phase 4 adds pruning).
2047    pub partition_by: Option<PartitionSpec>,
2048    /// Table-scoped multi-tenancy declaration (Phase 2.5.4).
2049    ///
2050    /// Syntax: `CREATE TABLE t (...) WITH (tenant_by = 'col_name')` or
2051    /// the shorthand `CREATE TABLE t (...) TENANT BY (col_name)`. The
2052    /// runtime treats the named column as the tenant discriminator and
2053    /// automatically:
2054    ///
2055    /// 1. Registers the table → column mapping so INSERTs that omit the
2056    ///    column get `CURRENT_TENANT()` auto-filled.
2057    /// 2. Installs an implicit RLS policy equivalent to
2058    ///    `USING (col = CURRENT_TENANT())` for SELECT/UPDATE/DELETE/INSERT.
2059    /// 3. Flips `rls_enabled_tables` on so the policy actually applies.
2060    ///
2061    /// None leaves the table non-tenant-scoped — callers manage tenancy
2062    /// manually via explicit CREATE POLICY if they want it.
2063    pub tenant_by: Option<String>,
2064    /// When true, UPDATE and DELETE on this table are rejected at
2065    /// parse time. Corresponds to `CREATE TABLE ... APPEND ONLY` or
2066    /// `WITH (append_only = true)`. Default false (mutable).
2067    pub append_only: bool,
2068    /// Declarative event subscriptions for this table. #291 stores
2069    /// metadata only; event emission is intentionally out of scope.
2070    pub subscriptions: Vec<reddb_types::catalog::SubscriptionDescriptor>,
2071    /// Analytics views declared by `CREATE GRAPH ... WITH ANALYTICS (...)`
2072    /// (issue #800). Empty for every collection model except graphs that
2073    /// opt in. Threaded into the persisted `CollectionContract` at execution
2074    /// time so each `<graph>.<output>` view is durable.
2075    pub analytics_config: Vec<reddb_types::catalog::AnalyticsViewDescriptor>,
2076    /// `CREATE VAULT ... WITH OWN MASTER KEY`: provision per-vault
2077    /// key material instead of using the cluster vault key.
2078    pub vault_own_master_key: bool,
2079    /// Per-collection AI policy declared by `WITH (EMBED (...) | MODERATE
2080    /// (...) | VISION (...))` (PRD #1267, issue #1271). `None` when no AI
2081    /// clause is present. The parser validates the grammar; the runtime
2082    /// validates each modality's provider/model against the capability
2083    /// matrix (#1269) at DDL execution time and persists the policy in
2084    /// the `CollectionContract`.
2085    pub ai_policy: Option<reddb_types::catalog::AiPolicy>,
2086}
2087
2088/// CREATE METRIC path TYPE kind ROLE role
2089///   [SOURCE <ident>] [QUERY '<text>'] [WINDOW <duration>] [TIME_FIELD <ident>]
2090///
2091/// Issue #790 — when any of the derived-metric clauses are present the
2092/// descriptor is a *derived* metric: it names the inputs that a future
2093/// execution layer would consume. v0 stores the metadata only; reads of
2094/// the metric's *output* (not its descriptor) return a structured
2095/// "not yet implemented" error.
2096#[derive(Debug, Clone)]
2097pub struct CreateMetricQuery {
2098    pub path: String,
2099    pub kind: String,
2100    pub role: String,
2101    pub source: Option<String>,
2102    pub query: Option<String>,
2103    pub window_ms: Option<u64>,
2104    pub time_field: Option<String>,
2105}
2106
2107/// ALTER METRIC path SET <field> <value>
2108///
2109/// v0 mutability:
2110/// - `set_role`: mutable — role is a semantic label (operational/kpi/sli).
2111/// - `attempted_kind`: parser captured a `SET KIND`/`SET TYPE` clause; the
2112///   runtime rejects with a clear error because kind changes alter the
2113///   metric's mathematical meaning (counter vs gauge vs histogram, etc.).
2114/// - `attempted_path`: parser captured a `SET PATH` clause; the runtime
2115///   rejects because path is the descriptor's identity.
2116#[derive(Debug, Clone)]
2117pub struct AlterMetricQuery {
2118    pub path: String,
2119    pub set_role: Option<String>,
2120    pub attempted_kind: Option<String>,
2121    pub attempted_path: Option<String>,
2122}
2123
2124/// CREATE SLO path ON metric_path TARGET t WINDOW d UNIT
2125///
2126/// Issue #791 — declared over an existing SLI-role metric descriptor.
2127/// `target` is the objective (0 < target <= 1, e.g. 0.999); `window_ms`
2128/// is the rolling window the objective is evaluated over. Burn-rate /
2129/// error-budget evaluation is deferred to later slices — v0 stores
2130/// catalog state only.
2131#[derive(Debug, Clone)]
2132pub struct CreateSloQuery {
2133    pub path: String,
2134    pub metric_path: String,
2135    pub target: f64,
2136    pub window_ms: u64,
2137}
2138
2139/// CREATE COLLECTION name KIND kind [SIGNED_BY ('pubkey_hex', ...)]
2140#[derive(Debug, Clone)]
2141pub struct CreateCollectionQuery {
2142    pub name: String,
2143    pub kind: String,
2144    pub if_not_exists: bool,
2145    pub vector_dimension: Option<usize>,
2146    pub vector_metric: Option<DistanceMetric>,
2147    /// Initial Ed25519 allowed-signer registry. Empty = unsigned collection.
2148    /// Each entry is a 32-byte Ed25519 public key. Mutable post-create via
2149    /// `ALTER COLLECTION ... ADD|REVOKE SIGNER` (see issue #520).
2150    pub allowed_signers: Vec<[u8; 32]>,
2151}
2152
2153/// CREATE VECTOR name DIM n [METRIC metric]
2154#[derive(Debug, Clone)]
2155pub struct CreateVectorQuery {
2156    pub name: String,
2157    pub dimension: usize,
2158    pub metric: DistanceMetric,
2159    pub if_not_exists: bool,
2160}
2161
2162/// `PARTITION BY RANGE|LIST|HASH (column)` clause.
2163#[derive(Debug, Clone, PartialEq, Eq)]
2164pub struct PartitionSpec {
2165    pub kind: PartitionKind,
2166    /// Partition key column(s). Simple single-column for Phase 2.2.
2167    pub column: String,
2168}
2169
2170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2171pub enum PartitionKind {
2172    /// `PARTITION BY RANGE(col)` — children bind `FOR VALUES FROM (a) TO (b)`.
2173    Range,
2174    /// `PARTITION BY LIST(col)` — children bind `FOR VALUES IN (v1, v2, ...)`.
2175    List,
2176    /// `PARTITION BY HASH(col)` — children bind `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
2177    Hash,
2178}
2179
2180/// Column definition for CREATE TABLE
2181#[derive(Debug, Clone)]
2182pub struct CreateColumnDef {
2183    /// Column name
2184    pub name: String,
2185    /// Legacy declared type string preserved for the runtime/storage pipeline.
2186    pub data_type: String,
2187    /// Structured SQL type used by the semantic layer.
2188    pub sql_type: SqlTypeName,
2189    /// NOT NULL constraint
2190    pub not_null: bool,
2191    /// DEFAULT value expression
2192    pub default: Option<String>,
2193    /// Compression level (COMPRESS:N)
2194    pub compress: Option<u8>,
2195    /// UNIQUE constraint
2196    pub unique: bool,
2197    /// PRIMARY KEY constraint
2198    pub primary_key: bool,
2199    /// Enum variant names (for ENUM type)
2200    pub enum_variants: Vec<String>,
2201    /// Array element type (for ARRAY type)
2202    pub array_element: Option<String>,
2203    /// Decimal precision (for DECIMAL type)
2204    pub decimal_precision: Option<u8>,
2205}
2206
2207/// DROP TABLE name
2208#[derive(Debug, Clone)]
2209pub struct DropTableQuery {
2210    /// Table name
2211    pub name: String,
2212    /// IF EXISTS flag
2213    pub if_exists: bool,
2214}
2215
2216/// DROP GRAPH [IF EXISTS] name
2217#[derive(Debug, Clone)]
2218pub struct DropGraphQuery {
2219    pub name: String,
2220    pub if_exists: bool,
2221}
2222
2223/// DROP VECTOR [IF EXISTS] name
2224#[derive(Debug, Clone)]
2225pub struct DropVectorQuery {
2226    pub name: String,
2227    pub if_exists: bool,
2228}
2229
2230/// DROP DOCUMENT [IF EXISTS] name
2231#[derive(Debug, Clone)]
2232pub struct DropDocumentQuery {
2233    pub name: String,
2234    pub if_exists: bool,
2235}
2236
2237/// DROP {KV|CONFIG|VAULT} [IF EXISTS] name
2238#[derive(Debug, Clone)]
2239pub struct DropKvQuery {
2240    pub name: String,
2241    pub if_exists: bool,
2242    pub model: CollectionModel,
2243}
2244
2245/// DROP COLLECTION [IF EXISTS] name
2246#[derive(Debug, Clone)]
2247pub struct DropCollectionQuery {
2248    pub name: String,
2249    pub if_exists: bool,
2250    pub model: Option<CollectionModel>,
2251}
2252
2253/// TRUNCATE {TABLE|GRAPH|VECTOR|DOCUMENT|TIMESERIES|KV|QUEUE|COLLECTION} [IF EXISTS] name
2254#[derive(Debug, Clone)]
2255pub struct TruncateQuery {
2256    pub name: String,
2257    pub model: Option<CollectionModel>,
2258    pub if_exists: bool,
2259}
2260
2261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2262pub enum VcsRefKind {
2263    Branch,
2264    Tag,
2265}
2266
2267/// CREATE BRANCH 'name' [FROM commitish] / CREATE TAG 'name' [AT commitish]
2268#[derive(Debug, Clone)]
2269pub struct CreateVcsRefQuery {
2270    pub kind: VcsRefKind,
2271    pub name: String,
2272    pub target: Option<String>,
2273}
2274
2275/// DROP BRANCH 'name' / DROP TAG 'name'
2276#[derive(Debug, Clone)]
2277pub struct DropVcsRefQuery {
2278    pub kind: VcsRefKind,
2279    pub name: String,
2280}
2281
2282/// FORK STORE AS name [AT LSN n]
2283#[derive(Debug, Clone)]
2284pub struct ForkStoreQuery {
2285    pub name: String,
2286    pub at_lsn: Option<u64>,
2287}
2288
2289/// PROMOTE FORK name
2290#[derive(Debug, Clone)]
2291pub struct PromoteForkQuery {
2292    pub name: String,
2293}
2294
2295/// DROP FORK name
2296#[derive(Debug, Clone)]
2297pub struct DropForkQuery {
2298    pub name: String,
2299    pub if_exists: bool,
2300}
2301
2302/// ALTER TABLE name operations
2303#[derive(Debug, Clone)]
2304pub struct AlterTableQuery {
2305    /// Table name
2306    pub name: String,
2307    /// Alter operations
2308    pub operations: Vec<AlterOperation>,
2309}
2310
2311/// Single ALTER TABLE operation
2312#[derive(Debug, Clone)]
2313pub enum AlterOperation {
2314    /// ADD COLUMN definition
2315    AddColumn(CreateColumnDef),
2316    /// DROP COLUMN name
2317    DropColumn(String),
2318    /// RENAME COLUMN from TO to
2319    RenameColumn { from: String, to: String },
2320    /// `ATTACH PARTITION child FOR VALUES ...` (Phase 2.2 PG parity).
2321    ///
2322    /// Binds an existing child table to the parent partitioned table.
2323    /// The `bound` string captures the raw bound expression so the
2324    /// runtime can round-trip it back into `red_config` without a
2325    /// dedicated per-kind AST.
2326    AttachPartition {
2327        child: String,
2328        /// Human-readable bound string, e.g. `FROM (2024-01-01) TO (2025-01-01)`
2329        /// or `IN (1, 2, 3)` or `WITH (MODULUS 4, REMAINDER 0)`.
2330        bound: String,
2331    },
2332    /// `DETACH PARTITION child`
2333    DetachPartition { child: String },
2334    /// `ENABLE ROW LEVEL SECURITY` (Phase 2.5 PG parity).
2335    ///
2336    /// Flips the table into RLS-enforced mode. Reads against the table
2337    /// will be filtered by every matching `CREATE POLICY` (for the
2338    /// current role) combined with `AND`.
2339    EnableRowLevelSecurity,
2340    /// `DISABLE ROW LEVEL SECURITY` — disables enforcement; policies
2341    /// remain defined but are ignored until re-enabled.
2342    DisableRowLevelSecurity,
2343    /// `ENABLE TENANCY ON (col)` (Phase 2.5.4 PG parity-ish).
2344    ///
2345    /// Retrofit a tenant-scoped declaration onto an existing table —
2346    /// registers the column, installs the auto `__tenant_iso` RLS
2347    /// policy, and flips RLS on. Equivalent to re-running
2348    /// `CREATE TABLE ... TENANT BY (col)` minus the schema creation.
2349    EnableTenancy { column: String },
2350    /// `DISABLE TENANCY` — tears down the auto-policy and clears the
2351    /// tenancy registration. User-defined policies on the table are
2352    /// untouched; RLS stays enabled if any survive.
2353    DisableTenancy,
2354    /// `SET APPEND_ONLY = true|false` — flips the catalog flag.
2355    /// Setting `true` rejects all future UPDATE/DELETE at parse-time
2356    /// guard; setting `false` re-enables them. Existing rows are
2357    /// untouched either way — this is a purely declarative switch.
2358    SetAppendOnly(bool),
2359    /// `SET VERSIONED = true|false` — opt the table into (or out of)
2360    /// Git-for-Data. Enables merge / diff / AS OF semantics against
2361    /// this collection. Works retroactively: previously-created
2362    /// rows become part of the history accessible via AS OF as long
2363    /// as their xmin is still pinned by an existing commit.
2364    SetVersioned(bool),
2365    /// `ENABLE EVENTS ...` — install or re-enable table event subscription metadata.
2366    EnableEvents(reddb_types::catalog::SubscriptionDescriptor),
2367    /// `DISABLE EVENTS` — mark all table event subscriptions disabled.
2368    DisableEvents,
2369    /// `ADD SUBSCRIPTION name TO queue [REDACT (...)] [WHERE ...]` — add a named subscription.
2370    AddSubscription {
2371        name: String,
2372        descriptor: reddb_types::catalog::SubscriptionDescriptor,
2373    },
2374    /// `DROP SUBSCRIPTION name` — remove a named subscription by name.
2375    DropSubscription { name: String },
2376    /// Issue #522 — `ALTER COLLECTION name ADD SIGNER 'hex_pubkey'`.
2377    /// Appends the key to the per-collection signer registry and
2378    /// records an `Add` entry on the `signer_history` audit log.
2379    AddSigner { pubkey: [u8; 32] },
2380    /// Issue #522 — `ALTER COLLECTION name REVOKE SIGNER 'hex_pubkey'`.
2381    /// Removes the key from the *currently allowed* set and records a
2382    /// `Revoke` entry. Past rows signed by `pubkey` remain readable
2383    /// and re-verifiable — only future inserts are rejected.
2384    RevokeSigner { pubkey: [u8; 32] },
2385    /// Issue #580 — `ALTER COLLECTION name SET RETENTION <duration>`.
2386    /// Stores a declarative retention policy on the collection contract.
2387    /// Enforcement is lazy-on-scan: reads silently filter out rows older
2388    /// than `now - duration_ms` by the collection's timestamp column.
2389    SetRetention { duration_ms: u64 },
2390    /// Issue #580 — `ALTER COLLECTION name UNSET RETENTION`.
2391    /// Removes the policy. Previously-hidden expired rows become
2392    /// readable again — the slice never physically dropped them.
2393    UnsetRetention,
2394    /// Issue #801 — `ALTER GRAPH name ADD ANALYTICS (<output> [opts] [, ...])`.
2395    /// Idempotently enables analytics outputs on an existing graph's
2396    /// `analytics_config` without recreating the collection. Adding an
2397    /// already-enabled output is a no-op (no error, no duplicate state);
2398    /// the next read of `<graph>.<output>` materializes on demand.
2399    AddAnalytics(Vec<reddb_types::catalog::AnalyticsViewDescriptor>),
2400    /// Issue #801 — `ALTER GRAPH name DROP ANALYTICS <output>`.
2401    /// Removes the output from `analytics_config`; the next read of
2402    /// `<graph>.<output>` no longer resolves. Dropping an output that is
2403    /// not currently enabled is a clean error (handled in the executor).
2404    DropAnalytics(reddb_types::catalog::AnalyticsOutput),
2405}
2406
2407// ============================================================================
2408// Shared Types
2409// ============================================================================
2410
2411/// Column/field projection
2412#[derive(Debug, Clone, PartialEq)]
2413pub enum Projection {
2414    /// Select all columns (*)
2415    All,
2416    /// Single column by name
2417    Column(String),
2418    /// Column with alias
2419    Alias(String, String),
2420    /// Function call (name, args)
2421    Function(String, Vec<Projection>),
2422    /// Expression with optional alias
2423    Expression(Box<Filter>, Option<String>),
2424    /// Field reference (for graph properties)
2425    Field(FieldRef, Option<String>),
2426    /// Window function call: `fn(args) OVER (PARTITION BY ... ORDER BY ...
2427    /// [frame])`. Carries the window specification as a sibling so the
2428    /// planner can lower it without re-parsing. No runtime in slice 7a —
2429    /// the analytics executor lands in a subsequent slice (issue #589
2430    /// follow-ups). See `super::WindowSpec`.
2431    Window {
2432        name: String,
2433        args: Vec<Projection>,
2434        window: Box<super::WindowSpec>,
2435        alias: Option<String>,
2436    },
2437}
2438
2439impl Projection {
2440    /// Create a projection from a field reference
2441    pub fn from_field(field: FieldRef) -> Self {
2442        Projection::Field(field, None)
2443    }
2444
2445    /// Create a column projection
2446    pub fn column(name: &str) -> Self {
2447        Projection::Column(name.to_string())
2448    }
2449
2450    /// Create an aliased projection
2451    pub fn with_alias(column: &str, alias: &str) -> Self {
2452        Projection::Alias(column.to_string(), alias.to_string())
2453    }
2454}
2455
2456/// Filter condition
2457#[derive(Debug, Clone, PartialEq)]
2458pub enum Filter {
2459    /// Comparison: field op value
2460    Compare {
2461        field: FieldRef,
2462        op: CompareOp,
2463        value: Value,
2464    },
2465    /// Field-to-field comparison: left.field op right.field. Used when
2466    /// WHERE / BETWEEN operands reference another column instead of a
2467    /// literal — the pre-Fase-2-parser-v2 shim for column-to-column
2468    /// predicates. Once the Expr-rewrite lands, this collapses into
2469    /// `Compare { left: Expr, op, right: Expr }`.
2470    CompareFields {
2471        left: FieldRef,
2472        op: CompareOp,
2473        right: FieldRef,
2474    },
2475    /// Expression-to-expression comparison: `lhs op rhs` where either
2476    /// side may be an arbitrary `Expr` tree (function call, CAST,
2477    /// arithmetic, nested CASE). This is the most general compare
2478    /// variant — `Compare` and `CompareFields` stay as fast-path
2479    /// specialisations because the planner / cost model / index
2480    /// selector all pattern-match on the simpler shapes. The parser
2481    /// only emits this variant when a simpler one cannot express the
2482    /// predicate.
2483    CompareExpr {
2484        lhs: super::Expr,
2485        op: CompareOp,
2486        rhs: super::Expr,
2487    },
2488    /// Logical AND
2489    And(Box<Filter>, Box<Filter>),
2490    /// Logical OR
2491    Or(Box<Filter>, Box<Filter>),
2492    /// Logical NOT
2493    Not(Box<Filter>),
2494    /// IS NULL
2495    IsNull(FieldRef),
2496    /// IS NOT NULL
2497    IsNotNull(FieldRef),
2498    /// IN (value1, value2, ...)
2499    In { field: FieldRef, values: Vec<Value> },
2500    /// BETWEEN low AND high
2501    Between {
2502        field: FieldRef,
2503        low: Value,
2504        high: Value,
2505    },
2506    /// LIKE pattern
2507    Like { field: FieldRef, pattern: String },
2508    /// STARTS WITH prefix
2509    StartsWith { field: FieldRef, prefix: String },
2510    /// ENDS WITH suffix
2511    EndsWith { field: FieldRef, suffix: String },
2512    /// CONTAINS substring
2513    Contains { field: FieldRef, substring: String },
2514}
2515
2516impl Filter {
2517    /// Create a comparison filter
2518    pub fn compare(field: FieldRef, op: CompareOp, value: Value) -> Self {
2519        Self::Compare { field, op, value }
2520    }
2521
2522    /// Combine with AND
2523    pub fn and(self, other: Filter) -> Self {
2524        Self::And(Box::new(self), Box::new(other))
2525    }
2526
2527    /// Combine with OR
2528    pub fn or(self, other: Filter) -> Self {
2529        Self::Or(Box::new(self), Box::new(other))
2530    }
2531
2532    /// Negate
2533    // Filter combinator wrapping `self` in `Filter::Not`; unrelated to
2534    // `std::ops::Not`, so that trait is intentionally not implemented.
2535    #[allow(clippy::should_implement_trait)]
2536    pub fn not(self) -> Self {
2537        Self::Not(Box::new(self))
2538    }
2539
2540    /// Bottom-up AST rewrites: OR-of-equalities → IN, AND/OR flatten.
2541    /// Inspired by MongoDB's `MatchExpression::optimize()`.
2542    /// Call on the result of `effective_table_filter()` before evaluation.
2543    pub fn optimize(self) -> Self {
2544        crate::filter_optimizer::optimize(self)
2545    }
2546}
2547
2548/// Comparison operator
2549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2550pub enum CompareOp {
2551    /// Equal (=)
2552    Eq,
2553    /// Not equal (<> or !=)
2554    Ne,
2555    /// Less than (<)
2556    Lt,
2557    /// Less than or equal (<=)
2558    Le,
2559    /// Greater than (>)
2560    Gt,
2561    /// Greater than or equal (>=)
2562    Ge,
2563}
2564
2565impl fmt::Display for CompareOp {
2566    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2567        match self {
2568            CompareOp::Eq => write!(f, "="),
2569            CompareOp::Ne => write!(f, "<>"),
2570            CompareOp::Lt => write!(f, "<"),
2571            CompareOp::Le => write!(f, "<="),
2572            CompareOp::Gt => write!(f, ">"),
2573            CompareOp::Ge => write!(f, ">="),
2574        }
2575    }
2576}
2577
2578/// Order by clause.
2579///
2580/// Fase 2 migration: `field` is the legacy bare column reference and
2581/// remains populated for back-compat with existing callers (SPARQL /
2582/// Gremlin / Cypher translators, the planner cost model, etc.). The
2583/// new `expr` slot carries an arbitrary `Expr` tree — when present,
2584/// runtime comparators prefer it over `field`, so the parser can
2585/// emit `ORDER BY CAST(a AS INT)`, `ORDER BY a + b * 2`, etc. without
2586/// breaking the rest of the codebase.
2587///
2588/// When `expr` is `None`, the clause behaves exactly like before.
2589/// When `expr` is `Some(Expr::Column(f))`, runtime code may still use
2590/// the legacy path — it's equivalent. Constructors default `expr` to
2591/// `None` so all existing call sites stay source-compatible.
2592#[derive(Debug, Clone)]
2593pub struct OrderByClause {
2594    /// Field to order by. Left populated even when `expr` is set so
2595    /// legacy consumers (planner cardinality estimate, cost model,
2596    /// mode translators) that still pattern-match on `field` keep
2597    /// working during the Fase 2 migration.
2598    pub field: FieldRef,
2599    /// Fase 2 expression-aware sort key. When `Some`, runtime order
2600    /// comparators evaluate this expression per row and sort on the
2601    /// resulting values — unlocks `ORDER BY expr` (Fase 1.6).
2602    pub expr: Option<super::Expr>,
2603    /// Ascending or descending
2604    pub ascending: bool,
2605    /// Nulls first or last
2606    pub nulls_first: bool,
2607}
2608
2609impl OrderByClause {
2610    /// Create ascending order
2611    pub fn asc(field: FieldRef) -> Self {
2612        Self {
2613            field,
2614            expr: None,
2615            ascending: true,
2616            nulls_first: false,
2617        }
2618    }
2619
2620    /// Create descending order
2621    pub fn desc(field: FieldRef) -> Self {
2622        Self {
2623            field,
2624            expr: None,
2625            ascending: false,
2626            nulls_first: true,
2627        }
2628    }
2629
2630    /// Attach an `Expr` sort key to an existing clause. Leaves `field`
2631    /// untouched so back-compat match sites keep their pattern.
2632    pub fn with_expr(mut self, expr: super::Expr) -> Self {
2633        self.expr = Some(expr);
2634        self
2635    }
2636}
2637
2638// ============================================================================
2639// Window OVER clause (issue #589 slice 7a)
2640// ============================================================================
2641//
2642// Syntactic representation of `OVER (PARTITION BY ... ORDER BY ... [frame])`.
2643// Slice 7a: AST + parser only. No runtime / executor wiring.
2644
2645/// Frame unit: `ROWS` (physical row offset) or `RANGE` (logical value
2646/// offset). Slice 7a stores the choice but does not yet differentiate
2647/// at runtime — semantics arrive with the analytics executor.
2648#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2649pub enum WindowFrameUnit {
2650    Rows,
2651    Range,
2652}
2653
2654/// One endpoint of a frame: UNBOUNDED PRECEDING / CURRENT ROW /
2655/// PRECEDING(expr) / FOLLOWING(expr) / UNBOUNDED FOLLOWING.
2656#[derive(Debug, Clone, PartialEq)]
2657pub enum WindowFrameBound {
2658    UnboundedPreceding,
2659    UnboundedFollowing,
2660    CurrentRow,
2661    Preceding(Box<super::Expr>),
2662    Following(Box<super::Expr>),
2663}
2664
2665/// `ROWS|RANGE BETWEEN start AND end` — or the single-bound shorthand
2666/// `ROWS start` (end implied as CURRENT ROW per SQL standard). Slice 7a
2667/// represents both shapes uniformly with `end: Option<...>` so downstream
2668/// code can normalise.
2669#[derive(Debug, Clone, PartialEq)]
2670pub struct WindowFrame {
2671    pub unit: WindowFrameUnit,
2672    pub start: WindowFrameBound,
2673    pub end: Option<WindowFrameBound>,
2674}
2675
2676/// One ORDER BY item inside a window spec. Window order keys are
2677/// expression-based by SQL standard, so we carry an `Expr` directly
2678/// rather than reusing the top-level `OrderByClause` (which still has
2679/// a legacy `FieldRef` slot for the Fase 2 migration).
2680#[derive(Debug, Clone, PartialEq)]
2681pub struct WindowOrderItem {
2682    pub expr: super::Expr,
2683    pub ascending: bool,
2684    pub nulls_first: bool,
2685}
2686
2687/// Full window specification — the AST node behind `OVER (...)`.
2688/// `frame` is `None` when the user did not specify a frame clause; the
2689/// analytics executor will materialise the SQL default (RANGE UNBOUNDED
2690/// PRECEDING AND CURRENT ROW when ORDER BY is present, the full
2691/// partition otherwise) once it lands.
2692#[derive(Debug, Clone, PartialEq, Default)]
2693pub struct WindowSpec {
2694    pub partition_by: Vec<super::Expr>,
2695    pub order_by: Vec<WindowOrderItem>,
2696    pub frame: Option<WindowFrame>,
2697}
2698
2699// ============================================================================
2700// Graph Commands
2701// ============================================================================
2702
2703/// Graph analytics command issued via SQL-like syntax
2704#[derive(Debug, Clone)]
2705pub struct GraphCommandOrderBy {
2706    pub metric: String,
2707    pub ascending: bool,
2708}
2709
2710#[derive(Debug, Clone)]
2711pub enum GraphCommand {
2712    /// GRAPH NEIGHBORHOOD 'source' [DEPTH n] [DIRECTION dir] [EDGES IN ('label', ...)]
2713    Neighborhood {
2714        source: String,
2715        depth: u32,
2716        direction: String,
2717        edge_labels: Option<Vec<String>>,
2718    },
2719    /// GRAPH SHORTEST_PATH 'source' TO 'target' [ALGORITHM alg] [DIRECTION dir] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2720    ShortestPath {
2721        source: String,
2722        target: String,
2723        algorithm: String,
2724        direction: String,
2725        limit: Option<u32>,
2726        order_by: Option<GraphCommandOrderBy>,
2727    },
2728    /// GRAPH TRAVERSE 'source' [STRATEGY bfs|dfs] [DEPTH n] [DIRECTION dir] [EDGES IN ('label', ...)]
2729    Traverse {
2730        source: String,
2731        strategy: String,
2732        depth: u32,
2733        direction: String,
2734        edge_labels: Option<Vec<String>>,
2735    },
2736    /// GRAPH CENTRALITY [ALGORITHM alg] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2737    ///
2738    /// `limit = None` keeps the historical implicit top-100 cap. `Some(n)`
2739    /// caps the returned rows at `n`.
2740    Centrality {
2741        algorithm: String,
2742        limit: Option<u32>,
2743        order_by: Option<GraphCommandOrderBy>,
2744    },
2745    /// GRAPH COMMUNITY [ALGORITHM alg] [MAX_ITERATIONS n] [ORDER BY metric [ASC|DESC]] [LIMIT n] [RETURN ASSIGNMENTS]
2746    ///
2747    /// `return_assignments = false` (default) keeps the historical per-community
2748    /// aggregate shape (`community_id`, `size`). `true` emits one row per node
2749    /// (`node_id`, `community_id`) — the node→community map (#660).
2750    Community {
2751        algorithm: String,
2752        max_iterations: u32,
2753        limit: Option<u32>,
2754        order_by: Option<GraphCommandOrderBy>,
2755        return_assignments: bool,
2756    },
2757    /// GRAPH COMPONENTS [MODE connected|weak|strong] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2758    Components {
2759        mode: String,
2760        limit: Option<u32>,
2761        order_by: Option<GraphCommandOrderBy>,
2762    },
2763    /// GRAPH CYCLES [MAX_LENGTH n]
2764    Cycles { max_length: u32 },
2765    /// GRAPH CLUSTERING
2766    Clustering,
2767    /// GRAPH TOPOLOGICAL_SORT
2768    TopologicalSort,
2769    /// GRAPH PROPERTIES ['<id-or-label>']
2770    ///
2771    /// `source = None` returns graph-wide stats. `source = Some("...")` returns
2772    /// the full property bag of a specific node, resolved via the same label
2773    /// index as `GRAPH NEIGHBORHOOD` / `GRAPH TRAVERSE` (issue #416).
2774    Properties { source: Option<String> },
2775}
2776
2777// ============================================================================
2778// Search Commands
2779// ============================================================================
2780
2781/// Search command issued via SQL-like syntax
2782#[derive(Debug, Clone)]
2783pub enum SearchCommand {
2784    /// SEARCH SIMILAR [v1, v2, ...] | $N | TEXT 'query' [COLLECTION col] [LIMIT n] [MIN_SCORE f] [USING provider]
2785    Similar {
2786        vector: Vec<f32>,
2787        text: Option<String>,
2788        provider: Option<String>,
2789        collection: String,
2790        limit: usize,
2791        min_score: f32,
2792        /// `$N` placeholder for the vector slot. `Some(idx)` when the SQL
2793        /// used `SEARCH SIMILAR $N ...`; the binder substitutes the
2794        /// user-supplied `Value::Vector` and clears this back to `None`.
2795        /// Runtime executors assert this is `None` post-bind.
2796        vector_param: Option<usize>,
2797        /// `$N` placeholder for the `LIMIT` slot (issue #361). The binder
2798        /// substitutes the user-supplied positive integer into `limit`
2799        /// and clears this back to `None`.
2800        limit_param: Option<usize>,
2801        /// `$N` placeholder for the `MIN_SCORE` slot (issue #361). The
2802        /// binder substitutes the user-supplied float into `min_score`
2803        /// and clears this back to `None`.
2804        min_score_param: Option<usize>,
2805        /// `$N` placeholder for `SEARCH SIMILAR TEXT $N` (issue #361).
2806        /// Binder substitutes the user-supplied text into `text` and
2807        /// clears this back to `None`.
2808        text_param: Option<usize>,
2809    },
2810    /// SEARCH TEXT 'query' [COLLECTION col] [LIMIT n] [FUZZY]
2811    Text {
2812        query: String,
2813        collection: Option<String>,
2814        limit: usize,
2815        fuzzy: bool,
2816        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2817        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2818        /// substitutes the user-supplied positive integer into `limit`
2819        /// and clears this back to `None`.
2820        limit_param: Option<usize>,
2821    },
2822    /// SEARCH HYBRID [vector] [TEXT 'query'] COLLECTION col [LIMIT n]
2823    Hybrid {
2824        vector: Option<Vec<f32>>,
2825        query: Option<String>,
2826        collection: String,
2827        limit: usize,
2828        /// `$N` placeholder for the `LIMIT` / `K` slot (issue #361).
2829        /// Same shape as `SearchCommand::Similar::limit_param`; the
2830        /// binder substitutes the user-supplied positive integer and
2831        /// clears this back to `None`.
2832        limit_param: Option<usize>,
2833    },
2834    /// SEARCH MULTIMODAL 'key_or_query' [COLLECTION col] [LIMIT n]
2835    Multimodal {
2836        query: String,
2837        collection: Option<String>,
2838        limit: usize,
2839        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2840        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2841        /// substitutes the user-supplied positive integer into `limit`
2842        /// and clears this back to `None`.
2843        limit_param: Option<usize>,
2844    },
2845    /// SEARCH INDEX index VALUE 'value' [COLLECTION col] [LIMIT n] [EXACT]
2846    Index {
2847        index: String,
2848        value: String,
2849        collection: Option<String>,
2850        limit: usize,
2851        exact: bool,
2852        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2853        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2854        /// substitutes the user-supplied positive integer into `limit`
2855        /// and clears this back to `None`.
2856        limit_param: Option<usize>,
2857    },
2858    /// SEARCH CONTEXT 'query' [FIELD field] [COLLECTION col] [LIMIT n] [DEPTH n]
2859    Context {
2860        query: String,
2861        field: Option<String>,
2862        collection: Option<String>,
2863        limit: usize,
2864        depth: usize,
2865        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2866        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2867        /// substitutes the user-supplied positive integer into `limit`
2868        /// and clears this back to `None`.
2869        limit_param: Option<usize>,
2870    },
2871    /// SEARCH SPATIAL RADIUS lat lon radius_km COLLECTION col COLUMN col [LIMIT n]
2872    SpatialRadius {
2873        center_lat: f64,
2874        center_lon: f64,
2875        radius_km: f64,
2876        collection: String,
2877        column: String,
2878        limit: usize,
2879        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2880        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2881        /// substitutes the user-supplied positive integer into `limit`
2882        /// and clears this back to `None`.
2883        limit_param: Option<usize>,
2884    },
2885    /// SEARCH SPATIAL BBOX min_lat min_lon max_lat max_lon COLLECTION col COLUMN col [LIMIT n]
2886    SpatialBbox {
2887        min_lat: f64,
2888        min_lon: f64,
2889        max_lat: f64,
2890        max_lon: f64,
2891        collection: String,
2892        column: String,
2893        limit: usize,
2894        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2895        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2896        /// substitutes the user-supplied positive integer into `limit`
2897        /// and clears this back to `None`.
2898        limit_param: Option<usize>,
2899    },
2900    /// SEARCH SPATIAL WITHIN POLYGON ((lat lon), ...) COLLECTION col COLUMN col [LIMIT n]
2901    SpatialWithinPolygon {
2902        vertices: Vec<(f64, f64)>,
2903        collection: String,
2904        column: String,
2905        limit: usize,
2906        /// `$N` placeholder for the `LIMIT` slot. The binder
2907        /// substitutes the user-supplied positive integer into `limit`
2908        /// and clears this back to `None`.
2909        limit_param: Option<usize>,
2910    },
2911    /// SEARCH SPATIAL NEAREST lat lon K n COLLECTION col COLUMN col
2912    SpatialNearest {
2913        lat: f64,
2914        lon: f64,
2915        k: usize,
2916        collection: String,
2917        column: String,
2918        /// `$N` placeholder for the `K` slot (issue #361). Same shape
2919        /// as `SearchCommand::Hybrid::limit_param`; the binder
2920        /// substitutes the user-supplied positive integer into `k`
2921        /// and clears this back to `None`.
2922        k_param: Option<usize>,
2923    },
2924}
2925
2926// ============================================================================
2927// Time-Series DDL
2928// ============================================================================
2929
2930/// CREATE TIMESERIES name [RETENTION duration] [CHUNK_SIZE n] [DOWNSAMPLE spec[, spec...]]
2931///
2932/// `CREATE HYPERTABLE` lands on the same AST with `hypertable` populated.
2933/// The TimescaleDB-style syntax (time column + chunk_interval) gives the
2934/// runtime enough to register a `HypertableSpec` alongside the
2935/// underlying collection contract, so chunk routing and TTL sweeps can
2936/// address the table without a separate DDL.
2937#[derive(Debug, Clone)]
2938pub struct CreateTimeSeriesQuery {
2939    pub name: String,
2940    pub retention_ms: Option<u64>,
2941    pub chunk_size: Option<usize>,
2942    pub downsample_policies: Vec<String>,
2943    pub if_not_exists: bool,
2944    /// When `Some`, the DDL was spelled `CREATE HYPERTABLE` and the
2945    /// runtime must register the spec with the hypertable registry.
2946    pub hypertable: Option<HypertableDdl>,
2947    /// `WITH SESSION_KEY <col>` — default partition column for the
2948    /// `SESSIONIZE` operator. Persisted on the collection contract so
2949    /// queries that omit `BY <col>` pick it up. Issue #576 slice 1.
2950    pub session_key: Option<String>,
2951    /// `SESSION_GAP <duration>` — default inactivity gap (ms) for the
2952    /// `SESSIONIZE` operator. Issue #576 slice 1.
2953    pub session_gap_ms: Option<u64>,
2954    /// Columnar analytical storage policy (PRD #850, #911, #1772).
2955    /// Defaults to true for in-scope collections; `NO COLUMNAR` sets it
2956    /// false as the per-collection opt-out. When true the collection
2957    /// contract is built with
2958    /// `analytical_storage.columnar = true`, so sealing a chunk routes
2959    /// through `seal_chunk_with_config`'s columnar arm and emits an
2960    /// RDCC `ColumnBlock` instead of the row seal.
2961    pub columnar: bool,
2962}
2963
2964/// Hypertable-specific DDL fields — set only when the caller used
2965/// `CREATE HYPERTABLE`.
2966#[derive(Debug, Clone)]
2967pub struct HypertableDdl {
2968    /// Column that carries the nanosecond timestamp axis.
2969    pub time_column: String,
2970    /// Chunk width in nanoseconds.
2971    pub chunk_interval_ns: u64,
2972    /// Per-chunk default TTL in nanoseconds (`None` = no TTL).
2973    pub default_ttl_ns: Option<u64>,
2974}
2975
2976/// DROP TIMESERIES [IF EXISTS] name
2977#[derive(Debug, Clone)]
2978pub struct DropTimeSeriesQuery {
2979    pub name: String,
2980    pub if_exists: bool,
2981}
2982
2983// ============================================================================
2984// Queue DDL & Commands
2985// ============================================================================
2986
2987/// Default `MAX_ATTEMPTS` for `CREATE QUEUE` when omitted.
2988pub const DEFAULT_QUEUE_MAX_ATTEMPTS: u32 = 3;
2989/// Default `LOCK_DEADLINE_MS` for `CREATE QUEUE` when omitted.
2990pub const DEFAULT_QUEUE_LOCK_DEADLINE_MS: u64 = 30_000;
2991/// Default `IN_FLIGHT_CAP_PER_GROUP` for `CREATE QUEUE` when omitted.
2992pub const DEFAULT_QUEUE_IN_FLIGHT_CAP_PER_GROUP: u32 = 10_000;
2993/// Default push idempotency window for `QUEUE PUSH ... DEDUP` when omitted.
2994pub const DEFAULT_QUEUE_DEDUP_WINDOW_MS: u64 = 300_000;
2995
2996/// CREATE QUEUE name [MAX_SIZE n] [PRIORITY] [WITH TTL duration] [WITH DLQ name]
2997/// [MAX_ATTEMPTS n] [LOCK_DEADLINE_MS n] [IN_FLIGHT_CAP_PER_GROUP n]
2998/// [RETRY_DELAY duration] [DEDUP_WINDOW duration]
2999#[derive(Debug, Clone)]
3000pub struct CreateQueueQuery {
3001    pub name: String,
3002    pub mode: QueueMode,
3003    pub priority: bool,
3004    pub max_size: Option<usize>,
3005    pub ttl_ms: Option<u64>,
3006    pub dlq: Option<String>,
3007    pub max_attempts: u32,
3008    pub lock_deadline_ms: u64,
3009    pub in_flight_cap_per_group: u32,
3010    pub if_not_exists: bool,
3011    /// Default retry delay applied to NACK-requeued messages before they
3012    /// become re-deliverable. `None` means no delay — the released
3013    /// message is immediately available again (pre-#723 behaviour).
3014    /// `Some(ms)` reuses the per-message availability machinery from
3015    /// issue #722 to defer the next delivery attempt. An authorized
3016    /// `NACK ... WITH DELAY <duration>` overrides this per-failure.
3017    pub retry_delay_ms: Option<u64>,
3018    /// Queue-scoped push idempotency window. `None` means the engine
3019    /// default applies at runtime.
3020    pub dedup_window_ms: Option<u64>,
3021}
3022
3023/// ALTER QUEUE name SET <clause>
3024///   MODE [FANOUT|WORK|STANDARD|FIFO]
3025///   MAX_ATTEMPTS n
3026///   LOCK_DEADLINE_MS n
3027///   IN_FLIGHT_CAP_PER_GROUP n
3028///   DLQ name
3029///   RETRY_DELAY duration
3030///   DEDUP_WINDOW duration
3031#[derive(Debug, Clone, Default)]
3032pub struct AlterQueueQuery {
3033    pub name: String,
3034    pub mode: Option<QueueMode>,
3035    pub max_attempts: Option<u32>,
3036    pub lock_deadline_ms: Option<u64>,
3037    pub in_flight_cap_per_group: Option<u32>,
3038    pub dlq: Option<String>,
3039    /// Update the queue's default retry delay (issue #723). `Some(0)`
3040    /// clears the delay back to immediate requeue.
3041    pub retry_delay_ms: Option<u64>,
3042    /// Update the queue-scoped push idempotency window.
3043    pub dedup_window_ms: Option<u64>,
3044}
3045
3046/// DROP QUEUE [IF EXISTS] name
3047#[derive(Debug, Clone)]
3048pub struct DropQueueQuery {
3049    pub name: String,
3050    pub if_exists: bool,
3051}
3052
3053/// SELECT <columns> FROM QUEUE name [WHERE filter] [LIMIT n]
3054#[derive(Debug, Clone)]
3055pub struct QueueSelectQuery {
3056    pub queue: String,
3057    pub columns: Vec<String>,
3058    pub filter: Option<Filter>,
3059    pub limit: Option<u64>,
3060}
3061
3062/// Which end of the queue
3063#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3064pub enum QueueSide {
3065    Left,
3066    Right,
3067}
3068
3069/// Per-message delayed availability for `QUEUE PUSH` (PRD #718 / #722).
3070///
3071/// `DelayMs` is relative — the runtime resolves it against the push-time
3072/// wall clock. `AtUnixMs` is absolute — the runtime promotes it to
3073/// nanoseconds unchanged. Both ultimately surface to consumers as an
3074/// `available_at_ns` metadata field that delivery paths filter on.
3075#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3076pub enum QueueAvailability {
3077    /// Delay the first delivery by this many milliseconds from push time.
3078    DelayMs(u64),
3079    /// Make the message first-deliverable at this absolute unix-ms instant.
3080    AtUnixMs(u64),
3081}
3082
3083/// Queue operation commands
3084// The largest variant carries an inline `Filter`; boxing it would ripple
3085// to every construction and match site for a marginal stack-size win, so
3086// the size difference is accepted.
3087#[allow(clippy::large_enum_variant)]
3088#[derive(Debug, Clone)]
3089pub enum QueueCommand {
3090    Push {
3091        queue: String,
3092        value: Value,
3093        side: QueueSide,
3094        priority: Option<i32>,
3095        /// Optional per-message ordering key. Grouped delivery serializes
3096        /// messages sharing this key per consumer group.
3097        key: Option<String>,
3098        /// Per-message delayed availability (issue #722). `None` means the
3099        /// message is deliverable immediately. `Some(_)` resolves to an
3100        /// `available_at_ns` metadata field at push time; delivery paths
3101        /// (`QUEUE READ`, `QUEUE POP`, `QUEUE READ … WAIT`) refuse to
3102        /// deliver the message until that instant.
3103        available: Option<QueueAvailability>,
3104        /// Optional producer push idempotency key. Duplicate pushes with
3105        /// the same key on the same queue return the first message id
3106        /// while the queue's dedup window is active.
3107        dedup_key: Option<String>,
3108    },
3109    Pop {
3110        queue: String,
3111        side: QueueSide,
3112        count: usize,
3113    },
3114    Peek {
3115        queue: String,
3116        count: usize,
3117    },
3118    Len {
3119        queue: String,
3120    },
3121    Purge {
3122        queue: String,
3123    },
3124    GroupCreate {
3125        queue: String,
3126        group: String,
3127    },
3128    GroupRead {
3129        queue: String,
3130        group: Option<String>,
3131        consumer: String,
3132        count: usize,
3133        /// Optional blocking-read deadline in milliseconds (PRD #718 slice
3134        /// A: `QUEUE READ … WAIT <duration>`). `None` means classic
3135        /// non-blocking semantics. The runtime currently honors the field
3136        /// synchronously — the actual wait registry lands in slice C.
3137        wait_ms: Option<u64>,
3138    },
3139    Pending {
3140        queue: String,
3141        group: String,
3142    },
3143    Claim {
3144        queue: String,
3145        group: String,
3146        consumer: String,
3147        min_idle_ms: u64,
3148    },
3149    Ack {
3150        queue: String,
3151        // Legacy tuple handle. Empty `group` / `message_id` strings mean
3152        // the request relies solely on `delivery_id`. ADR 0026: when both
3153        // `delivery_id` and the tuple are supplied, `delivery_id` wins.
3154        group: String,
3155        message_id: String,
3156        /// Server-issued opaque base32 delivery handle (ADR 0026). When
3157        /// present, takes precedence over the legacy tuple; the tuple is
3158        /// kept for one minor release as a wire-compat bridge.
3159        delivery_id: Option<String>,
3160    },
3161    Nack {
3162        queue: String,
3163        group: String,
3164        message_id: String,
3165        delivery_id: Option<String>,
3166        /// Per-failure retry delay override (issue #723). `Some(ms)`
3167        /// requests that the failed message become re-deliverable only
3168        /// after `ms` milliseconds; takes precedence over the queue's
3169        /// default `retry_delay_ms`. Authorization is enforced at the
3170        /// runtime layer: requests from a read-only identity are
3171        /// rejected.
3172        delay_ms: Option<u64>,
3173    },
3174    Move {
3175        source: String,
3176        destination: String,
3177        filter: Option<Filter>,
3178        limit: usize,
3179    },
3180}
3181
3182// ============================================================================
3183// Tree DDL & Commands
3184// ============================================================================
3185
3186#[derive(Debug, Clone)]
3187pub struct TreeNodeSpec {
3188    pub label: String,
3189    pub node_type: Option<String>,
3190    pub properties: Vec<(String, Value)>,
3191    pub metadata: Vec<(String, Value)>,
3192    pub max_children: Option<usize>,
3193}
3194
3195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3196pub enum TreePosition {
3197    First,
3198    Last,
3199    Index(usize),
3200}
3201
3202#[derive(Debug, Clone)]
3203pub struct CreateTreeQuery {
3204    pub collection: String,
3205    pub name: String,
3206    pub root: TreeNodeSpec,
3207    pub default_max_children: usize,
3208    pub if_not_exists: bool,
3209}
3210
3211#[derive(Debug, Clone)]
3212pub struct DropTreeQuery {
3213    pub collection: String,
3214    pub name: String,
3215    pub if_exists: bool,
3216}
3217
3218#[derive(Debug, Clone)]
3219pub enum TreeCommand {
3220    Insert {
3221        collection: String,
3222        tree_name: String,
3223        parent_id: u64,
3224        node: TreeNodeSpec,
3225        position: TreePosition,
3226    },
3227    Move {
3228        collection: String,
3229        tree_name: String,
3230        node_id: u64,
3231        parent_id: u64,
3232        position: TreePosition,
3233    },
3234    Delete {
3235        collection: String,
3236        tree_name: String,
3237        node_id: u64,
3238    },
3239    Validate {
3240        collection: String,
3241        tree_name: String,
3242    },
3243    Rebalance {
3244        collection: String,
3245        tree_name: String,
3246        dry_run: bool,
3247    },
3248}
3249
3250// ============================================================================
3251// KV DSL Commands
3252// ============================================================================
3253
3254/// KV verb commands: `KV PUT key = value [EXPIRE n] [IF NOT EXISTS]`, `KV GET key`, `KV DELETE key`
3255#[derive(Debug, Clone)]
3256pub enum KvCommand {
3257    Put {
3258        model: CollectionModel,
3259        collection: String,
3260        key: String,
3261        value: Value,
3262        /// TTL in milliseconds (from EXPIRE clause)
3263        ttl_ms: Option<u64>,
3264        tags: Vec<String>,
3265        if_not_exists: bool,
3266    },
3267    InvalidateTags {
3268        collection: String,
3269        tags: Vec<String>,
3270    },
3271    Get {
3272        model: CollectionModel,
3273        collection: String,
3274        key: String,
3275    },
3276    Unseal {
3277        collection: String,
3278        key: String,
3279        version: Option<i64>,
3280    },
3281    Rotate {
3282        collection: String,
3283        key: String,
3284        value: Value,
3285        tags: Vec<String>,
3286    },
3287    History {
3288        collection: String,
3289        key: String,
3290    },
3291    List {
3292        model: CollectionModel,
3293        collection: String,
3294        prefix: Option<String>,
3295        limit: Option<usize>,
3296        offset: usize,
3297        as_json: bool,
3298    },
3299    Purge {
3300        collection: String,
3301        key: String,
3302    },
3303    Watch {
3304        model: CollectionModel,
3305        collection: String,
3306        key: String,
3307        prefix: bool,
3308        from_lsn: Option<u64>,
3309    },
3310    Delete {
3311        model: CollectionModel,
3312        collection: String,
3313        key: String,
3314    },
3315    /// `KV INCR key [BY n] [EXPIRE dur]` — atomic increment; negative `by` = decrement.
3316    Incr {
3317        model: CollectionModel,
3318        collection: String,
3319        key: String,
3320        /// Step value; negative for DECR. Defaults to 1.
3321        by: i64,
3322        ttl_ms: Option<u64>,
3323    },
3324    /// `KV CAS key EXPECT <expected|NULL> SET <new> [EXPIRE dur]` — compare-and-set.
3325    ///
3326    /// `expected = None` means `EXPECT NULL` (key must be absent).
3327    Cas {
3328        model: CollectionModel,
3329        collection: String,
3330        key: String,
3331        /// The value the caller expects to be current; `None` = key must be absent.
3332        expected: Option<Value>,
3333        new_value: Value,
3334        ttl_ms: Option<u64>,
3335    },
3336}
3337
3338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3339pub enum ConfigValueType {
3340    Bool,
3341    Int,
3342    String,
3343    Url,
3344    Object,
3345    Array,
3346}
3347
3348impl ConfigValueType {
3349    pub fn as_str(self) -> &'static str {
3350        match self {
3351            Self::Bool => "bool",
3352            Self::Int => "int",
3353            Self::String => "string",
3354            Self::Url => "url",
3355            Self::Object => "object",
3356            Self::Array => "array",
3357        }
3358    }
3359
3360    pub fn parse(input: &str) -> Option<Self> {
3361        match input.to_ascii_lowercase().as_str() {
3362            "bool" | "boolean" => Some(Self::Bool),
3363            "int" | "integer" => Some(Self::Int),
3364            "string" | "str" | "text" => Some(Self::String),
3365            "url" => Some(Self::Url),
3366            "object" | "json_object" => Some(Self::Object),
3367            "array" | "list" => Some(Self::Array),
3368            _ => None,
3369        }
3370    }
3371}
3372
3373#[derive(Debug, Clone)]
3374pub enum ConfigCommand {
3375    Put {
3376        collection: String,
3377        key: String,
3378        value: Value,
3379        value_type: Option<ConfigValueType>,
3380        tags: Vec<String>,
3381    },
3382    Get {
3383        collection: String,
3384        key: String,
3385    },
3386    Resolve {
3387        collection: String,
3388        key: String,
3389    },
3390    Rotate {
3391        collection: String,
3392        key: String,
3393        value: Value,
3394        value_type: Option<ConfigValueType>,
3395        tags: Vec<String>,
3396    },
3397    Delete {
3398        collection: String,
3399        key: String,
3400    },
3401    History {
3402        collection: String,
3403        key: String,
3404    },
3405    List {
3406        collection: String,
3407        prefix: Option<String>,
3408        limit: Option<usize>,
3409        offset: usize,
3410    },
3411    Watch {
3412        collection: String,
3413        key: String,
3414        prefix: bool,
3415        from_lsn: Option<u64>,
3416    },
3417    InvalidVolatileOperation {
3418        operation: String,
3419        collection: String,
3420        key: Option<String>,
3421    },
3422}
3423
3424#[cfg(test)]
3425mod tests {
3426    use super::*;
3427    use crate::ast::{Expr, Span};
3428
3429    fn table_expr(name: &str) -> QueryExpr {
3430        QueryExpr::Table(TableQuery::new(name))
3431    }
3432
3433    #[test]
3434    fn policy_target_kind_identifiers_cover_all_variants() {
3435        assert_eq!(PolicyTargetKind::Table.as_ident(), "table");
3436        assert_eq!(PolicyTargetKind::Nodes.as_ident(), "nodes");
3437        assert_eq!(PolicyTargetKind::Edges.as_ident(), "edges");
3438        assert_eq!(PolicyTargetKind::Vectors.as_ident(), "vectors");
3439        assert_eq!(PolicyTargetKind::Messages.as_ident(), "messages");
3440        assert_eq!(PolicyTargetKind::Points.as_ident(), "points");
3441        assert_eq!(PolicyTargetKind::Documents.as_ident(), "documents");
3442    }
3443
3444    #[test]
3445    fn index_method_display_names_are_sql_keywords() {
3446        assert_eq!(IndexMethod::BTree.to_string(), "BTREE");
3447        assert_eq!(IndexMethod::Hash.to_string(), "HASH");
3448        assert_eq!(IndexMethod::Bitmap.to_string(), "BITMAP");
3449        assert_eq!(IndexMethod::Spatial.to_string(), "SPATIAL");
3450        assert_eq!(IndexMethod::H3 { resolution: 9 }.to_string(), "H3");
3451    }
3452
3453    #[test]
3454    fn table_query_defaults_and_subquery_source() {
3455        let query = TableQuery::new("hosts");
3456        assert_eq!(query.table, "hosts");
3457        assert!(query.source.is_none());
3458        assert!(query.alias.is_none());
3459        assert!(query.select_items.is_empty());
3460        assert!(!query.distinct);
3461
3462        let subquery = table_expr("inner");
3463        let wrapped = TableQuery::from_subquery(subquery, Some("h".to_string()));
3464        assert_eq!(wrapped.table, "__subq_h");
3465        assert_eq!(wrapped.alias.as_deref(), Some("h"));
3466        assert!(matches!(wrapped.source, Some(TableSource::Subquery(_))));
3467
3468        let anonymous = TableQuery::from_subquery(table_expr("inner"), None);
3469        assert_eq!(anonymous.table, "__subq_anon");
3470        assert!(anonymous.alias.is_none());
3471    }
3472
3473    #[test]
3474    fn graph_pattern_query_and_components_builders_set_fields() {
3475        let node = NodePattern::new("h").of_label("Host").with_property(
3476            "os",
3477            CompareOp::Eq,
3478            Value::text("linux"),
3479        );
3480        assert_eq!(node.alias, "h");
3481        assert_eq!(node.node_label.as_deref(), Some("Host"));
3482        assert_eq!(node.properties.len(), 1);
3483
3484        let edge = EdgePattern::new("h", "s")
3485            .alias("r")
3486            .of_label("HAS_SERVICE")
3487            .direction(EdgeDirection::Incoming)
3488            .hops(2, 4);
3489        assert_eq!(edge.alias.as_deref(), Some("r"));
3490        assert_eq!(edge.edge_label.as_deref(), Some("HAS_SERVICE"));
3491        assert_eq!(edge.direction, EdgeDirection::Incoming);
3492        assert_eq!((edge.min_hops, edge.max_hops), (2, 4));
3493
3494        let pattern = GraphPattern::new().node(node).edge(edge);
3495        assert_eq!(pattern.nodes.len(), 1);
3496        assert_eq!(pattern.edges.len(), 1);
3497
3498        let graph = GraphQuery::new(pattern).alias("g");
3499        assert_eq!(graph.alias.as_deref(), Some("g"));
3500        assert!(graph.filter.is_none());
3501        assert!(graph.return_.is_empty());
3502    }
3503
3504    #[test]
3505    fn join_condition_and_join_query_defaults() {
3506        let left = FieldRef::column("hosts", "id");
3507        let right = FieldRef::node_id("h");
3508        let condition = JoinCondition::new(left.clone(), right.clone());
3509        assert_eq!(condition.left_field, left);
3510        assert_eq!(condition.right_field, right);
3511
3512        let join = JoinQuery::new(
3513            table_expr("hosts"),
3514            table_expr("services"),
3515            condition.clone(),
3516        )
3517        .join_type(JoinType::LeftOuter);
3518        assert!(matches!(*join.left, QueryExpr::Table(_)));
3519        assert!(matches!(*join.right, QueryExpr::Table(_)));
3520        assert_eq!(join.join_type, JoinType::LeftOuter);
3521        assert_eq!(join.on.left_field, condition.left_field);
3522        assert!(join.return_items.is_empty());
3523    }
3524
3525    #[test]
3526    fn field_ref_projection_filter_and_order_builders() {
3527        let column = FieldRef::column("hosts", "ip");
3528        let node_prop = FieldRef::node_prop("h", "os");
3529        let edge_prop = FieldRef::edge_prop("r", "weight");
3530        assert!(matches!(column, FieldRef::TableColumn { .. }));
3531        assert!(matches!(node_prop, FieldRef::NodeProperty { .. }));
3532        assert!(matches!(edge_prop, FieldRef::EdgeProperty { .. }));
3533
3534        assert!(matches!(
3535            Projection::from_field(column.clone()),
3536            Projection::Field(_, None)
3537        ));
3538        assert!(matches!(
3539            Projection::column("ip"),
3540            Projection::Column(ref name) if name == "ip"
3541        ));
3542        assert!(matches!(
3543            Projection::with_alias("ip", "addr"),
3544            Projection::Alias(ref column, ref alias) if column == "ip" && alias == "addr"
3545        ));
3546
3547        let eq = Filter::compare(column.clone(), CompareOp::Eq, Value::text("127.0.0.1"));
3548        let gt = Filter::compare(node_prop, CompareOp::Gt, Value::Integer(7));
3549        let combined = eq.clone().and(gt.clone()).or(eq.clone().not());
3550        assert!(matches!(combined, Filter::Or(_, _)));
3551        assert!(matches!(gt.optimize(), Filter::Compare { .. }));
3552
3553        assert_eq!(CompareOp::Eq.to_string(), "=");
3554        assert_eq!(CompareOp::Ne.to_string(), "<>");
3555        assert_eq!(CompareOp::Lt.to_string(), "<");
3556        assert_eq!(CompareOp::Le.to_string(), "<=");
3557        assert_eq!(CompareOp::Gt.to_string(), ">");
3558        assert_eq!(CompareOp::Ge.to_string(), ">=");
3559
3560        let asc = OrderByClause::asc(column.clone());
3561        assert!(asc.ascending);
3562        assert!(!asc.nulls_first);
3563        assert!(asc.expr.is_none());
3564
3565        let desc = OrderByClause::desc(column).with_expr(Expr::lit(Value::Integer(1)));
3566        assert!(!desc.ascending);
3567        assert!(desc.nulls_first);
3568        assert!(desc.expr.is_some());
3569    }
3570
3571    #[test]
3572    fn path_and_node_selector_builders_set_expected_defaults() {
3573        let from = NodeSelector::by_id("a");
3574        let to = NodeSelector::by_row("hosts", 42);
3575        let path = PathQuery::new(from, to).alias("p").via_label("CONNECTS_TO");
3576        assert_eq!(path.alias.as_deref(), Some("p"));
3577        assert_eq!(path.via, vec!["CONNECTS_TO"]);
3578        assert_eq!(path.max_length, 10);
3579        assert!(path.filter.is_none());
3580
3581        assert!(matches!(NodeSelector::by_id("n1"), NodeSelector::ById(id) if id == "n1"));
3582        assert!(matches!(
3583            NodeSelector::by_label("Host"),
3584            NodeSelector::ByType { node_label, filter: None } if node_label == "Host"
3585        ));
3586        assert!(matches!(
3587            NodeSelector::by_row("hosts", 7),
3588            NodeSelector::ByRow { table, row_id } if table == "hosts" && row_id == 7
3589        ));
3590    }
3591
3592    #[test]
3593    fn vector_and_hybrid_builders_set_options() {
3594        let literal = VectorSource::literal(vec![0.1, 0.2]);
3595        assert!(matches!(literal, VectorSource::Literal(ref values) if values == &[0.1, 0.2]));
3596        assert!(matches!(VectorSource::text("ssh"), VectorSource::Text(ref text) if text == "ssh"));
3597        assert!(matches!(
3598            VectorSource::reference("embeddings", 9),
3599            VectorSource::Reference { collection, vector_id }
3600                if collection == "embeddings" && vector_id == 9
3601        ));
3602
3603        let vector = VectorQuery::new("embeddings", VectorSource::text("ssh"))
3604            .limit(3)
3605            .with_filter(MetadataFilter::eq("source", "nmap"))
3606            .with_vectors()
3607            .min_similarity(0.8)
3608            .alias("sim");
3609        assert_eq!(vector.collection, "embeddings");
3610        assert_eq!(vector.k, 3);
3611        assert!(vector.filter.is_some());
3612        assert!(vector.include_vectors);
3613        assert!(vector.include_metadata);
3614        assert_eq!(vector.threshold, Some(0.8));
3615        assert_eq!(vector.alias.as_deref(), Some("sim"));
3616
3617        let hybrid = HybridQuery::new(table_expr("hosts"), vector)
3618            .with_fusion(FusionStrategy::RRF { k: 60 })
3619            .limit(5)
3620            .alias("hy");
3621        assert!(matches!(*hybrid.structured, QueryExpr::Table(_)));
3622        assert_eq!(hybrid.limit, Some(5));
3623        assert_eq!(hybrid.alias.as_deref(), Some("hy"));
3624        assert!(matches!(hybrid.fusion, FusionStrategy::RRF { k: 60 }));
3625    }
3626
3627    #[test]
3628    fn window_spec_structs_are_constructible() {
3629        let order = WindowOrderItem {
3630            expr: Expr::lit(Value::Integer(1)),
3631            ascending: false,
3632            nulls_first: true,
3633        };
3634        let frame = WindowFrame {
3635            unit: WindowFrameUnit::Rows,
3636            start: WindowFrameBound::UnboundedPreceding,
3637            end: Some(WindowFrameBound::CurrentRow),
3638        };
3639        let spec = WindowSpec {
3640            partition_by: vec![Expr::lit(Value::text("tenant"))],
3641            order_by: vec![order],
3642            frame: Some(frame),
3643        };
3644        assert_eq!(spec.partition_by.len(), 1);
3645        assert_eq!(spec.order_by.len(), 1);
3646        assert!(matches!(
3647            spec.frame,
3648            Some(WindowFrame {
3649                unit: WindowFrameUnit::Rows,
3650                ..
3651            })
3652        ));
3653
3654        let default = WindowSpec::default();
3655        assert!(default.partition_by.is_empty());
3656        assert!(default.order_by.is_empty());
3657        assert!(default.frame.is_none());
3658
3659        let _ = Span::synthetic();
3660    }
3661
3662    #[test]
3663    fn config_value_type_aliases_and_display_names() {
3664        for (input, expected, as_str) in [
3665            ("bool", ConfigValueType::Bool, "bool"),
3666            ("boolean", ConfigValueType::Bool, "bool"),
3667            ("int", ConfigValueType::Int, "int"),
3668            ("integer", ConfigValueType::Int, "int"),
3669            ("str", ConfigValueType::String, "string"),
3670            ("text", ConfigValueType::String, "string"),
3671            ("url", ConfigValueType::Url, "url"),
3672            ("json_object", ConfigValueType::Object, "object"),
3673            ("list", ConfigValueType::Array, "array"),
3674        ] {
3675            let parsed = ConfigValueType::parse(input).expect("known type");
3676            assert_eq!(parsed, expected);
3677            assert_eq!(parsed.as_str(), as_str);
3678        }
3679        assert_eq!(ConfigValueType::parse("bogus"), None);
3680    }
3681}
3682
3683// ============================================================================
3684// Builders (Fluent API)
3685// ============================================================================