vibesql_executor/create_table.rs
1//! CREATE TABLE statement execution
2
3use vibesql_ast::{CreateTableStmt, IndexColumn, OrderDirection};
4use vibesql_catalog::{ColumnSchema, TableIdentifier, TableSchema};
5use vibesql_storage::Database;
6use vibesql_types::DataType;
7
8use crate::{
9 constraint_validator::ConstraintValidator, errors::ExecutorError,
10 privilege_checker::PrivilegeChecker, SelectExecutor,
11};
12
13/// Executor for CREATE TABLE statements
14pub struct CreateTableExecutor;
15
16impl CreateTableExecutor {
17 /// Execute a CREATE TABLE statement
18 ///
19 /// # Arguments
20 ///
21 /// * `stmt` - The CREATE TABLE statement AST node
22 /// * `database` - The database to create the table in
23 ///
24 /// # Returns
25 ///
26 /// Success message or error
27 ///
28 /// # Examples
29 ///
30 /// ```
31 /// use vibesql_ast::{ColumnDef, CreateTableStmt};
32 /// use vibesql_executor::CreateTableExecutor;
33 /// use vibesql_storage::Database;
34 /// use vibesql_types::DataType;
35 ///
36 /// let mut db = Database::new();
37 /// let stmt = CreateTableStmt { temporary: false,
38 /// if_not_exists: false,
39 /// table_name: "users".to_string(),
40 /// columns: vec![
41 /// ColumnDef {
42 /// name: "id".to_string(),
43 /// data_type: DataType::Integer,
44 /// nullable: false,
45 /// constraints: vec![],
46 /// default_value: None,
47 /// comment: None,
48 /// generated_expr: None, is_exact_integer_type: false,
49 /// },
50 /// ColumnDef {
51 /// name: "name".to_string(),
52 /// data_type: DataType::Varchar { max_length: Some(255) },
53 /// nullable: true,
54 /// constraints: vec![],
55 /// default_value: None,
56 /// comment: None,
57 /// generated_expr: None, is_exact_integer_type: false,
58 /// },
59 /// ],
60 /// table_constraints: vec![],
61 /// table_options: vec![],
62 /// quoted: false,
63 /// name_source: None,
64 /// as_query: None, without_rowid: false,
65 /// };
66 ///
67 /// let result = CreateTableExecutor::execute(&stmt, &mut db);
68 /// assert!(result.is_ok());
69 /// ```
70 pub fn execute(
71 stmt: &CreateTableStmt,
72 database: &mut Database,
73 ) -> Result<String, ExecutorError> {
74 Self::execute_impl(stmt, database, false, None)
75 }
76
77 /// Execute a `CREATE TABLE` statement (user-facing path), recording the
78 /// verbatim original source text.
79 ///
80 /// SQLite stores the byte-for-byte original `CREATE TABLE` statement in
81 /// `sqlite_master.sql` (whitespace and formatting preserved). When
82 /// `sql_source` is `Some`, it is stamped onto the catalog `TableSchema` so
83 /// that `sqlite_master`/`sqlite_schema` returns it verbatim instead of a
84 /// reconstruction. The trailing semicolon (if any) is stripped by
85 /// `TableSchema::set_sql_source` to match SQLite. Pass `None` when the source
86 /// text is unavailable (e.g. AST built programmatically); callers then fall
87 /// back to reconstructing the SQL. See issue #5619.
88 ///
89 /// This is the UNTRUSTED path — it keeps the #5614 reserved-name and #5553
90 /// duplicate-column guards. For replaying a persisted dump use
91 /// [`execute_for_load_with_source`].
92 pub fn execute_with_source(
93 stmt: &CreateTableStmt,
94 database: &mut Database,
95 sql_source: Option<&str>,
96 ) -> Result<String, ExecutorError> {
97 Self::execute_impl(stmt, database, false, sql_source)
98 }
99
100 /// Execute a CREATE TABLE statement on a TRUSTED (engine-internal) path.
101 ///
102 /// This is used when replaying the engine's own persisted schema (e.g.
103 /// `load_sql_dump` / consensus state-machine replay), not for user-issued
104 /// DDL. A persisted dump is engine-internal data that was already validated
105 /// when first created, so it must always round-trip — it must NOT be
106 /// re-rejected by the user-facing conformance guards.
107 ///
108 /// Specifically this skips:
109 /// - the reserved `sqlite_`-prefix guard, so a table that legitimately
110 /// reached the catalog (e.g. a pre-#5614 DB where ALTER TABLE RENAME TO
111 /// `sqlite_x` was still accepted) reloads cleanly instead of bricking the
112 /// database (issue #5614);
113 /// - the duplicate-column guard, for the same backward-compat reason — a
114 /// schema that was once persisted must remain loadable.
115 ///
116 /// All other validation (namespace collisions, etc.) still applies, matching
117 /// the prior behavior of the load path for #5613.
118 pub fn execute_for_load(
119 stmt: &CreateTableStmt,
120 database: &mut Database,
121 ) -> Result<String, ExecutorError> {
122 Self::execute_impl(stmt, database, true, None)
123 }
124
125 /// Trusted load path (see [`execute_for_load`]) that ALSO records the
126 /// verbatim original `CREATE TABLE` source text for `sqlite_master.sql`.
127 ///
128 /// This is the entry point used when reloading a persisted `.sql` dump: it
129 /// must bypass the user-facing guards (#5614/#5553) so a previously
130 /// persisted schema always round-trips, while still preserving the byte-for-
131 /// byte original source so `sqlite_master.sql` keeps the user's formatting
132 /// across a save/reload cycle (issue #5619).
133 pub fn execute_for_load_with_source(
134 stmt: &CreateTableStmt,
135 database: &mut Database,
136 sql_source: Option<&str>,
137 ) -> Result<String, ExecutorError> {
138 Self::execute_impl(stmt, database, true, sql_source)
139 }
140
141 fn execute_impl(
142 stmt: &CreateTableStmt,
143 database: &mut Database,
144 trusted: bool,
145 sql_source: Option<&str>,
146 ) -> Result<String, ExecutorError> {
147 // Parse qualified table name (schema.table or just table)
148 // For TEMP tables, use the session-specific temp schema (SQLite compatibility)
149 let (schema_name, table_name, identifier) = if stmt.temporary {
150 // Temporary table - use session-specific temp schema
151 // Each session gets its own temp schema (e.g., "temp_1", "temp_2")
152 // for isolation between database connections
153 let temp_schema = database.catalog.temp_schema_name();
154 let id = TableIdentifier::qualified(temp_schema, false, &stmt.table_name, stmt.quoted);
155 (temp_schema.to_string(), stmt.table_name.clone(), id)
156 } else if let Some((schema_part, table_part)) = stmt.table_name.split_once('.') {
157 // Schema-qualified table name - use qualified identifier
158 // Note: We use stmt.quoted for both parts since the parser combined them
159 // In a future iteration, CREATE TABLE could also store schema/table quoted status separately
160 let id = TableIdentifier::qualified(schema_part, stmt.quoted, table_part, stmt.quoted);
161 (schema_part.to_string(), table_part.to_string(), id)
162 } else {
163 // Simple table name - use current schema
164 let id = TableIdentifier::new(&stmt.table_name, stmt.quoted);
165 (database.catalog.get_current_schema().to_string(), stmt.table_name.clone(), id)
166 };
167
168 // Check CREATE privilege on the schema
169 PrivilegeChecker::check_create(database, &schema_name)?;
170
171 // The reserved-name and duplicate-column guards are USER-facing
172 // conformance checks (issue #5614). They are skipped on the trusted
173 // load/replay path so the engine's own persisted dump always reloads —
174 // see `execute_for_load`.
175 if !trusted {
176 // Reject user attempts to create a table with a reserved name. SQLite
177 // reserves the `sqlite_` prefix for its own schema objects and errors
178 // `object name reserved for internal use: <name>` (sqlite3 3.51.0),
179 // echoing the name exactly as the user spelled it (dequoted, original
180 // case — e.g. `SQLITE_foo`). `table_name` already holds that verbatim
181 // bare-name spelling. This guard is on the user-facing executor only;
182 // the engine's internal sqlite_-prefixed objects are created via
183 // dedicated catalog APIs and never pass through here (issue #5614).
184 if crate::sqlite_schema::is_reserved_object_name(&table_name) {
185 return Err(ExecutorError::SqliteCompatError(format!(
186 "object name reserved for internal use: {}",
187 table_name
188 )));
189 }
190
191 // Reject duplicate column names. SQLite compares column names
192 // case-insensitively (#5553) and errors `duplicate column name: <name>`,
193 // echoing the *second* (colliding) occurrence's original spelling —
194 // `CREATE TABLE t(a, A)` reports `duplicate column name: A`
195 // (sqlite3 3.51.0). The AST preserves each column's as-written casing.
196 for (i, col) in stmt.columns.iter().enumerate() {
197 if stmt.columns[..i].iter().any(|prev| prev.name.eq_ignore_ascii_case(&col.name)) {
198 return Err(ExecutorError::SqliteCompatError(format!(
199 "duplicate column name: {}",
200 col.name
201 )));
202 }
203 }
204 }
205
206 // Handle CREATE TABLE AS SELECT syntax
207 if let Some(query) = &stmt.as_query {
208 return Self::execute_create_as_select(
209 database,
210 &table_name,
211 &schema_name,
212 identifier,
213 stmt.if_not_exists,
214 stmt.name_source.as_deref(),
215 query,
216 );
217 }
218
219 // Check if table already exists in the target schema using SQL:1999 identifier semantics
220 // For CREATE TABLE, we only check the target schema (not temp schema)
221 // Temp tables can shadow main tables, but we allow creating in main even if temp exists
222 // Use table_exists_by_identifier which keys on the canonical (case-folded)
223 // form. SQLite case-folds identifiers regardless of quoting (issue #5553),
224 // so `CREATE TABLE "TBL1"` collides with an existing `tbl1`.
225 if database.catalog.table_exists_by_identifier(&identifier) {
226 if stmt.if_not_exists {
227 // IF NOT EXISTS - silently return success without creating the table
228 return Ok(format!(
229 "Table '{}' already exists in schema '{}' (skipped)",
230 table_name, schema_name
231 ));
232 }
233 // A bare `CREATE TABLE` over an existing name is an error. SQLite
234 // echoes the table name *exactly as written in the source*,
235 // preserving its quoting form and casing — `table "tbl1" already
236 // exists` / `table [tbl1] already exists` (sqlite3 3.51.0). The
237 // parser captures that verbatim spelling in `name_source`; fall back
238 // to the schema-qualified normalized name when it is unavailable
239 // (e.g. programmatically-built AST). Mirrors the CREATE TRIGGER
240 // mechanism from #5538.
241 let echoed = stmt
242 .name_source
243 .clone()
244 .unwrap_or_else(|| format!("{}.{}", schema_name, identifier.display()));
245 return Err(ExecutorError::TableAlreadyExists(echoed));
246 }
247
248 // SQLite places tables, indexes, and views in ONE object namespace per
249 // schema. A `CREATE TABLE <name>` must therefore fail when an index of
250 // that name already exists, reporting `there is already an index named
251 // <name>` (sqlite3 3.51.0). This mirrors the symmetric CREATE INDEX
252 // check (`there is already a table named <name>`) and—critically—keeps
253 // the on-disk schema reloadable: previously VibeSQL accepted the
254 // colliding table, then the DDL-replay on the next open hit its own
255 // collision and bricked the database (issue #5613).
256 //
257 // The check is schema-aware (a temp index does not collide with a main
258 // table) and case-insensitive (#5553 identifier folding), reusing the
259 // canonical identifier's table-name component for the comparison.
260 //
261 // Note: IF NOT EXISTS does NOT suppress this cross-type collision —
262 // sqlite3 still raises `there is already an index named X` (verified
263 // against 3.51.0). IF NOT EXISTS only silences a same-type (table)
264 // collision, handled above.
265 if database
266 .catalog
267 .index_name_exists_in_schema(&schema_name, identifier.table_canonical())
268 {
269 // Echo the name as the user spelled it (sqlite3 prints the bare
270 // index name without schema qualification).
271 return Err(ExecutorError::SqliteCompatError(format!(
272 "there is already an index named {}",
273 table_name
274 )));
275 }
276
277 // Check for AUTO_INCREMENT constraints
278 // MySQL allows only one AUTO_INCREMENT column per table
279 let auto_increment_columns: Vec<&str> = stmt
280 .columns
281 .iter()
282 .filter(|col_def| {
283 col_def
284 .constraints
285 .iter()
286 .any(|c| matches!(c.kind, vibesql_ast::ColumnConstraintKind::AutoIncrement))
287 })
288 .map(|col_def| col_def.name.as_str())
289 .collect();
290
291 if auto_increment_columns.len() > 1 {
292 return Err(ExecutorError::ConstraintViolation(
293 "Only one AUTO_INCREMENT column allowed per table".to_string(),
294 ));
295 }
296
297 // Convert AST ColumnDef → Catalog ColumnSchema
298 let mut columns: Vec<ColumnSchema> = stmt
299 .columns
300 .iter()
301 .map(|col_def| {
302 // For AUTO_INCREMENT columns, set default to NEXT VALUE FOR sequence
303 let default_value = if col_def
304 .constraints
305 .iter()
306 .any(|c| matches!(c.kind, vibesql_ast::ColumnConstraintKind::AutoIncrement))
307 {
308 // Create sequence name: {table_name}_{column_name}_seq
309 let sequence_name = format!("{}_{}_seq", table_name, col_def.name);
310 Some(vibesql_ast::Expression::NextValue { sequence_name })
311 } else {
312 col_def.default_value.as_ref().map(|expr| (**expr).clone())
313 };
314
315 // Extract column-level collation from constraints
316 let collation = col_def.constraints.iter().find_map(|c| {
317 if let vibesql_ast::ColumnConstraintKind::Collate(coll) = &c.kind {
318 Some(coll.clone())
319 } else {
320 None
321 }
322 });
323
324 ColumnSchema {
325 name: col_def.name.clone(),
326 data_type: col_def.data_type.clone(),
327 nullable: col_def.nullable,
328 default_value,
329 generated_expr: col_def.generated_expr.as_ref().map(|expr| (**expr).clone()),
330 collation,
331 is_exact_integer_type: col_def.is_exact_integer_type,
332 }
333 })
334 .collect();
335
336 // Process constraints using the constraint validator
337 let constraint_result =
338 ConstraintValidator::process_constraints(&stmt.columns, &stmt.table_constraints)?;
339
340 // Apply constraint results to columns (updates nullability)
341 ConstraintValidator::apply_to_columns(&mut columns, &constraint_result);
342
343 // Create TableSchema with unqualified name
344 let mut table_schema = TableSchema::new(table_name.clone(), columns);
345
346 // Preserve the verbatim original CREATE TABLE text for sqlite_master.sql
347 // (SQLite stores it byte-for-byte; see issue #5619). The trailing
348 // semicolon is stripped by set_sql_source to match SQLite.
349 if let Some(src) = sql_source {
350 table_schema.set_sql_source(src);
351 }
352
353 // Apply WITHOUT ROWID flag from AST (SQLite compatibility)
354 table_schema.without_rowid = stmt.without_rowid;
355
356 // Apply constraint results to schema (sets PK, unique, and check constraints)
357 ConstraintValidator::apply_to_schema(&mut table_schema, &constraint_result);
358
359 // WITHOUT ROWID tables must have a PRIMARY KEY (SQLite requirement, Issue #4953)
360 if stmt.without_rowid && table_schema.primary_key.is_none() {
361 return Err(ExecutorError::ConstraintViolation(
362 "WITHOUT ROWID tables must have a PRIMARY KEY".to_string(),
363 ));
364 }
365
366 // Detect INTEGER PRIMARY KEY for SQLite rowid aliasing (Issue #4536)
367 // In SQLite, a single-column PRIMARY KEY with exactly "INTEGER" type is an alias for rowid.
368 // The column's value IS the rowid, and SELECT rowid returns this column's value.
369 // IMPORTANT: Only exact "INTEGER" qualifies - "INT" does NOT (even though both parse to DataType::Integer)
370 if let Some(pk_cols) = &table_schema.primary_key {
371 if pk_cols.len() == 1 {
372 if let Some(col_idx) = table_schema.get_column_index(&pk_cols[0]) {
373 let col = &table_schema.columns[col_idx];
374 // Only exact "INTEGER" type qualifies for rowid aliasing, not "INT"
375 if matches!(col.data_type, DataType::Integer) && col.is_exact_integer_type {
376 table_schema.set_rowid_alias_column(Some(col_idx));
377 }
378 }
379 }
380 }
381
382 // Check for STORAGE table option and apply storage format
383 for option in &stmt.table_options {
384 if let vibesql_ast::TableOption::Storage(format) = option {
385 table_schema.set_storage_format(*format);
386 }
387 }
388
389 // Process foreign key constraints from table_constraints
390 for constraint in &stmt.table_constraints {
391 if let vibesql_ast::TableConstraintKind::ForeignKey {
392 columns: fk_columns,
393 references_table,
394 references_columns,
395 on_delete,
396 on_update,
397 deferral,
398 } = &constraint.kind
399 {
400 // Resolve column indices for FK columns
401 let column_indices: Vec<usize> = fk_columns
402 .iter()
403 .map(|col_name| {
404 table_schema.get_column_index(col_name).ok_or_else(|| {
405 ExecutorError::ColumnNotFound {
406 column_name: col_name.to_string(),
407 table_name: table_name.clone(),
408 searched_tables: vec![table_name.clone()],
409 available_columns: table_schema
410 .columns
411 .iter()
412 .map(|c| c.name.clone())
413 .collect(),
414 }
415 })
416 })
417 .collect::<Result<Vec<_>, _>>()?;
418
419 // Lookup parent table to get parent column indices
420 // If the parent table doesn't exist yet, use placeholder indices.
421 // SQLite stores FK metadata at CREATE TABLE time even if the
422 // referenced table doesn't exist yet.
423 let parent_schema = database.catalog.get_table(references_table);
424
425 let parent_column_indices: Vec<usize> = references_columns
426 .iter()
427 .map(|col_name| {
428 parent_schema
429 .as_ref()
430 .and_then(|s| s.get_column_index(col_name))
431 .unwrap_or(0)
432 })
433 .collect();
434
435 // Convert ReferentialAction from AST to catalog type
436 let convert_action = |action: &Option<vibesql_ast::ReferentialAction>| match action
437 .as_ref()
438 .unwrap_or(&vibesql_ast::ReferentialAction::NoAction)
439 {
440 vibesql_ast::ReferentialAction::Cascade => {
441 vibesql_catalog::ReferentialAction::Cascade
442 }
443 vibesql_ast::ReferentialAction::SetNull => {
444 vibesql_catalog::ReferentialAction::SetNull
445 }
446 vibesql_ast::ReferentialAction::SetDefault => {
447 vibesql_catalog::ReferentialAction::SetDefault
448 }
449 vibesql_ast::ReferentialAction::Restrict => {
450 vibesql_catalog::ReferentialAction::Restrict
451 }
452 vibesql_ast::ReferentialAction::NoAction => {
453 vibesql_catalog::ReferentialAction::NoAction
454 }
455 };
456
457 // If no parent columns specified, pad with empty strings to match
458 // the FK column count. SQLite stores empty "to" in PRAGMA foreign_key_list
459 // when no column list is given after REFERENCES.
460 let effective_parent_names = if references_columns.is_empty() {
461 vec![String::new(); fk_columns.len()]
462 } else {
463 references_columns.clone()
464 };
465 let effective_parent_indices = if parent_column_indices.is_empty() {
466 vec![0; fk_columns.len()]
467 } else {
468 parent_column_indices
469 };
470
471 let (is_deferrable, initially_deferred) = deferral
472 .map(|d| (d.is_deferrable, d.initially_deferred))
473 .unwrap_or((false, false));
474
475 let fk = vibesql_catalog::ForeignKeyConstraint {
476 name: constraint.name.clone(),
477 column_names: fk_columns.clone(),
478 column_indices,
479 parent_table: references_table.clone(),
480 parent_column_names: effective_parent_names,
481 parent_column_indices: effective_parent_indices,
482 on_delete: convert_action(on_delete),
483 on_update: convert_action(on_update),
484 is_deferrable,
485 initially_deferred,
486 };
487
488 table_schema.add_foreign_key(fk)?;
489 }
490 }
491
492 // Process column-level REFERENCES constraints
493 // These are parsed as ColumnConstraintKind::References but need to be
494 // converted into ForeignKeyConstraint entries in the schema.
495 // Note: In SQLite, FK constraints are stored as metadata at CREATE TABLE time
496 // even if the referenced table doesn't exist yet. Validation only happens
497 // at INSERT/UPDATE/DELETE time when PRAGMA foreign_keys=ON.
498 // SQLite assigns FK IDs in reverse column order for column-level constraints,
499 // so we collect them first and then add in reverse order.
500 let mut column_level_fks = Vec::new();
501 for col_def in &stmt.columns {
502 for constraint in &col_def.constraints {
503 if let vibesql_ast::ColumnConstraintKind::References {
504 table: ref_table,
505 column: ref_column,
506 on_delete,
507 on_update,
508 deferral,
509 } = &constraint.kind
510 {
511 let col_idx =
512 table_schema.get_column_index(&col_def.name).ok_or_else(|| {
513 ExecutorError::ColumnNotFound {
514 column_name: col_def.name.clone(),
515 table_name: table_name.clone(),
516 searched_tables: vec![table_name.clone()],
517 available_columns: table_schema
518 .columns
519 .iter()
520 .map(|c| c.name.clone())
521 .collect(),
522 }
523 })?;
524
525 // Try to lookup parent table to resolve column references.
526 // If the parent table doesn't exist yet, store the FK with
527 // the column name but use placeholder index 0.
528 let parent_schema = database.catalog.get_table(ref_table);
529
530 let (parent_col_name, parent_col_idx) = if let Some(col) = ref_column {
531 // Explicit column reference
532 let idx = parent_schema
533 .as_ref()
534 .and_then(|s| s.get_column_index(col))
535 .unwrap_or(0);
536 (col.clone(), idx)
537 } else {
538 // No column specified - store empty string to match SQLite behavior.
539 // PRAGMA foreign_key_list shows empty "to" column for implicit PK refs.
540 // The actual PK column is resolved at enforcement time.
541 (String::new(), 0)
542 };
543
544 let convert_action =
545 |action: &Option<vibesql_ast::ReferentialAction>| match action
546 .as_ref()
547 .unwrap_or(&vibesql_ast::ReferentialAction::NoAction)
548 {
549 vibesql_ast::ReferentialAction::Cascade => {
550 vibesql_catalog::ReferentialAction::Cascade
551 }
552 vibesql_ast::ReferentialAction::SetNull => {
553 vibesql_catalog::ReferentialAction::SetNull
554 }
555 vibesql_ast::ReferentialAction::SetDefault => {
556 vibesql_catalog::ReferentialAction::SetDefault
557 }
558 vibesql_ast::ReferentialAction::Restrict => {
559 vibesql_catalog::ReferentialAction::Restrict
560 }
561 vibesql_ast::ReferentialAction::NoAction => {
562 vibesql_catalog::ReferentialAction::NoAction
563 }
564 };
565
566 let (is_deferrable, initially_deferred) = deferral
567 .map(|d| (d.is_deferrable, d.initially_deferred))
568 .unwrap_or((false, false));
569
570 let fk = vibesql_catalog::ForeignKeyConstraint {
571 name: constraint.name.clone(),
572 column_names: vec![col_def.name.clone()],
573 column_indices: vec![col_idx],
574 parent_table: ref_table.clone(),
575 parent_column_names: vec![parent_col_name],
576 parent_column_indices: vec![parent_col_idx],
577 on_delete: convert_action(on_delete),
578 on_update: convert_action(on_update),
579 is_deferrable,
580 initially_deferred,
581 };
582
583 column_level_fks.push(fk);
584 }
585 }
586 }
587 // Add column-level FKs in reverse order to match SQLite's FK ID assignment
588 for fk in column_level_fks.into_iter().rev() {
589 table_schema.add_foreign_key(fk)?;
590 }
591
592 // If creating in a non-current schema, temporarily switch to it
593 let original_schema = database.catalog.get_current_schema().to_string();
594 let needs_schema_switch = schema_name != original_schema;
595
596 if needs_schema_switch {
597 database
598 .catalog
599 .set_current_schema(&schema_name)
600 .map_err(|e| ExecutorError::StorageError(format!("Schema error: {:?}", e)))?;
601 }
602
603 // Create internal sequences for AUTO_INCREMENT columns
604 for auto_inc_col in &auto_increment_columns {
605 let sequence_name = format!("{}_{}_seq", table_name, auto_inc_col);
606 database
607 .catalog
608 .create_sequence(
609 sequence_name.clone(),
610 Some(1), // start_with: 1
611 1, // increment_by: 1
612 Some(1), // min_value: 1
613 None, // max_value: unlimited
614 false, // cycle: false
615 )
616 .map_err(|e| {
617 ExecutorError::StorageError(format!(
618 "Failed to create sequence for AUTO_INCREMENT: {:?}",
619 e
620 ))
621 })?;
622 }
623
624 // Create table using Database API with TableIdentifier (handles both catalog and storage)
625 // Note: identifier was created at the start of this function with proper quoted semantics
626 let result = database
627 .create_table_with_identifier(table_schema.clone(), identifier.clone())
628 .map_err(|e| ExecutorError::StorageError(e.to_string()));
629
630 // Check if table creation succeeded before creating indexes
631 result?;
632
633 // Auto-create indexes for PRIMARY KEY and UNIQUE constraints
634 Self::create_implicit_indexes(database, &table_name, &table_schema)?;
635
636 // Restore original schema if we switched
637 if needs_schema_switch {
638 database
639 .catalog
640 .set_current_schema(&original_schema)
641 .map_err(|e| ExecutorError::StorageError(format!("Schema error: {:?}", e)))?;
642 }
643
644 // Return success message
645 Ok(format!("Table '{}' created successfully in schema '{}'", table_name, schema_name))
646 }
647
648 /// Create implicit indexes for PRIMARY KEY and UNIQUE constraints
649 ///
650 /// Production databases automatically create B-tree indexes for these constraints
651 /// to enable efficient query optimization. This function replicates that behavior.
652 fn create_implicit_indexes(
653 database: &mut Database,
654 table_name: &str,
655 table_schema: &TableSchema,
656 ) -> Result<(), ExecutorError> {
657 // Counter for SQLite-compatible auto-index naming: sqlite_autoindex_<table>_<n>
658 let mut autoindex_counter = 1;
659
660 // Auto-create PRIMARY KEY index
661 // Skip autoindex for INTEGER PRIMARY KEY - it's an alias for rowid
662 // and doesn't need a separate B-tree index (matches SQLite behavior)
663 if let Some(pk_cols) = &table_schema.primary_key {
664 if table_schema.rowid_alias_column.is_none() {
665 let index_name = format!("sqlite_autoindex_{}_{}", table_name, autoindex_counter);
666 autoindex_counter += 1;
667
668 // Create IndexColumn specs for the PRIMARY KEY columns
669 let index_columns: Vec<IndexColumn> = pk_cols
670 .iter()
671 .map(|col_name| IndexColumn::Column {
672 column_name: col_name.to_string(),
673 direction: OrderDirection::Asc,
674 prefix_length: None,
675 })
676 .collect();
677
678 // Add to catalog first
679 let index_metadata = vibesql_catalog::IndexMetadata::new(
680 index_name.clone(),
681 table_name.to_string(),
682 vibesql_catalog::IndexType::BTree,
683 index_columns
684 .iter()
685 .map(|col| {
686 vibesql_catalog::IndexedColumn::new_column(
687 col.expect_column_name().to_string(),
688 vibesql_catalog::SortOrder::Ascending,
689 )
690 })
691 .collect(),
692 true, // unique
693 );
694 database
695 .catalog
696 .add_index(index_metadata)
697 .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
698
699 // Create the actual B-tree index
700 database
701 .create_index(index_name, table_name.to_string(), true, index_columns)
702 .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
703 }
704 }
705
706 // Auto-create UNIQUE constraint indexes
707 for unique_cols in &table_schema.unique_constraints {
708 let index_name = format!("sqlite_autoindex_{}_{}", table_name, autoindex_counter);
709 autoindex_counter += 1;
710
711 // Create IndexColumn specs for the UNIQUE columns
712 let index_columns: Vec<IndexColumn> = unique_cols
713 .iter()
714 .map(|col_name| IndexColumn::Column {
715 column_name: col_name.to_string(),
716 direction: OrderDirection::Asc,
717 prefix_length: None,
718 })
719 .collect();
720
721 // Add to catalog first
722 let index_metadata = vibesql_catalog::IndexMetadata::new(
723 index_name.clone(),
724 table_name.to_string(),
725 vibesql_catalog::IndexType::BTree,
726 index_columns
727 .iter()
728 .map(|col| {
729 vibesql_catalog::IndexedColumn::new_column(
730 col.expect_column_name().to_string(),
731 vibesql_catalog::SortOrder::Ascending,
732 )
733 })
734 .collect(),
735 true, // unique
736 );
737 database
738 .catalog
739 .add_index(index_metadata)
740 .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
741
742 // Create the actual B-tree index
743 database
744 .create_index(index_name, table_name.to_string(), true, index_columns)
745 .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
746 }
747
748 Ok(())
749 }
750
751 /// Execute CREATE TABLE ... AS SELECT
752 ///
753 /// Creates a new table with schema derived from the SELECT result,
754 /// and populates it with the query results.
755 fn execute_create_as_select(
756 database: &mut Database,
757 table_name: &str,
758 schema_name: &str,
759 identifier: TableIdentifier,
760 if_not_exists: bool,
761 name_source: Option<&str>,
762 query: &vibesql_ast::SelectStmt,
763 ) -> Result<String, ExecutorError> {
764 // Check if table already exists
765 if database.catalog.table_exists_by_identifier(&identifier) {
766 if if_not_exists {
767 return Ok(format!(
768 "Table '{}' already exists in schema '{}' (skipped)",
769 table_name, schema_name
770 ));
771 }
772 // Echo the source quoting form when available (see the bare CREATE
773 // TABLE path above for rationale); fall back to the schema-qualified
774 // normalized name for programmatically-built ASTs.
775 let echoed = name_source
776 .map(str::to_string)
777 .unwrap_or_else(|| format!("{}.{}", schema_name, identifier.display()));
778 return Err(ExecutorError::TableAlreadyExists(echoed));
779 }
780
781 // Cross-type namespace collision: a CTAS name already used by an index
782 // must fail with `there is already an index named <name>`, just like the
783 // bare CREATE TABLE path. Keeps the schema reloadable (issue #5613).
784 if database
785 .catalog
786 .index_name_exists_in_schema(schema_name, identifier.table_canonical())
787 {
788 return Err(ExecutorError::SqliteCompatError(format!(
789 "there is already an index named {}",
790 table_name
791 )));
792 }
793
794 // Execute the SELECT query to get results
795 let rows = SelectExecutor::new(database).execute(query)?;
796
797 // Derive column names from the SELECT list (expanding wildcards if needed)
798 let column_names =
799 Self::derive_column_names_from_select_list(&query.select_list, &query.from, database)?;
800
801 // Derive column schema from the first row (if any) or default to BLOB
802 let columns: Vec<ColumnSchema> = column_names
803 .iter()
804 .enumerate()
805 .map(|(idx, col_name)| {
806 // Try to infer data type from the first row if available
807 let data_type = if !rows.is_empty() && idx < rows[0].values.len() {
808 Self::infer_data_type(&rows[0].values[idx])
809 } else {
810 // No rows or column - default to BLOB affinity
811 DataType::BinaryLargeObject
812 };
813
814 ColumnSchema {
815 name: col_name.to_string(),
816 data_type,
817 nullable: true, // Default to nullable for CTAS
818 default_value: None,
819 generated_expr: None,
820 collation: None, // CTAS doesn't preserve collation
821 is_exact_integer_type: false, // CTAS doesn't preserve exact type
822 }
823 })
824 .collect();
825
826 // Create the table schema
827 let table_schema = TableSchema::new(table_name.to_string(), columns);
828
829 // Create the table
830 database
831 .create_table_with_identifier(table_schema, identifier)
832 .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
833
834 // Insert the result rows into the new table
835 let row_count = rows.len();
836 for row in rows {
837 database
838 .insert_row(table_name, row)
839 .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
840 }
841
842 Ok(format!(
843 "Table '{}' created successfully in schema '{}' with {} rows",
844 table_name, schema_name, row_count
845 ))
846 }
847
848 /// Derive column names from a SELECT list, expanding wildcards using the database schema
849 fn derive_column_names_from_select_list(
850 select_list: &[vibesql_ast::SelectItem],
851 from: &Option<vibesql_ast::FromClause>,
852 database: &Database,
853 ) -> Result<Vec<String>, ExecutorError> {
854 let mut names = Vec::new();
855 let mut counter = 0;
856
857 for item in select_list {
858 match item {
859 vibesql_ast::SelectItem::Wildcard { .. } => {
860 // Expand wildcard using the FROM clause tables
861 let table_names = Self::get_table_names_from_from(from)?;
862 for table_name in table_names {
863 if let Some(schema) = database.catalog.get_table(&table_name) {
864 for col in &schema.columns {
865 names.push(col.name.clone());
866 }
867 } else {
868 return Err(ExecutorError::TableNotFound(table_name));
869 }
870 }
871 }
872 vibesql_ast::SelectItem::QualifiedWildcard { qualifier, .. } => {
873 // Expand table.* using the specific table's schema
874 if let Some(schema) = database.catalog.get_table(qualifier) {
875 for col in &schema.columns {
876 names.push(col.name.clone());
877 }
878 } else {
879 return Err(ExecutorError::TableNotFound(qualifier.clone()));
880 }
881 }
882 vibesql_ast::SelectItem::Expression { expr, alias, .. } => {
883 let name = if let Some(alias) = alias {
884 alias.clone()
885 } else {
886 // Try to derive from expression
887 Self::derive_column_name_from_expr(expr, &mut counter)
888 };
889 names.push(name);
890 }
891 }
892 }
893
894 Ok(names)
895 }
896
897 /// Extract table names from a FROM clause
898 fn get_table_names_from_from(
899 from: &Option<vibesql_ast::FromClause>,
900 ) -> Result<Vec<String>, ExecutorError> {
901 let mut names = Vec::new();
902
903 match from {
904 None => {
905 // No FROM clause - can't expand wildcard
906 return Err(ExecutorError::UnsupportedFeature(
907 "CREATE TABLE AS SELECT * requires a FROM clause".to_string(),
908 ));
909 }
910 Some(vibesql_ast::FromClause::Table { name, .. }) => {
911 names.push(name.clone());
912 }
913 Some(vibesql_ast::FromClause::Join { left, right, .. }) => {
914 // Recursively get tables from join
915 names.extend(Self::get_table_names_from_from(&Some(*left.clone()))?);
916 names.extend(Self::get_table_names_from_from(&Some(*right.clone()))?);
917 }
918 Some(vibesql_ast::FromClause::Subquery { alias, .. }) => {
919 // For derived tables (subqueries), we can't easily expand *
920 // because we'd need to recursively process the subquery
921 return Err(ExecutorError::UnsupportedFeature(format!(
922 "CREATE TABLE AS SELECT * from subquery '{}' not supported - please specify columns explicitly",
923 alias
924 )));
925 }
926 Some(vibesql_ast::FromClause::Values { alias, .. }) => {
927 // VALUES clause - can't determine column names from schema
928 return Err(ExecutorError::UnsupportedFeature(format!(
929 "CREATE TABLE AS SELECT * from VALUES '{}' not supported - please specify columns explicitly",
930 alias
931 )));
932 }
933 }
934
935 Ok(names)
936 }
937
938 /// Derive a column name from an expression
939 fn derive_column_name_from_expr(expr: &vibesql_ast::Expression, counter: &mut usize) -> String {
940 match expr {
941 vibesql_ast::Expression::ColumnRef(col_id) => col_id.column_canonical().to_string(),
942 vibesql_ast::Expression::Literal(_) => {
943 *counter += 1;
944 format!("column{}", counter)
945 }
946 vibesql_ast::Expression::Function { name, .. } => {
947 // Use the function name as the column name
948 name.to_string().to_lowercase()
949 }
950 _ => {
951 *counter += 1;
952 format!("column{}", counter)
953 }
954 }
955 }
956
957 /// Infer DataType from an SqlValue
958 fn infer_data_type(value: &vibesql_types::SqlValue) -> DataType {
959 use vibesql_types::SqlValue;
960 match value {
961 SqlValue::Null => DataType::BinaryLargeObject,
962 SqlValue::Boolean(_) => DataType::Boolean,
963 SqlValue::Integer(_) => DataType::Integer,
964 SqlValue::Bigint(_) => DataType::Bigint,
965 SqlValue::Smallint(_) => DataType::Smallint,
966 SqlValue::Unsigned(_) => DataType::Unsigned,
967 SqlValue::Float(_) | SqlValue::Real(_) => DataType::Real,
968 SqlValue::Double(_) | SqlValue::Numeric(_) => DataType::DoublePrecision,
969 SqlValue::Character(_) => DataType::Character { length: 255 },
970 SqlValue::Varchar(_) => DataType::Varchar { max_length: None },
971 SqlValue::Date(_) => DataType::Date,
972 SqlValue::Time(_) => DataType::Time { with_timezone: false },
973 SqlValue::Timestamp(_) => DataType::Timestamp { with_timezone: false },
974 SqlValue::Interval(_) => DataType::Interval {
975 start_field: vibesql_types::IntervalField::Day,
976 end_field: None,
977 },
978 SqlValue::Vector(v) => DataType::Vector { dimensions: v.len() as u32 },
979 SqlValue::Blob(_) => DataType::BinaryLargeObject,
980 }
981 }
982}