Skip to main content

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}