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