Skip to main content

reddb_server/storage/query/
core.rs

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