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