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