heliosdb_nano/sql/logical_plan.rs
1//! Logical query plan structures
2//!
3//! This module defines the logical plan representation for SQL queries.
4//! The logical plan is a tree of operators that represents the semantics
5//! of a query without specifying how it should be executed.
6
7use crate::{Schema, DataType, Value};
8#[cfg(feature = "ha-tier1")]
9use crate::Column;
10use serde::{Serialize, Deserialize};
11use std::sync::Arc;
12
13use super::explain_options::ExplainOptions;
14
15/// Helper module for `Arc<Schema>` serialization
16mod arc_schema_serde {
17 use super::*;
18 use serde::{Deserializer, Serializer};
19
20 pub fn serialize<S>(schema: &Arc<Schema>, serializer: S) -> Result<S::Ok, S::Error>
21 where
22 S: Serializer,
23 {
24 schema.as_ref().serialize(serializer)
25 }
26
27 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<Schema>, D::Error>
28 where
29 D: Deserializer<'de>,
30 {
31 let schema = Schema::deserialize(deserializer)?;
32 Ok(Arc::new(schema))
33 }
34}
35
36/// Trigger timing specification (BEFORE, AFTER, INSTEAD OF)
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub enum TriggerTiming {
39 /// Fire before the operation
40 Before,
41 /// Fire after the operation
42 After,
43 /// Fire instead of the operation (views/tables only)
44 InsteadOf,
45}
46
47/// Trigger event (INSERT, UPDATE, DELETE, TRUNCATE)
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub enum TriggerEvent {
50 /// INSERT event
51 Insert,
52 /// UPDATE event (optionally restricted to columns)
53 Update(Option<Vec<String>>),
54 /// DELETE event
55 Delete,
56 /// TRUNCATE event (PostgreSQL: FOR EACH STATEMENT only)
57 Truncate,
58}
59
60/// Trigger for each clause (ROW or STATEMENT)
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub enum TriggerFor {
63 /// Fire once per affected row
64 Row,
65 /// Fire once per statement
66 Statement,
67}
68
69/// Transition table reference for REFERENCING clause
70/// Used in statement-level triggers to access all affected rows as a virtual table
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub enum TransitionTable {
73 /// OLD TABLE AS alias_name - access old row values (UPDATE/DELETE)
74 OldTable { alias: String },
75 /// NEW TABLE AS alias_name - access new row values (INSERT/UPDATE)
76 NewTable { alias: String },
77}
78
79/// A single item in a RETURNING clause
80///
81/// Represents one expression in `RETURNING expr1, expr2, ...`
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub enum ReturningItem {
84 /// Wildcard (`RETURNING *`) - return all columns
85 Wildcard,
86 /// A simple column reference (`RETURNING col_name`)
87 Column(String),
88 /// An arbitrary expression with an alias (`RETURNING expr AS alias`)
89 Expression {
90 /// The expression to evaluate
91 expr: LogicalExpr,
92 /// The output column name/alias
93 alias: String,
94 },
95}
96
97/// ON CONFLICT action for INSERT ... ON CONFLICT DO NOTHING / DO UPDATE
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99pub enum OnConflictAction {
100 /// ON CONFLICT DO NOTHING — silently skip conflicting rows
101 DoNothing,
102 /// ON CONFLICT DO UPDATE SET col = expr — upsert semantics
103 DoUpdate {
104 /// Column assignments (col_name, expression).
105 /// Expressions may reference `EXCLUDED.col` for the proposed insert values.
106 assignments: Vec<(String, LogicalExpr)>,
107 },
108}
109
110/// Trigger type: Regular or Constraint
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
112pub enum TriggerType {
113 /// Regular trigger (default)
114 #[default]
115 Regular,
116 /// Constraint trigger - always deferrable, fires at commit
117 Constraint,
118}
119
120/// Trigger execution characteristics for deferred execution
121/// PostgreSQL-compatible DEFERRABLE and INITIALLY DEFERRED/IMMEDIATE
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
123pub struct TriggerCharacteristics {
124 /// Whether the trigger can be deferred (DEFERRABLE vs NOT DEFERRABLE)
125 pub deferrable: bool,
126 /// Whether the trigger starts deferred (INITIALLY DEFERRED vs INITIALLY IMMEDIATE)
127 pub initially_deferred: bool,
128}
129
130impl TriggerCharacteristics {
131 /// Create default non-deferrable trigger characteristics
132 pub fn new() -> Self {
133 Self::default()
134 }
135
136 /// Create deferrable trigger that starts immediate
137 pub fn deferrable() -> Self {
138 Self {
139 deferrable: true,
140 initially_deferred: false,
141 }
142 }
143
144 /// Create deferrable trigger that starts deferred
145 pub fn deferrable_initially_deferred() -> Self {
146 Self {
147 deferrable: true,
148 initially_deferred: true,
149 }
150 }
151}
152
153/// Logical plan node
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub enum LogicalPlan {
156 /// Scan a table
157 Scan {
158 /// Table name
159 table_name: String,
160 /// Table alias (for JOIN disambiguation)
161 alias: Option<String>,
162 /// Schema of the table
163 #[serde(with = "arc_schema_serde")]
164 schema: Arc<Schema>,
165 /// Optional projection (column indices)
166 projection: Option<Vec<usize>>,
167 /// Time-travel AS OF clause
168 as_of: Option<AsOfClause>,
169 },
170
171 /// Scan with storage-level predicate pushdown
172 /// This combines scanning and filtering at the storage layer for efficiency
173 FilteredScan {
174 /// Table name
175 table_name: String,
176 /// Table alias (for JOIN disambiguation)
177 alias: Option<String>,
178 /// Schema of the table
179 #[serde(with = "arc_schema_serde")]
180 schema: Arc<Schema>,
181 /// Optional projection (column indices)
182 projection: Option<Vec<usize>>,
183 /// Predicate pushed down to storage layer
184 predicate: Option<LogicalExpr>,
185 /// Time-travel AS OF clause
186 as_of: Option<AsOfClause>,
187 },
188
189 /// Filter rows based on a predicate
190 Filter {
191 /// Input plan
192 input: Box<LogicalPlan>,
193 /// Filter predicate
194 predicate: LogicalExpr,
195 },
196
197 /// Project columns
198 Project {
199 /// Input plan
200 input: Box<LogicalPlan>,
201 /// Expressions to project
202 exprs: Vec<LogicalExpr>,
203 /// Aliases for projected columns
204 aliases: Vec<String>,
205 /// Whether to deduplicate results (DISTINCT)
206 distinct: bool,
207 /// DISTINCT ON expressions (PostgreSQL extension)
208 distinct_on: Option<Vec<LogicalExpr>>,
209 },
210
211 /// Aggregate
212 Aggregate {
213 /// Input plan
214 input: Box<LogicalPlan>,
215 /// Group by expressions
216 group_by: Vec<LogicalExpr>,
217 /// Aggregate expressions
218 aggr_exprs: Vec<LogicalExpr>,
219 /// HAVING clause (filter on aggregated results)
220 having: Option<LogicalExpr>,
221 },
222
223 /// Join two plans
224 Join {
225 /// Left input
226 left: Box<LogicalPlan>,
227 /// Right input
228 right: Box<LogicalPlan>,
229 /// Join type
230 join_type: JoinType,
231 /// Join condition
232 on: Option<LogicalExpr>,
233 /// LATERAL join - right side can reference left side columns
234 #[serde(default)]
235 lateral: bool,
236 },
237
238 /// Sort
239 Sort {
240 /// Input plan
241 input: Box<LogicalPlan>,
242 /// Sort expressions
243 exprs: Vec<LogicalExpr>,
244 /// Ascending or descending for each expression
245 asc: Vec<bool>,
246 },
247
248 /// Limit
249 Limit {
250 /// Input plan
251 input: Box<LogicalPlan>,
252 /// Number of rows to return (resolved at plan time when the
253 /// SQL used a literal; set to `usize::MAX` as a sentinel when
254 /// `LIMIT $N` was used — the executor resolves the real value
255 /// from `limit_param` at execution time).
256 limit: usize,
257 /// Number of rows to skip (same semantics as `limit`).
258 offset: usize,
259 /// When `Some(n)`, the real LIMIT was `$n` — executor must
260 /// resolve from parameters before running.
261 #[serde(default)]
262 limit_param: Option<usize>,
263 /// When `Some(n)`, the real OFFSET was `$n` — executor must
264 /// resolve from parameters before running.
265 #[serde(default)]
266 offset_param: Option<usize>,
267 },
268
269 /// UNION - combine results from two queries
270 Union {
271 /// Left input plan
272 left: Box<LogicalPlan>,
273 /// Right input plan
274 right: Box<LogicalPlan>,
275 /// If true, keep duplicates (UNION ALL)
276 all: bool,
277 },
278
279 /// INTERSECT - rows that appear in both queries
280 Intersect {
281 /// Left input plan
282 left: Box<LogicalPlan>,
283 /// Right input plan
284 right: Box<LogicalPlan>,
285 /// If true, keep duplicates (INTERSECT ALL)
286 all: bool,
287 },
288
289 /// EXCEPT - rows in left that don't appear in right
290 Except {
291 /// Left input plan
292 left: Box<LogicalPlan>,
293 /// Right input plan
294 right: Box<LogicalPlan>,
295 /// If true, keep duplicates (EXCEPT ALL)
296 all: bool,
297 },
298
299 /// Insert values
300 Insert {
301 /// Table name
302 table_name: String,
303 /// Column names (if specified)
304 columns: Option<Vec<String>>,
305 /// Values to insert
306 values: Vec<Vec<LogicalExpr>>,
307 /// RETURNING clause (expressions to return from affected rows)
308 returning: Option<Vec<ReturningItem>>,
309 /// ON CONFLICT action (DO NOTHING or DO UPDATE)
310 on_conflict: Option<OnConflictAction>,
311 },
312
313 /// Insert from a SELECT query (INSERT INTO ... SELECT ...)
314 InsertSelect {
315 /// Target table name
316 table_name: String,
317 /// Target column names (if specified)
318 columns: Option<Vec<String>>,
319 /// Source SELECT query plan
320 source: Box<LogicalPlan>,
321 /// RETURNING clause (expressions to return from affected rows)
322 returning: Option<Vec<ReturningItem>>,
323 },
324
325 /// Create table
326 CreateTable {
327 /// Table name
328 name: String,
329 /// Column definitions
330 columns: Vec<ColumnDef>,
331 /// If table already exists, do nothing
332 if_not_exists: bool,
333 /// Table constraints (FK, CHECK, UNIQUE)
334 #[serde(default)]
335 constraints: Vec<TableConstraint>,
336 },
337
338 /// Drop table
339 DropTable {
340 /// Table name
341 name: String,
342 /// If table doesn't exist, do nothing
343 if_exists: bool,
344 },
345
346 /// Truncate table (remove all rows)
347 Truncate {
348 /// Table name
349 table_name: String,
350 },
351
352 /// Update rows
353 Update {
354 /// Table name
355 table_name: String,
356 /// Assignments (column name, value expression)
357 assignments: Vec<(String, LogicalExpr)>,
358 /// Optional WHERE clause
359 selection: Option<LogicalExpr>,
360 /// RETURNING clause (expressions to return from affected rows)
361 returning: Option<Vec<ReturningItem>>,
362 },
363
364 /// Delete rows
365 Delete {
366 /// Table name
367 table_name: String,
368 /// Optional WHERE clause
369 selection: Option<LogicalExpr>,
370 /// RETURNING clause (expressions to return from affected rows)
371 returning: Option<Vec<ReturningItem>>,
372 },
373
374 /// Create index
375 CreateIndex {
376 /// Index name
377 name: String,
378 /// Table name
379 table_name: String,
380 /// Column name
381 column_name: String,
382 /// Index type (e.g., "hnsw")
383 index_type: Option<String>,
384 /// Index options (e.g., quantization, pq_subquantizers, etc.)
385 options: Vec<IndexOption>,
386 /// If index already exists, do nothing
387 if_not_exists: bool,
388 },
389
390 /// Create a named sequence (for nextval / currval / setval).
391 ///
392 /// HeliosDB's sequences are currently process-scoped in-memory
393 /// counters (see `evaluator::sequences`). This is enough to unblock
394 /// Prisma / Drizzle / Django migrations that emit
395 /// `CREATE SEQUENCE` DDL but don't rely on cross-connection
396 /// monotonicity. Persistent sequences are a follow-up.
397 CreateSequence {
398 /// Sequence name (already normalised)
399 name: String,
400 /// Silently succeed if the sequence already exists
401 if_not_exists: bool,
402 },
403
404 /// `CREATE EXTENSION <name>` — install a named extension. For
405 /// `hdb_code` this runs the code-graph bootstrap (see
406 /// `src/code_graph/storage.rs::bootstrap_tables`). Unknown
407 /// extension names return a clear error unless `if_not_exists`
408 /// is set.
409 CreateExtension {
410 /// Extension name (already normalised, lower-case).
411 name: String,
412 /// Silently succeed if the extension is already installed or
413 /// unrecognised in a known-safe way.
414 if_not_exists: bool,
415 },
416
417 /// `CREATE DATABASE <name>` — registers a new tenant in the
418 /// in-memory `TenantManager` with `IsolationMode::DatabasePerTenant`.
419 /// Reserved names (`heliosdb`, `postgres`) are refused unless
420 /// `IF NOT EXISTS` is set. This is the SQL surface for Bug 1 from
421 /// the dashboard-migration triage; it intentionally wraps the
422 /// existing tenant-management infrastructure rather than
423 /// introducing a new storage layer.
424 CreateDatabase {
425 /// Database / tenant name (case-preserving, normalised below).
426 name: String,
427 /// Silently succeed if the name is already registered or is
428 /// one of the reserved names.
429 if_not_exists: bool,
430 },
431
432 /// `DROP DATABASE <name>` — removes a tenant from the
433 /// `TenantManager`. Reserved names are always refused.
434 DropDatabase {
435 /// Database / tenant name.
436 name: String,
437 /// Silently succeed if the name doesn't exist.
438 if_exists: bool,
439 },
440
441 /// `DROP EXTENSION <name>` — uninstall. Phase 1 drops the
442 /// `_hdb_code_*` tables (cascade-style); future phases may keep
443 /// data and only unregister functions.
444 DropExtension {
445 name: String,
446 if_exists: bool,
447 },
448
449 /// Alter column storage mode
450 /// Changes per-column storage mode (Dictionary, Content-Addressed, Columnar)
451 AlterColumnStorage {
452 /// Table name
453 table_name: String,
454 /// Column name
455 column_name: String,
456 /// New storage mode
457 storage_mode: crate::ColumnStorageMode,
458 },
459
460 // === ALTER TABLE operations ===
461
462 /// Add a column to an existing table
463 AlterTableAddColumn {
464 /// Table name
465 table_name: String,
466 /// Column definition
467 column_def: ColumnDef,
468 /// IF NOT EXISTS
469 if_not_exists: bool,
470 },
471
472 /// Drop a column from an existing table
473 AlterTableDropColumn {
474 /// Table name
475 table_name: String,
476 /// Column name to drop
477 column_name: String,
478 /// IF EXISTS
479 if_exists: bool,
480 /// CASCADE
481 cascade: bool,
482 },
483
484 /// Rename a column in an existing table
485 AlterTableRenameColumn {
486 /// Table name
487 table_name: String,
488 /// Old column name
489 old_column_name: String,
490 /// New column name
491 new_column_name: String,
492 },
493
494 /// Rename a table
495 AlterTableRename {
496 /// Current table name
497 table_name: String,
498 /// New table name
499 new_table_name: String,
500 },
501
502 /// Add a foreign-key constraint to an existing table.
503 /// Mirrors the inline `REFERENCES …` form already supported in
504 /// CREATE TABLE; persists FK metadata via
505 /// `catalog.add_foreign_key` and creates the FK ART index.
506 AlterTableAddForeignKey {
507 /// Table name (owning the FK)
508 table_name: String,
509 /// Optional constraint name (auto-generated if absent)
510 constraint_name: Option<String>,
511 /// Local columns
512 columns: Vec<String>,
513 /// Referenced (parent) table
514 references_table: String,
515 /// Referenced (parent) columns
516 references_columns: Vec<String>,
517 /// ON DELETE action (logical-plan form, converted at executor time)
518 on_delete: Option<ReferentialAction>,
519 /// ON UPDATE action (logical-plan form, converted at executor time)
520 on_update: Option<ReferentialAction>,
521 /// DEFERRABLE
522 deferrable: bool,
523 /// INITIALLY DEFERRED
524 initially_deferred: bool,
525 },
526
527 /// Multiple ALTER TABLE operations in a single statement
528 AlterTableMulti {
529 /// The individual ALTER TABLE operations to execute sequentially
530 operations: Vec<LogicalPlan>,
531 },
532
533 // === Phase 3: Database Branching ===
534
535 /// Create a database branch
536 CreateBranch {
537 /// Branch name
538 branch_name: String,
539 /// Parent branch (None = CURRENT)
540 parent: Option<String>,
541 /// Creation point
542 as_of: AsOfClause,
543 /// Branch options
544 options: Vec<BranchOption>,
545 },
546
547 /// Drop a database branch
548 DropBranch {
549 /// Branch name
550 branch_name: String,
551 /// If branch doesn't exist, do nothing
552 if_exists: bool,
553 },
554
555 /// Merge a branch into another
556 MergeBranch {
557 /// Source branch
558 source: String,
559 /// Target branch
560 target: String,
561 /// Merge options
562 options: Vec<MergeOption>,
563 },
564
565 /// Switch to a database branch
566 UseBranch {
567 /// Branch name to switch to
568 branch_name: String,
569 },
570
571 /// List all database branches
572 ShowBranches,
573
574 // === Phase 3: Materialized Views ===
575
576 /// Create materialized view
577 CreateMaterializedView {
578 /// View name
579 name: String,
580 /// Query definition
581 query: Box<LogicalPlan>,
582 /// Materialized view options
583 options: Vec<MaterializedViewOption>,
584 /// If view already exists, do nothing
585 if_not_exists: bool,
586 },
587
588 /// Refresh materialized view
589 RefreshMaterializedView {
590 /// View name
591 name: String,
592 /// Concurrent refresh (doesn't block reads)
593 concurrent: bool,
594 /// Use incremental refresh (apply deltas instead of full recompute)
595 incremental: bool,
596 },
597
598 /// Drop materialized view
599 DropMaterializedView {
600 /// View name
601 name: String,
602 /// If view doesn't exist, do nothing
603 if_exists: bool,
604 },
605
606 /// Alter materialized view options
607 AlterMaterializedView {
608 /// View name
609 name: String,
610 /// Options to set (key-value pairs)
611 options: std::collections::HashMap<String, String>,
612 },
613
614 // === Regular Views (non-materialized) ===
615
616 /// Create a regular view (virtual table)
617 CreateView {
618 /// View name
619 name: String,
620 /// Query definition (stored as SQL string for expansion)
621 query_sql: String,
622 /// If view already exists, do nothing
623 if_not_exists: bool,
624 /// OR REPLACE - replace existing view
625 or_replace: bool,
626 },
627
628 /// Drop a regular view
629 DropView {
630 /// View name
631 name: String,
632 /// If view doesn't exist, do nothing
633 if_exists: bool,
634 },
635
636 // === Phase 3: System Views ===
637
638 /// Query a system view (e.g., pg_database_branches())
639 SystemView {
640 /// View name
641 name: String,
642 /// Arguments (for function-like views)
643 args: Vec<LogicalExpr>,
644 },
645
646 // === Table Functions ===
647
648 /// Table-valued function (e.g., generate_series, unnest)
649 ///
650 /// Produces a virtual table from a function call in the FROM clause.
651 /// For example: `SELECT * FROM generate_series(1, 10)`
652 TableFunction {
653 /// Function name (e.g., "generate_series", "unnest")
654 function_name: String,
655 /// Evaluated arguments as logical expressions
656 args: Vec<LogicalExpr>,
657 /// Optional alias for the virtual table
658 alias: Option<String>,
659 },
660
661 /// Common Table Expression (WITH clause)
662 With {
663 /// CTE definitions (name -> query plan -> optional column aliases)
664 /// Column aliases rename the output columns (e.g., `nums(n)` renames column to `n`)
665 ctes: Vec<(String, Box<LogicalPlan>, Option<Vec<String>>)>,
666 /// Main query plan
667 query: Box<LogicalPlan>,
668 /// Whether this is WITH RECURSIVE
669 recursive: bool,
670 },
671
672 /// Create a trigger
673 CreateTrigger {
674 /// Trigger name
675 name: String,
676 /// Table name
677 table_name: String,
678 /// Trigger timing (BEFORE, AFTER, INSTEAD OF)
679 timing: TriggerTiming,
680 /// Trigger events (INSERT, UPDATE, DELETE)
681 events: Vec<TriggerEvent>,
682 /// For each row or statement
683 for_each: TriggerFor,
684 /// Optional WHEN clause condition
685 when_condition: Option<Box<LogicalExpr>>,
686 /// Trigger body statements
687 body: Vec<LogicalPlan>,
688 /// If trigger already exists, do nothing
689 if_not_exists: bool,
690 /// REFERENCING clause: transition table aliases for statement-level triggers
691 referencing: Vec<TransitionTable>,
692 /// DEFERRABLE characteristics (DEFERRABLE, INITIALLY DEFERRED/IMMEDIATE)
693 characteristics: TriggerCharacteristics,
694 /// Trigger type (Regular or Constraint)
695 trigger_type: TriggerType,
696 /// Referenced constraint name (for CONSTRAINT triggers with FROM clause)
697 from_constraint: Option<String>,
698 },
699
700 /// Drop a trigger
701 DropTrigger {
702 /// Trigger name
703 name: String,
704 /// Table name (optional for some databases)
705 table_name: Option<String>,
706 /// If trigger doesn't exist, do nothing
707 if_exists: bool,
708 },
709
710 /// Explain a query plan
711 Explain {
712 /// The plan to explain
713 input: Box<LogicalPlan>,
714 /// EXPLAIN options (FORMAT, ANALYZE, VERBOSE, STORAGE, AI, etc.)
715 options: ExplainOptions,
716 },
717
718 // === Transaction Control ===
719
720 /// Start a transaction
721 StartTransaction,
722
723 /// Commit the current transaction
724 Commit,
725
726 /// Rollback the current transaction
727 Rollback,
728
729 /// Create a savepoint within a transaction
730 Savepoint {
731 /// Savepoint name
732 name: String,
733 },
734
735 /// Release (commit) a savepoint
736 ReleaseSavepoint {
737 /// Savepoint name
738 name: String,
739 },
740
741 /// Rollback to a savepoint
742 RollbackToSavepoint {
743 /// Savepoint name
744 name: String,
745 },
746
747 // === Prepared Statements ===
748
749 /// Prepare a statement for later execution
750 Prepare {
751 /// Statement name
752 name: String,
753 /// Parameter data types (optional)
754 param_types: Vec<DataType>,
755 /// The statement to prepare
756 statement: Box<LogicalPlan>,
757 },
758
759 /// Execute a prepared statement
760 Execute {
761 /// Statement name
762 name: String,
763 /// Parameter values
764 parameters: Vec<LogicalExpr>,
765 },
766
767 /// Deallocate (drop) a prepared statement
768 Deallocate {
769 /// Statement name (None = ALL)
770 name: Option<String>,
771 },
772
773 /// SET CONSTRAINTS - change deferral mode for constraints/triggers
774 SetConstraints {
775 /// Constraint/trigger names (empty = ALL)
776 names: Vec<String>,
777 /// Whether to defer until commit
778 deferred: bool,
779 },
780
781 // === Procedural ===
782
783 /// Create a stored function
784 CreateFunction {
785 /// Function name
786 name: String,
787 /// Replace if exists
788 or_replace: bool,
789 /// Function parameters
790 params: Vec<FunctionParam>,
791 /// Return type
792 return_type: Option<crate::types::DataType>,
793 /// Function body (procedural code)
794 body: String,
795 /// Language (plpgsql, sql, etc.)
796 language: String,
797 /// Volatility (IMMUTABLE, STABLE, VOLATILE)
798 volatility: Option<String>,
799 },
800
801 /// Create a stored procedure
802 CreateProcedure {
803 /// Procedure name
804 name: String,
805 /// Replace if exists
806 or_replace: bool,
807 /// Procedure parameters
808 params: Vec<FunctionParam>,
809 /// Procedure body (procedural code)
810 body: String,
811 /// Language (plpgsql, sql, etc.)
812 language: String,
813 },
814
815 /// Drop a function
816 DropFunction {
817 /// Function name
818 name: String,
819 /// If exists clause
820 if_exists: bool,
821 },
822
823 /// Drop a procedure
824 DropProcedure {
825 /// Procedure name
826 name: String,
827 /// If exists clause
828 if_exists: bool,
829 },
830
831 /// Call a stored procedure
832 Call {
833 /// Procedure name
834 name: String,
835 /// Arguments
836 args: Vec<LogicalExpr>,
837 },
838
839 // === Utility ===
840
841 /// Dual scan for SELECT without FROM (like Oracle's DUAL)
842 /// Returns a single row with no columns for expression evaluation
843 DualScan,
844
845 // === HA Operations ===
846
847 /// Controlled switchover to a target standby node
848 /// Example: SELECT helios_switchover('node-uuid')
849 #[cfg(feature = "ha-tier1")]
850 Switchover {
851 /// Target node ID (UUID) to promote to primary
852 target_node: String,
853 },
854
855 /// Check switchover preconditions before executing
856 /// Example: SELECT helios_switchover_check('node-uuid')
857 #[cfg(feature = "ha-tier1")]
858 SwitchoverCheck {
859 /// Target node ID (UUID) to check
860 target_node: String,
861 },
862
863 /// Show HA cluster status (primary, standbys, lag, etc.)
864 /// Example: SELECT * FROM helios_cluster_status()
865 #[cfg(feature = "ha-tier1")]
866 ClusterStatus,
867
868 /// Set or remove a node alias
869 /// Example: SET NODE ALIAS 'my-standby' FOR 'node-uuid'
870 /// Example: SET NODE ALIAS NULL FOR 'node-uuid' (removes alias)
871 #[cfg(feature = "ha-tier1")]
872 SetNodeAlias {
873 /// Node identifier (UUID or existing alias)
874 node_id: String,
875 /// New alias (None to remove)
876 alias: Option<String>,
877 },
878
879 /// Show detailed cluster topology with health information
880 /// Example: SHOW TOPOLOGY
881 #[cfg(feature = "ha-tier1")]
882 ShowTopology,
883}
884
885/// Function/Procedure parameter
886#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
887pub struct FunctionParam {
888 /// Parameter name
889 pub name: String,
890 /// Parameter data type
891 pub data_type: crate::types::DataType,
892 /// Parameter mode (IN, OUT, INOUT)
893 pub mode: ParamMode,
894 /// Default value expression
895 pub default: Option<LogicalExpr>,
896}
897
898/// Parameter mode
899#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
900pub enum ParamMode {
901 In,
902 Out,
903 InOut,
904}
905
906/// Logical expression
907#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
908pub enum LogicalExpr {
909 /// Column reference
910 Column {
911 /// Optional table alias or name for disambiguation in JOINs
912 table: Option<String>,
913 /// Column name
914 name: String,
915 },
916
917 /// Literal value
918 Literal(Value),
919
920 /// Binary expression (a + b, a = b, etc.)
921 BinaryExpr {
922 /// Left expression
923 left: Box<LogicalExpr>,
924 /// Operator
925 op: BinaryOperator,
926 /// Right expression
927 right: Box<LogicalExpr>,
928 },
929
930 /// Unary expression (NOT a, -a, etc.)
931 UnaryExpr {
932 /// Operator
933 op: UnaryOperator,
934 /// Expression
935 expr: Box<LogicalExpr>,
936 },
937
938 /// Aggregate function
939 AggregateFunction {
940 /// Function name
941 fun: AggregateFunction,
942 /// Arguments
943 args: Vec<LogicalExpr>,
944 /// DISTINCT
945 distinct: bool,
946 },
947
948 /// Scalar function
949 ScalarFunction {
950 /// Function name
951 fun: String,
952 /// Arguments
953 args: Vec<LogicalExpr>,
954 },
955
956 /// CASE expression
957 Case {
958 /// Optional base expression for CASE expr WHEN ...
959 expr: Option<Box<LogicalExpr>>,
960 /// WHEN conditions and results
961 when_then: Vec<(LogicalExpr, LogicalExpr)>,
962 /// ELSE result
963 else_result: Option<Box<LogicalExpr>>,
964 },
965
966 /// CAST expression
967 Cast {
968 /// Expression to cast
969 expr: Box<LogicalExpr>,
970 /// Target data type
971 data_type: DataType,
972 },
973
974 /// IS NULL / IS NOT NULL
975 IsNull {
976 /// Expression to check
977 expr: Box<LogicalExpr>,
978 /// True for IS NULL, false for IS NOT NULL
979 is_null: bool,
980 },
981
982 /// BETWEEN
983 Between {
984 /// Expression to test
985 expr: Box<LogicalExpr>,
986 /// Lower bound
987 low: Box<LogicalExpr>,
988 /// Upper bound
989 high: Box<LogicalExpr>,
990 /// True for BETWEEN, false for NOT BETWEEN
991 negated: bool,
992 },
993
994 /// IN list
995 InList {
996 /// Expression to test
997 expr: Box<LogicalExpr>,
998 /// List of values
999 list: Vec<LogicalExpr>,
1000 /// True for IN, false for NOT IN
1001 negated: bool,
1002 },
1003
1004 /// IN set (HashSet for O(1) membership test, used for large IN lists)
1005 InSet {
1006 /// Expression to test
1007 expr: Box<LogicalExpr>,
1008 /// Set of values for fast lookup
1009 values: std::collections::HashSet<crate::Value>,
1010 /// True for NOT IN, false for IN
1011 negated: bool,
1012 },
1013
1014 /// Scalar subquery: `(SELECT col FROM t WHERE ...)` appearing as
1015 /// a single expression. Yields the first column of the first row
1016 /// returned by the plan, or `NULL` if the plan returns no rows.
1017 /// Used in `UPDATE ... SET col = (SELECT ...)` and projection
1018 /// lists.
1019 ///
1020 /// Correlation (references to the outer row) is resolved at
1021 /// execute time by the caller (UPDATE executor substitutes outer
1022 /// columns with literals before running the plan).
1023 ScalarSubquery {
1024 /// The subquery plan
1025 subquery: Box<LogicalPlan>,
1026 },
1027
1028 /// IN subquery: expr IN (SELECT ...)
1029 InSubquery {
1030 /// Expression to test
1031 expr: Box<LogicalExpr>,
1032 /// Subquery that returns a single column
1033 subquery: Box<LogicalPlan>,
1034 /// True for NOT IN, false for IN
1035 negated: bool,
1036 },
1037
1038 /// EXISTS subquery: EXISTS (SELECT ...)
1039 Exists {
1040 /// Subquery to check for existence
1041 subquery: Box<LogicalPlan>,
1042 /// True for NOT EXISTS, false for EXISTS
1043 negated: bool,
1044 },
1045
1046 /// `DEFAULT` keyword in an INSERT VALUES list. Signals to the
1047 /// INSERT executor that this slot should fall through to the
1048 /// column's declared DEFAULT expression (or NULL if none) —
1049 /// same path as a fully-omitted column. Stock PostgreSQL: any
1050 /// `DEFAULT` inside a VALUES row means "use the column's
1051 /// default", NOT "substitute NULL".
1052 DefaultValue,
1053
1054 /// Wildcard (SELECT *)
1055 Wildcard,
1056
1057 /// Parameter placeholder ($1, $2, etc.)
1058 Parameter {
1059 /// Parameter index (1-based, matching PostgreSQL convention)
1060 index: usize,
1061 },
1062
1063 /// NEW context variable (trigger context - inserted/updated row)
1064 /// Used in trigger bodies to access the new row values
1065 /// Valid for INSERT and UPDATE triggers
1066 NewRow {
1067 /// Column name to access from NEW row
1068 column: String,
1069 },
1070
1071 /// OLD context variable (trigger context - deleted/updated row)
1072 /// Used in trigger bodies to access the old row values
1073 /// Valid for UPDATE and DELETE triggers
1074 OldRow {
1075 /// Column name to access from OLD row
1076 column: String,
1077 },
1078
1079 /// Array subscript operator: `arr[index]`
1080 ArraySubscript {
1081 /// Array expression
1082 array: Box<LogicalExpr>,
1083 /// Index expression (1-based for PostgreSQL compatibility)
1084 index: Box<LogicalExpr>,
1085 },
1086
1087 /// Row/tuple constructor: `(expr1, expr2, ...)`.
1088 ///
1089 /// Enables keyset-style comparisons like
1090 /// `WHERE (created_at, id) < ($1, $2)` which are compared
1091 /// lexicographically element-by-element by the evaluator.
1092 Tuple {
1093 /// Elements of the tuple
1094 items: Vec<LogicalExpr>,
1095 },
1096
1097 /// Window function: func(args) OVER (PARTITION BY ... ORDER BY ...)
1098 WindowFunction {
1099 /// Window function type
1100 fun: WindowFunctionType,
1101 /// Arguments to the function
1102 args: Vec<LogicalExpr>,
1103 /// PARTITION BY columns
1104 partition_by: Vec<LogicalExpr>,
1105 /// ORDER BY expressions and directions (expr, ascending)
1106 order_by: Vec<(LogicalExpr, bool)>,
1107 /// Window frame specification
1108 frame: Option<WindowFrame>,
1109 },
1110}
1111
1112/// Window function type
1113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1114pub enum WindowFunctionType {
1115 /// ROW_NUMBER() - sequential row number within partition
1116 RowNumber,
1117 /// RANK() - rank with gaps for ties
1118 Rank,
1119 /// DENSE_RANK() - rank without gaps for ties
1120 DenseRank,
1121 /// PERCENT_RANK() - relative rank (0 to 1)
1122 PercentRank,
1123 /// CUME_DIST() - cumulative distribution
1124 CumeDist,
1125 /// NTILE(n) - divide into n buckets
1126 Ntile,
1127 /// LAG(expr, offset, default) - value from previous row
1128 Lag,
1129 /// LEAD(expr, offset, default) - value from next row
1130 Lead,
1131 /// FIRST_VALUE(expr) - first value in window frame
1132 FirstValue,
1133 /// LAST_VALUE(expr) - last value in window frame
1134 LastValue,
1135 /// NTH_VALUE(expr, n) - nth value in window frame
1136 NthValue,
1137 /// Aggregate function used as window function (SUM, AVG, etc.)
1138 Aggregate(AggregateFunction),
1139}
1140
1141/// Window frame specification
1142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1143pub struct WindowFrame {
1144 /// Frame type (ROWS, RANGE, GROUPS)
1145 pub frame_type: WindowFrameType,
1146 /// Frame start bound
1147 pub start: WindowFrameBound,
1148 /// Frame end bound (None means CURRENT ROW for RANGE/ROWS BETWEEN)
1149 pub end: Option<WindowFrameBound>,
1150}
1151
1152/// Window frame type
1153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1154pub enum WindowFrameType {
1155 /// ROWS - physical rows
1156 Rows,
1157 /// RANGE - logical value ranges
1158 Range,
1159 /// GROUPS - peer groups (PostgreSQL 11+)
1160 Groups,
1161}
1162
1163/// Window frame bound
1164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1165pub enum WindowFrameBound {
1166 /// UNBOUNDED PRECEDING
1167 UnboundedPreceding,
1168 /// n PRECEDING
1169 Preceding(u64),
1170 /// CURRENT ROW
1171 CurrentRow,
1172 /// n FOLLOWING
1173 Following(u64),
1174 /// UNBOUNDED FOLLOWING
1175 UnboundedFollowing,
1176}
1177
1178/// Binary operator
1179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1180pub enum BinaryOperator {
1181 // Arithmetic
1182 Plus,
1183 Minus,
1184 Multiply,
1185 Divide,
1186 Modulo,
1187
1188 // Comparison
1189 Eq,
1190 NotEq,
1191 Lt,
1192 LtEq,
1193 Gt,
1194 GtEq,
1195
1196 // Logical
1197 And,
1198 Or,
1199
1200 // String pattern matching
1201 Like,
1202 NotLike,
1203 /// Case-insensitive LIKE
1204 ILike,
1205 /// Case-insensitive NOT LIKE
1206 NotILike,
1207 /// Regular expression match (POSIX ~)
1208 RegexMatch,
1209 /// Case-insensitive regex match (~*)
1210 RegexIMatch,
1211 /// Negated regex match (!~)
1212 NotRegexMatch,
1213 /// Negated case-insensitive regex match (!~*)
1214 NotRegexIMatch,
1215 /// SQL standard SIMILAR TO
1216 SimilarTo,
1217 /// Negated SIMILAR TO
1218 NotSimilarTo,
1219
1220 // Vector similarity operators (pgvector compatible)
1221 /// L2 distance (Euclidean): <->
1222 VectorL2Distance,
1223 /// Cosine distance: <=>
1224 VectorCosineDistance,
1225 /// Inner product (dot product): <#>
1226 VectorInnerProduct,
1227
1228 // JSONB operators (PostgreSQL compatible)
1229 /// Get JSON object field as JSON: ->
1230 JsonGet,
1231 /// Get JSON object field as text: ->>
1232 JsonGetText,
1233 /// Contains JSON value: @>
1234 JsonContains,
1235 /// JSON value is contained in: <@
1236 JsonContainedBy,
1237 /// Key/element exists: ?
1238 JsonExists,
1239 /// Any key/element exists: ?|
1240 JsonExistsAny,
1241 /// All keys/elements exist: ?&
1242 JsonExistsAll,
1243
1244 // Array operators (PostgreSQL compatible)
1245 /// Array concatenation: ||
1246 ArrayConcat,
1247
1248 // String operators
1249 /// String concatenation: ||
1250 StringConcat,
1251
1252 // Full-text search operators
1253 /// Text-search match: tsvector @@ tsquery → boolean
1254 TsMatch,
1255}
1256
1257/// Unary operator
1258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1259pub enum UnaryOperator {
1260 Not,
1261 Minus,
1262 Plus,
1263}
1264
1265/// Aggregate function
1266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1267pub enum AggregateFunction {
1268 Count,
1269 Sum,
1270 Avg,
1271 Min,
1272 Max,
1273 JsonAgg,
1274 /// ARRAY_AGG - collect values into an array
1275 ArrayAgg,
1276 /// STRING_AGG(value, delimiter) - concatenate strings with delimiter
1277 StringAgg { delimiter: String },
1278}
1279
1280/// Join type
1281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1282pub enum JoinType {
1283 Inner,
1284 Left,
1285 Right,
1286 Full,
1287 Cross,
1288}
1289
1290/// Column definition for CREATE TABLE
1291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1292pub struct ColumnDef {
1293 /// Column name
1294 pub name: String,
1295 /// Data type
1296 pub data_type: DataType,
1297 /// NOT NULL constraint
1298 pub not_null: bool,
1299 /// PRIMARY KEY constraint
1300 pub primary_key: bool,
1301 /// UNIQUE constraint
1302 pub unique: bool,
1303 /// DEFAULT value
1304 pub default: Option<LogicalExpr>,
1305 /// Storage mode for per-column optimization
1306 #[serde(default)]
1307 pub storage_mode: crate::ColumnStorageMode,
1308}
1309
1310/// Table-level constraint for CREATE TABLE
1311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1312pub enum TableConstraint {
1313 /// PRIMARY KEY constraint
1314 PrimaryKey {
1315 /// Constraint name (optional)
1316 name: Option<String>,
1317 /// Columns forming the primary key
1318 columns: Vec<String>,
1319 },
1320 /// UNIQUE constraint
1321 Unique {
1322 /// Constraint name (optional)
1323 name: Option<String>,
1324 /// Columns that must be unique together
1325 columns: Vec<String>,
1326 },
1327 /// FOREIGN KEY constraint
1328 ForeignKey {
1329 /// Constraint name (optional)
1330 name: Option<String>,
1331 /// Foreign key columns in this table
1332 columns: Vec<String>,
1333 /// Referenced table name
1334 references_table: String,
1335 /// Referenced columns
1336 references_columns: Vec<String>,
1337 /// ON DELETE action
1338 on_delete: Option<ReferentialAction>,
1339 /// ON UPDATE action
1340 on_update: Option<ReferentialAction>,
1341 /// Whether constraint is deferrable
1342 deferrable: bool,
1343 /// If deferrable, whether initially deferred
1344 initially_deferred: bool,
1345 },
1346 /// CHECK constraint
1347 Check {
1348 /// Constraint name (optional)
1349 name: Option<String>,
1350 /// Expression that must evaluate to true
1351 expression: LogicalExpr,
1352 },
1353}
1354
1355/// Referential action for foreign key ON DELETE / ON UPDATE
1356#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1357pub enum ReferentialAction {
1358 NoAction,
1359 Restrict,
1360 Cascade,
1361 SetNull,
1362 SetDefault,
1363}
1364
1365impl Default for ReferentialAction {
1366 fn default() -> Self {
1367 ReferentialAction::NoAction
1368 }
1369}
1370
1371// === Phase 3: Supporting Types ===
1372
1373/// Time-travel AS OF clause
1374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1375pub enum AsOfClause {
1376 /// AS OF NOW - current time
1377 Now,
1378 /// AS OF TIMESTAMP '2025-11-15 06:00:00'
1379 Timestamp(String),
1380 /// AS OF TRANSACTION 987654
1381 Transaction(u64),
1382 /// AS OF SCN 123456789 (System Change Number)
1383 Scn(u64),
1384 /// AS OF COMMIT 'abc123def' (Git commit SHA)
1385 Commit(String),
1386 /// VERSIONS BETWEEN start AND end - returns all versions in range
1387 VersionsBetween {
1388 /// Start timestamp (inclusive)
1389 start: Box<AsOfClause>,
1390 /// End timestamp (inclusive)
1391 end: Box<AsOfClause>,
1392 },
1393}
1394
1395/// Index creation options
1396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1397pub enum IndexOption {
1398 /// quantization = 'product'
1399 Quantization(QuantizationType),
1400 /// pq_subquantizers = 8
1401 PqSubquantizers(usize),
1402 /// pq_centroids = 256
1403 PqCentroids(usize),
1404 /// m = 16 (HNSW M parameter)
1405 HnswM(usize),
1406 /// ef_construction = 200
1407 EfConstruction(usize),
1408 /// sharding_strategy = 'hash'
1409 ShardingStrategy(String),
1410 /// shard_count = 16
1411 ShardCount(usize),
1412}
1413
1414/// Quantization type for vector indexes
1415#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1416pub enum QuantizationType {
1417 /// No quantization
1418 None,
1419 /// Scalar quantization
1420 Scalar,
1421 /// Product quantization
1422 Product,
1423 /// Auto-detect based on index size
1424 Auto,
1425}
1426
1427/// Branch creation options
1428#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1429pub enum BranchOption {
1430 /// replication_factor = 3
1431 ReplicationFactor(usize),
1432 /// region = 'us-west'
1433 Region(String),
1434 /// metadata key-value pairs
1435 Metadata(String, String),
1436}
1437
1438/// Branch merge options
1439#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1440pub enum MergeOption {
1441 /// conflict_resolution = 'branch_wins'
1442 ConflictResolution(ConflictResolution),
1443 /// delete_branch_after = true
1444 DeleteBranchAfter(bool),
1445}
1446
1447/// Conflict resolution strategy
1448#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1449pub enum ConflictResolution {
1450 /// Source branch wins
1451 BranchWins,
1452 /// Target branch wins
1453 TargetWins,
1454 /// Fail on conflict
1455 Fail,
1456}
1457
1458/// Materialized view options
1459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1460pub enum MaterializedViewOption {
1461 /// auto_refresh = true
1462 AutoRefresh(bool),
1463 /// threshold_table_size = '1GB'
1464 ThresholdTableSize(String),
1465 /// threshold_dml_rate = 100 (DML operations per minute)
1466 ThresholdDmlRate(usize),
1467 /// max_cpu_percent = 15
1468 MaxCpuPercent(f32),
1469 /// lazy_update = true
1470 LazyUpdate(bool),
1471 /// lazy_catchup_window = '1 hour'
1472 LazyCatchupWindow(String),
1473 /// distribution = 'hash(user_id)'
1474 Distribution(String),
1475 /// replication_factor = 3
1476 ReplicationFactor(usize),
1477}
1478
1479/// Diff level for branch/time-travel comparison
1480#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1481pub enum DiffLevel {
1482 /// Schema-only comparison (tables, columns, indexes)
1483 #[default]
1484 SchemaOnly,
1485 /// Schema plus sampled data comparison
1486 Sampled {
1487 /// Number of sample rows to compare
1488 sample_size: usize,
1489 },
1490 /// Full data comparison for all rows
1491 Full,
1492}
1493
1494impl std::fmt::Display for DiffLevel {
1495 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1496 match self {
1497 DiffLevel::SchemaOnly => write!(f, "SCHEMA ONLY"),
1498 DiffLevel::Sampled { sample_size } => write!(f, "SAMPLED ({})", sample_size),
1499 DiffLevel::Full => write!(f, "FULL"),
1500 }
1501 }
1502}
1503
1504impl LogicalPlan {
1505 /// Get the variant name for tracing/debugging
1506 pub fn plan_type_name(&self) -> &'static str {
1507 match self {
1508 Self::Scan { .. } => "Scan",
1509 Self::FilteredScan { .. } => "FilteredScan",
1510 Self::Filter { .. } => "Filter",
1511 Self::Project { .. } => "Project",
1512 Self::Aggregate { .. } => "Aggregate",
1513 Self::Sort { .. } => "Sort",
1514 Self::Limit { .. } => "Limit",
1515 Self::Join { .. } => "Join",
1516 Self::Insert { .. } | Self::InsertSelect { .. } => "Insert",
1517 Self::Update { .. } => "Update",
1518 Self::Delete { .. } => "Delete",
1519 Self::CreateTable { .. } => "CreateTable",
1520 Self::DropTable { .. } => "DropTable",
1521 Self::CreateIndex { .. } => "CreateIndex",
1522 Self::CreateSequence { .. } => "CreateSequence",
1523 Self::CreateExtension { .. } => "CreateExtension",
1524 Self::CreateDatabase { .. } => "CreateDatabase",
1525 Self::DropDatabase { .. } => "DropDatabase",
1526 Self::DropExtension { .. } => "DropExtension",
1527 Self::Explain { .. } => "Explain",
1528 Self::Union { .. } => "Union",
1529 Self::Intersect { .. } => "Intersect",
1530 Self::Except { .. } => "Except",
1531 Self::With { .. } => "CTE",
1532 Self::Truncate { .. } => "Truncate",
1533 Self::CreateBranch { .. } => "CreateBranch",
1534 Self::MergeBranch { .. } => "MergeBranch",
1535 Self::SystemView { .. } => "SystemView",
1536 Self::TableFunction { .. } => "TableFunction",
1537 Self::AlterTableMulti { .. } => "AlterTableMulti",
1538 _ => "Other",
1539 }
1540 }
1541
1542 /// Get the schema of this plan's output
1543 pub fn schema(&self) -> Arc<Schema> {
1544 match self {
1545 LogicalPlan::Scan { schema, projection, .. } => {
1546 if let Some(indices) = projection {
1547 let columns: Vec<_> = indices.iter()
1548 .filter_map(|&i| schema.columns.get(i).cloned())
1549 .collect();
1550 Arc::new(Schema { columns })
1551 } else {
1552 schema.clone()
1553 }
1554 }
1555 LogicalPlan::FilteredScan { schema, projection, .. } => {
1556 if let Some(indices) = projection {
1557 let columns: Vec<_> = indices.iter()
1558 .filter_map(|&i| schema.columns.get(i).cloned())
1559 .collect();
1560 Arc::new(Schema { columns })
1561 } else {
1562 schema.clone()
1563 }
1564 }
1565 LogicalPlan::Filter { input, .. } => input.schema(),
1566 LogicalPlan::Project { input, exprs, aliases, .. } => {
1567 use crate::sql::type_inference::TypeInference;
1568 let input_schema = input.schema();
1569 let columns = aliases.iter()
1570 .zip(exprs.iter())
1571 .map(|(alias, expr)| {
1572 // Use the new to_column method for complete type + nullability inference
1573 expr.to_column(alias.clone(), &input_schema)
1574 })
1575 .collect();
1576 Arc::new(Schema { columns })
1577 }
1578 LogicalPlan::Aggregate { input, group_by, aggr_exprs, .. } => {
1579 use crate::sql::type_inference::TypeInference;
1580 let input_schema = input.schema();
1581 let mut columns = Vec::new();
1582
1583 // Add GROUP BY columns with complete type and nullability inference
1584 for (i, expr) in group_by.iter().enumerate() {
1585 columns.push(expr.to_column(format!("group_{}", i), &input_schema));
1586 }
1587
1588 // Add aggregate columns with complete type and nullability inference
1589 for (i, expr) in aggr_exprs.iter().enumerate() {
1590 columns.push(expr.to_column(format!("agg_{}", i), &input_schema));
1591 }
1592
1593 Arc::new(Schema { columns })
1594 }
1595 LogicalPlan::Sort { input, .. } => input.schema(),
1596 LogicalPlan::Limit { input, .. } => input.schema(),
1597 // Set operations use left schema (both sides must have compatible schemas)
1598 LogicalPlan::Union { left, .. } => left.schema(),
1599 LogicalPlan::Intersect { left, .. } => left.schema(),
1600 LogicalPlan::Except { left, .. } => left.schema(),
1601 LogicalPlan::Join { left, right, .. } => {
1602 // Combine schemas from left and right
1603 let mut columns = left.schema().columns.clone();
1604 columns.extend(right.schema().columns.clone());
1605 Arc::new(Schema { columns })
1606 }
1607 LogicalPlan::Insert { .. } | LogicalPlan::InsertSelect { .. } => {
1608 // Insert doesn't have output schema
1609 Arc::new(Schema { columns: vec![] })
1610 }
1611 LogicalPlan::CreateTable { .. } => {
1612 Arc::new(Schema { columns: vec![] })
1613 }
1614 LogicalPlan::DropTable { .. } => {
1615 Arc::new(Schema { columns: vec![] })
1616 }
1617 LogicalPlan::Truncate { .. } => {
1618 // Truncate doesn't have output schema
1619 Arc::new(Schema { columns: vec![] })
1620 }
1621 LogicalPlan::Update { .. } => {
1622 // Update doesn't have output schema
1623 Arc::new(Schema { columns: vec![] })
1624 }
1625 LogicalPlan::Delete { .. } => {
1626 // Delete doesn't have output schema
1627 Arc::new(Schema { columns: vec![] })
1628 }
1629 LogicalPlan::CreateIndex { .. } => {
1630 // CreateIndex doesn't have output schema
1631 Arc::new(Schema { columns: vec![] })
1632 }
1633 LogicalPlan::CreateSequence { .. } => {
1634 // CREATE SEQUENCE is DDL — no output schema.
1635 Arc::new(Schema { columns: vec![] })
1636 }
1637 LogicalPlan::CreateExtension { .. }
1638 | LogicalPlan::DropExtension { .. }
1639 | LogicalPlan::CreateDatabase { .. }
1640 | LogicalPlan::DropDatabase { .. } => {
1641 Arc::new(Schema { columns: vec![] })
1642 }
1643 LogicalPlan::AlterColumnStorage { .. } => {
1644 // AlterColumnStorage doesn't have output schema
1645 Arc::new(Schema { columns: vec![] })
1646 }
1647 LogicalPlan::AlterTableAddColumn { .. } => {
1648 // ALTER TABLE ADD COLUMN doesn't have output schema
1649 Arc::new(Schema { columns: vec![] })
1650 }
1651 LogicalPlan::AlterTableDropColumn { .. } => {
1652 // ALTER TABLE DROP COLUMN doesn't have output schema
1653 Arc::new(Schema { columns: vec![] })
1654 }
1655 LogicalPlan::AlterTableRenameColumn { .. } => {
1656 // ALTER TABLE RENAME COLUMN doesn't have output schema
1657 Arc::new(Schema { columns: vec![] })
1658 }
1659 LogicalPlan::AlterTableRename { .. } => {
1660 // ALTER TABLE RENAME doesn't have output schema
1661 Arc::new(Schema { columns: vec![] })
1662 }
1663 LogicalPlan::AlterTableAddForeignKey { .. } => {
1664 Arc::new(Schema { columns: vec![] })
1665 }
1666 LogicalPlan::AlterTableMulti { .. } => {
1667 // ALTER TABLE with multiple operations doesn't have output schema
1668 Arc::new(Schema { columns: vec![] })
1669 }
1670 LogicalPlan::CreateBranch { .. } => {
1671 // CreateBranch doesn't have output schema
1672 Arc::new(Schema { columns: vec![] })
1673 }
1674 LogicalPlan::DropBranch { .. } => {
1675 // DropBranch doesn't have output schema
1676 Arc::new(Schema { columns: vec![] })
1677 }
1678 LogicalPlan::MergeBranch { .. } => {
1679 // MergeBranch doesn't have output schema
1680 Arc::new(Schema { columns: vec![] })
1681 }
1682 LogicalPlan::UseBranch { .. } => {
1683 // UseBranch doesn't have output schema
1684 Arc::new(Schema { columns: vec![] })
1685 }
1686 LogicalPlan::ShowBranches => {
1687 // ShowBranches returns a table with branch information
1688 use crate::DataType;
1689 Arc::new(Schema {
1690 columns: vec![
1691 crate::Column {
1692 name: "branch_name".to_string(),
1693 data_type: DataType::Text,
1694 nullable: false,
1695 primary_key: false,
1696 source_table: None,
1697 source_table_name: None,
1698 default_expr: None,
1699 unique: false,
1700 storage_mode: Default::default(),
1701 },
1702 crate::Column {
1703 name: "branch_id".to_string(),
1704 data_type: DataType::Int8,
1705 nullable: false,
1706 primary_key: false,
1707 source_table: None,
1708 source_table_name: None,
1709 default_expr: None,
1710 unique: false,
1711 storage_mode: Default::default(),
1712 },
1713 crate::Column {
1714 name: "parent_branch".to_string(),
1715 data_type: DataType::Text,
1716 nullable: true,
1717 primary_key: false,
1718 source_table: None,
1719 source_table_name: None,
1720 default_expr: None,
1721 unique: false,
1722 storage_mode: Default::default(),
1723 },
1724 crate::Column {
1725 name: "created_at".to_string(),
1726 data_type: DataType::Timestamp,
1727 nullable: false,
1728 primary_key: false,
1729 source_table: None,
1730 source_table_name: None,
1731 default_expr: None,
1732 unique: false,
1733 storage_mode: Default::default(),
1734 },
1735 crate::Column {
1736 name: "state".to_string(),
1737 data_type: DataType::Text,
1738 nullable: false,
1739 primary_key: false,
1740 source_table: None,
1741 source_table_name: None,
1742 default_expr: None,
1743 unique: false,
1744 storage_mode: Default::default(),
1745 },
1746 ],
1747 })
1748 }
1749 LogicalPlan::CreateMaterializedView { .. } => {
1750 // CreateMaterializedView doesn't have output schema
1751 Arc::new(Schema { columns: vec![] })
1752 }
1753 LogicalPlan::RefreshMaterializedView { .. } => {
1754 // RefreshMaterializedView doesn't have output schema
1755 Arc::new(Schema { columns: vec![] })
1756 }
1757 LogicalPlan::DropMaterializedView { .. } => {
1758 // DropMaterializedView doesn't have output schema
1759 Arc::new(Schema { columns: vec![] })
1760 }
1761 LogicalPlan::AlterMaterializedView { .. } => {
1762 // AlterMaterializedView doesn't have output schema
1763 Arc::new(Schema { columns: vec![] })
1764 }
1765 LogicalPlan::SystemView { name, .. } => {
1766 // System views have predefined schemas from the registry
1767 use crate::sql::phase3::SystemViewRegistry;
1768 let registry = SystemViewRegistry::new();
1769 if let Some(schema) = registry.get_schema(name) {
1770 Arc::new(schema.clone())
1771 } else {
1772 // Fallback to empty schema if view not found
1773 Arc::new(Schema { columns: vec![] })
1774 }
1775 }
1776 LogicalPlan::TableFunction { function_name, .. } => {
1777 // Table functions produce a virtual table with a single column
1778 use crate::DataType;
1779 let col_name = match function_name.as_str() {
1780 "generate_series" => "generate_series",
1781 "unnest" => "unnest",
1782 _ => function_name.as_str(),
1783 };
1784 Arc::new(Schema {
1785 columns: vec![crate::Column {
1786 name: col_name.to_string(),
1787 data_type: DataType::Int8,
1788 nullable: false,
1789 primary_key: false,
1790 source_table: None,
1791 source_table_name: None,
1792 default_expr: None,
1793 unique: false,
1794 storage_mode: Default::default(),
1795 }],
1796 })
1797 }
1798 LogicalPlan::With { query, .. } => {
1799 // With clause returns the schema of the inner query
1800 query.schema()
1801 }
1802 LogicalPlan::CreateTrigger { .. } => {
1803 // CreateTrigger doesn't have output schema
1804 Arc::new(Schema { columns: vec![] })
1805 }
1806 LogicalPlan::DropTrigger { .. } => {
1807 // DropTrigger doesn't have output schema
1808 Arc::new(Schema { columns: vec![] })
1809 }
1810 LogicalPlan::Explain { .. } => {
1811 // EXPLAIN returns a single text column with the query plan
1812 use crate::DataType;
1813 Arc::new(Schema {
1814 columns: vec![
1815 crate::Column {
1816 name: "QUERY PLAN".to_string(),
1817 data_type: DataType::Text,
1818 nullable: false,
1819 primary_key: false,
1820 source_table: None,
1821 source_table_name: None,
1822 default_expr: None,
1823 unique: false,
1824 storage_mode: Default::default(),
1825 },
1826 ],
1827 })
1828 }
1829 // Transaction control - no output schema
1830 LogicalPlan::StartTransaction => Arc::new(Schema { columns: vec![] }),
1831 LogicalPlan::Commit => Arc::new(Schema { columns: vec![] }),
1832 LogicalPlan::Rollback => Arc::new(Schema { columns: vec![] }),
1833 LogicalPlan::Savepoint { .. } => Arc::new(Schema { columns: vec![] }),
1834 LogicalPlan::ReleaseSavepoint { .. } => Arc::new(Schema { columns: vec![] }),
1835 LogicalPlan::RollbackToSavepoint { .. } => Arc::new(Schema { columns: vec![] }),
1836 LogicalPlan::SetConstraints { .. } => Arc::new(Schema { columns: vec![] }),
1837 // Prepared statements - no output schema for DDL
1838 LogicalPlan::Prepare { .. } => Arc::new(Schema { columns: vec![] }),
1839 LogicalPlan::Execute { .. } => Arc::new(Schema { columns: vec![] }),
1840 LogicalPlan::Deallocate { .. } => Arc::new(Schema { columns: vec![] }),
1841 // Procedural statements - no output schema for DDL
1842 LogicalPlan::CreateFunction { .. } => Arc::new(Schema { columns: vec![] }),
1843 LogicalPlan::CreateProcedure { .. } => Arc::new(Schema { columns: vec![] }),
1844 LogicalPlan::DropFunction { .. } => Arc::new(Schema { columns: vec![] }),
1845 LogicalPlan::DropProcedure { .. } => Arc::new(Schema { columns: vec![] }),
1846 LogicalPlan::Call { .. } => Arc::new(Schema { columns: vec![] }),
1847 // DualScan - empty schema (single row, no columns)
1848 // Used as input for SELECT without FROM, expressions are evaluated in Project
1849 LogicalPlan::DualScan => Arc::new(Schema { columns: vec![] }),
1850
1851 // Regular Views
1852 LogicalPlan::CreateView { .. } => {
1853 // CreateView doesn't have output schema
1854 Arc::new(Schema { columns: vec![] })
1855 }
1856 LogicalPlan::DropView { .. } => {
1857 // DropView doesn't have output schema
1858 Arc::new(Schema { columns: vec![] })
1859 }
1860
1861 // HA Operations
1862 #[cfg(feature = "ha-tier1")]
1863 LogicalPlan::Switchover { .. } => {
1864 // Returns status message
1865 Arc::new(Schema {
1866 columns: vec![
1867 Column::new("result", DataType::Text),
1868 ],
1869 })
1870 }
1871 #[cfg(feature = "ha-tier1")]
1872 LogicalPlan::SwitchoverCheck { .. } => {
1873 // Returns check results
1874 Arc::new(Schema {
1875 columns: vec![
1876 Column::new("can_proceed", DataType::Boolean),
1877 Column::new("target_healthy", DataType::Boolean),
1878 Column::new("target_lsn", DataType::Int8),
1879 Column::new("primary_lsn", DataType::Int8),
1880 Column::new("lag_bytes", DataType::Int8),
1881 Column::new("warnings", DataType::Text),
1882 Column::new("blockers", DataType::Text),
1883 ],
1884 })
1885 }
1886 #[cfg(feature = "ha-tier1")]
1887 LogicalPlan::ClusterStatus => {
1888 // Returns cluster status table
1889 Arc::new(Schema {
1890 columns: vec![
1891 Column::new("node_id", DataType::Text),
1892 Column::new("role", DataType::Text),
1893 Column::new("address", DataType::Text),
1894 Column::new("is_healthy", DataType::Boolean),
1895 Column::new("lsn", DataType::Int8),
1896 Column::new("lag_ms", DataType::Int8),
1897 Column::new("priority", DataType::Int4),
1898 ],
1899 })
1900 }
1901 #[cfg(feature = "ha-tier1")]
1902 LogicalPlan::SetNodeAlias { .. } => {
1903 // Returns confirmation message
1904 Arc::new(Schema {
1905 columns: vec![
1906 Column::new("result", DataType::Text),
1907 ],
1908 })
1909 }
1910 #[cfg(feature = "ha-tier1")]
1911 LogicalPlan::ShowTopology => {
1912 // Returns detailed topology table
1913 Arc::new(Schema {
1914 columns: vec![
1915 Column::new("node_id", DataType::Text),
1916 Column::new("alias", DataType::Text),
1917 Column::new("role", DataType::Text),
1918 Column::new("client_addr", DataType::Text),
1919 Column::new("replication_addr", DataType::Text),
1920 Column::new("healthy", DataType::Boolean),
1921 Column::new("health_msg", DataType::Text),
1922 Column::new("last_seen_secs", DataType::Int8),
1923 Column::new("lsn", DataType::Int8),
1924 Column::new("lag_ms", DataType::Int8),
1925 Column::new("priority", DataType::Int4),
1926 Column::new("weight", DataType::Int4),
1927 ],
1928 })
1929 }
1930 }
1931 }
1932}
1933
1934#[cfg(test)]
1935#[allow(clippy::unwrap_used, clippy::expect_used)]
1936mod tests {
1937 use super::*;
1938
1939 #[test]
1940 fn test_scan_schema() {
1941 let schema = Arc::new(Schema {
1942 columns: vec![
1943 crate::Column {
1944 name: "id".to_string(),
1945 data_type: DataType::Int4,
1946 nullable: false,
1947 primary_key: false,
1948 source_table: None,
1949 source_table_name: None,
1950 default_expr: None,
1951 unique: false,
1952 storage_mode: Default::default(),
1953 },
1954 crate::Column {
1955 name: "name".to_string(),
1956 data_type: DataType::Text,
1957 nullable: true,
1958 primary_key: false,
1959 source_table: None,
1960 source_table_name: None,
1961 default_expr: None,
1962 unique: false,
1963 storage_mode: Default::default(),
1964 },
1965 ],
1966 });
1967
1968 let plan = LogicalPlan::Scan {
1969 table_name: "users".to_string(),
1970 alias: None,
1971 schema: schema.clone(),
1972 projection: None,
1973 as_of: None,
1974 };
1975
1976 assert_eq!(plan.schema().columns.len(), 2);
1977 }
1978
1979 #[test]
1980 fn test_scan_with_projection() {
1981 let schema = Arc::new(Schema {
1982 columns: vec![
1983 crate::Column {
1984 name: "id".to_string(),
1985 data_type: DataType::Int4,
1986 nullable: false,
1987 primary_key: false,
1988 source_table: None,
1989 source_table_name: None,
1990 default_expr: None,
1991 unique: false,
1992 storage_mode: Default::default(),
1993 },
1994 crate::Column {
1995 name: "name".to_string(),
1996 data_type: DataType::Text,
1997 nullable: true,
1998 primary_key: false,
1999 source_table: None,
2000 source_table_name: None,
2001 default_expr: None,
2002 unique: false,
2003 storage_mode: Default::default(),
2004 },
2005 ],
2006 });
2007
2008 let plan = LogicalPlan::Scan {
2009 table_name: "users".to_string(),
2010 alias: None,
2011 schema: schema.clone(),
2012 projection: Some(vec![0]), // Only id column
2013 as_of: None,
2014 };
2015
2016 assert_eq!(plan.schema().columns.len(), 1);
2017 assert_eq!(plan.schema().columns[0].name, "id");
2018 }
2019
2020 #[test]
2021 fn test_scan_with_time_travel() {
2022 let schema = Arc::new(Schema {
2023 columns: vec![
2024 crate::Column {
2025 name: "id".to_string(),
2026 data_type: DataType::Int4,
2027 nullable: false,
2028 primary_key: false,
2029 source_table: None,
2030 source_table_name: None,
2031 default_expr: None,
2032 unique: false,
2033 storage_mode: Default::default(),
2034 },
2035 ],
2036 });
2037
2038 let plan = LogicalPlan::Scan {
2039 table_name: "orders".to_string(),
2040 alias: None,
2041 schema: schema.clone(),
2042 projection: None,
2043 as_of: Some(AsOfClause::Timestamp("2025-11-15 06:00:00".to_string())),
2044 };
2045
2046 assert_eq!(plan.schema().columns.len(), 1);
2047 }
2048}