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