Skip to main content

reddb_rql/
core.rs

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