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