feat: ADR-0035 4b — foreign keys in CREATE TABLE
Add foreign keys to advanced-mode SQL CREATE TABLE — the SQL spelling of an ADR-0013 named relationship, created in the same transaction as the table (one undo step). - Grammar: inline `<col> … REFERENCES <parent>[(<col>)] [ON DELETE/UPDATE …]` (a new column constraint) and table-level `[CONSTRAINT <name>] FOREIGN KEY (<col>) REFERENCES …` (two new element branches — both start on a concrete keyword, never a leading Optional, which would abort the element Choice). Referential clauses reuse shared::REFERENTIAL_CLAUSES. - Builder: greedy FK-clause consumption (parens consumed internally so they don't perturb the 4a.3 element-boundary depth tracker); inline FK auto-named, table FK takes an optional CONSTRAINT name. - Worker: do_create_table resolves + validates each FK before building the DDL (self-ref validates against the in-statement columns/PK; bare REFERENCES resolves to the parent's single-column PK, composite -> error; PK-target + Type::fk_target_type compatibility), emits the FOREIGN KEY clause identically to schema_to_ddl, and writes the relationship metadata in the create transaction. - Reuse: name/uniqueness/metadata-insert/type-compat factored into shared helpers; do_add_relationship refactored to use them. - FKs round-trip via the existing relationship plumbing (no new persistence structures); describe surfaces the relationship. Self-references and bare `REFERENCES <parent>` supported (user-confirmed). Self-ref pre-submit indicator wrinkle deferred to 4i (tracked in ADR §13, a code comment, and the plan). DA/runda round added cross-cutting probes (FK survives the add-column rebuild + a later rebuild_from_text; referential actions survive rebuild; drop-child clears the relationship; drop-parent refused; bare self-ref resolves to own PK) — all green, no fixes needed. 27 new tests (grammar/builder + Tier-3). Docs: ADR-0035 Status/§13, README, requirements.md Q1. Tests: 1795 passing, 0 failing, 1 ignored. Clippy clean.
This commit is contained in:
@@ -33,7 +33,7 @@ use tracing::{debug, info, warn};
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{
|
||||
ChangeColumnMode, Command, CompareOp, Constraint, ConstraintKind, Expr, IndexSelector,
|
||||
Operand, Predicate, RelationshipSelector, RowFilter,
|
||||
Operand, Predicate, RelationshipSelector, RowFilter, SqlForeignKey,
|
||||
};
|
||||
use crate::dsl::ColumnSpec;
|
||||
use crate::dsl::shortid;
|
||||
@@ -467,6 +467,7 @@ enum Request {
|
||||
primary_key: Vec<String>,
|
||||
unique_constraints: Vec<Vec<String>>,
|
||||
check_constraints: Vec<String>,
|
||||
foreign_keys: Vec<SqlForeignKey>,
|
||||
if_not_exists: bool,
|
||||
source: Option<String>,
|
||||
reply: oneshot::Sender<Result<CreateOutcome, DbError>>,
|
||||
@@ -838,6 +839,7 @@ impl Database {
|
||||
primary_key: Vec<String>,
|
||||
unique_constraints: Vec<Vec<String>>,
|
||||
check_constraints: Vec<String>,
|
||||
foreign_keys: Vec<SqlForeignKey>,
|
||||
if_not_exists: bool,
|
||||
source: Option<String>,
|
||||
) -> Result<CreateOutcome, DbError> {
|
||||
@@ -848,6 +850,7 @@ impl Database {
|
||||
primary_key,
|
||||
unique_constraints,
|
||||
check_constraints,
|
||||
foreign_keys,
|
||||
if_not_exists,
|
||||
source,
|
||||
reply,
|
||||
@@ -1708,6 +1711,7 @@ fn handle_request(
|
||||
&primary_key,
|
||||
&[],
|
||||
&[],
|
||||
&[],
|
||||
));
|
||||
}
|
||||
Request::SqlCreateTable {
|
||||
@@ -1716,6 +1720,7 @@ fn handle_request(
|
||||
primary_key,
|
||||
unique_constraints,
|
||||
check_constraints,
|
||||
foreign_keys,
|
||||
if_not_exists,
|
||||
source,
|
||||
reply,
|
||||
@@ -1745,6 +1750,7 @@ fn handle_request(
|
||||
&primary_key,
|
||||
&unique_constraints,
|
||||
&check_constraints,
|
||||
&foreign_keys,
|
||||
)
|
||||
.map(CreateOutcome::Created)
|
||||
});
|
||||
@@ -2637,6 +2643,7 @@ fn do_create_table(
|
||||
primary_key: &[String],
|
||||
unique_constraints: &[Vec<String>],
|
||||
check_constraints: &[String],
|
||||
foreign_keys: &[SqlForeignKey],
|
||||
) -> Result<TableDescription, DbError> {
|
||||
if columns.is_empty() {
|
||||
// SQLite requires at least one column. The DSL grammar
|
||||
@@ -2647,6 +2654,11 @@ fn do_create_table(
|
||||
"tables need at least one column".to_string(),
|
||||
));
|
||||
}
|
||||
// Resolve + validate any foreign keys before building the DDL, so
|
||||
// an invalid reference aborts before the table is created (ADR-0035
|
||||
// §5, sub-phase 4b). Self-references validate against the columns
|
||||
// being defined; other parents must already exist.
|
||||
let resolved_fks = resolve_create_table_fks(conn, name, columns, primary_key, foreign_keys)?;
|
||||
|
||||
// Inline `PRIMARY KEY` on the column when the table has a single
|
||||
// primary-key column and it is the **first** column — the exact
|
||||
@@ -2736,6 +2748,21 @@ fn do_create_table(
|
||||
ddl.push(')');
|
||||
}
|
||||
|
||||
// Foreign keys (ADR-0035 §5, sub-phase 4b) — emitted identically to
|
||||
// `schema_to_ddl` (the §6.1 two-generators rule): always the
|
||||
// explicit resolved parent column + both actions, so the create DDL
|
||||
// and the rebuild DDL match byte-for-byte.
|
||||
for fk in &resolved_fks {
|
||||
ddl.push_str(&format!(
|
||||
", FOREIGN KEY ({child}) REFERENCES {parent}({pcol}) ON DELETE {od} ON UPDATE {ou}",
|
||||
child = quote_ident(&fk.child_column),
|
||||
parent = quote_ident(&fk.parent_table),
|
||||
pcol = quote_ident(&fk.parent_column),
|
||||
od = fk.on_delete.sql_clause(),
|
||||
ou = fk.on_update.sql_clause(),
|
||||
));
|
||||
}
|
||||
|
||||
ddl.push_str(") STRICT;");
|
||||
debug!(ddl = %ddl, "create_table");
|
||||
|
||||
@@ -2761,6 +2788,21 @@ fn do_create_table(
|
||||
)
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
}
|
||||
// Foreign-key relationships (ADR-0035 §5): one metadata row per FK,
|
||||
// in the same transaction as the table — so the whole statement is
|
||||
// one undo step.
|
||||
for fk in &resolved_fks {
|
||||
insert_relationship_metadata(
|
||||
&tx,
|
||||
&fk.name,
|
||||
&fk.parent_table,
|
||||
&fk.parent_column,
|
||||
name,
|
||||
&fk.child_column,
|
||||
fk.on_delete,
|
||||
fk.on_update,
|
||||
)?;
|
||||
}
|
||||
let description = do_describe_table(conn, name)?;
|
||||
let changes = Changes {
|
||||
schema_dirty: true,
|
||||
@@ -5314,6 +5356,225 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Auto-name a relationship per ADR-0013
|
||||
/// (`<Parent>_<pcol>_to_<Child>_<ccol>`, reading in the declared
|
||||
/// direction) when `name` is `None`; otherwise use the supplied name.
|
||||
/// Shared by the DSL `add relationship` path and advanced-mode SQL
|
||||
/// `CREATE TABLE` foreign keys (ADR-0035 §5).
|
||||
fn resolve_relationship_name(
|
||||
name: Option<&str>,
|
||||
parent_table: &str,
|
||||
parent_column: &str,
|
||||
child_table: &str,
|
||||
child_column: &str,
|
||||
) -> String {
|
||||
name.map_or_else(
|
||||
|| format!("{parent_table}_{parent_column}_to_{child_table}_{child_column}"),
|
||||
ToString::to_string,
|
||||
)
|
||||
}
|
||||
|
||||
/// Reject a relationship name that collides with an existing one (the
|
||||
/// `name` column is UNIQUE, ADR-0013). Engine-neutral message.
|
||||
fn ensure_relationship_name_unique(conn: &Connection, name: &str) -> Result<(), DbError> {
|
||||
let collision: i64 = conn
|
||||
.query_row(
|
||||
&format!("SELECT COUNT(*) FROM {REL_TABLE} WHERE name = ?1;"),
|
||||
[name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
if collision > 0 {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"a relationship named `{name}` already exists. \
|
||||
Pick a different name or drop the existing one first."
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert one relationship metadata row into `REL_TABLE` (ADR-0013).
|
||||
/// Shared by `add relationship` (inside its rebuild transaction) and
|
||||
/// SQL `CREATE TABLE` foreign keys (inside the create transaction);
|
||||
/// `conn` may be a `&Transaction` (deref-coerces to `&Connection`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert_relationship_metadata(
|
||||
conn: &Connection,
|
||||
name: &str,
|
||||
parent_table: &str,
|
||||
parent_column: &str,
|
||||
child_table: &str,
|
||||
child_column: &str,
|
||||
on_delete: ReferentialAction,
|
||||
on_update: ReferentialAction,
|
||||
) -> Result<(), DbError> {
|
||||
conn.execute(
|
||||
&format!(
|
||||
"INSERT INTO {REL_TABLE} \
|
||||
(name, parent_table, parent_column, child_table, child_column, on_delete, on_update) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);"
|
||||
),
|
||||
[
|
||||
name,
|
||||
parent_table,
|
||||
parent_column,
|
||||
child_table,
|
||||
child_column,
|
||||
on_delete.keyword(),
|
||||
on_update.keyword(),
|
||||
],
|
||||
)
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate that an FK child column's type is compatible with the
|
||||
/// referenced parent column's type — it must equal the parent type's
|
||||
/// `fk_target_type()` (ADR-0011). Engine-neutral mismatch error.
|
||||
fn check_fk_type_compat(
|
||||
parent_table: &str,
|
||||
parent_column: &str,
|
||||
parent_type: Type,
|
||||
child_table: &str,
|
||||
child_column: &str,
|
||||
child_type: Type,
|
||||
) -> Result<(), DbError> {
|
||||
let expected = parent_type.fk_target_type();
|
||||
if child_type != expected {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"type mismatch: `{child_table}.{child_column}` is `{child_type}` but \
|
||||
a foreign key referencing `{parent_table}.{parent_column}` \
|
||||
(`{parent_type}`) requires `{expected}`. \
|
||||
Either change the column type, or pick a different FK column."
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A `CREATE TABLE` foreign key after resolution + validation
|
||||
/// (ADR-0035 §5, sub-phase 4b): the bare-`REFERENCES` parent column is
|
||||
/// resolved, the relationship name is decided, and PK-target /
|
||||
/// type-compat are checked.
|
||||
struct ResolvedFk {
|
||||
name: String,
|
||||
child_column: String,
|
||||
parent_table: String,
|
||||
parent_column: String,
|
||||
on_delete: ReferentialAction,
|
||||
on_update: ReferentialAction,
|
||||
}
|
||||
|
||||
/// Resolve + validate every foreign key declared in a `CREATE TABLE`
|
||||
/// (ADR-0035 §5, sub-phase 4b) **before** the table is built, so an
|
||||
/// invalid reference aborts cleanly. A self-referencing FK (parent is
|
||||
/// the table being created) is validated against the columns/PK being
|
||||
/// defined; any other parent must already exist. The bare
|
||||
/// `REFERENCES <parent>` form resolves to the parent's single-column
|
||||
/// PK (composite → error). Reuses the relationship validation/naming
|
||||
/// helpers shared with `do_add_relationship`.
|
||||
fn resolve_create_table_fks(
|
||||
conn: &Connection,
|
||||
table_name: &str,
|
||||
columns: &[ColumnSpec],
|
||||
primary_key: &[String],
|
||||
foreign_keys: &[SqlForeignKey],
|
||||
) -> Result<Vec<ResolvedFk>, DbError> {
|
||||
let mut out = Vec::with_capacity(foreign_keys.len());
|
||||
for fk in foreign_keys {
|
||||
// The parent's PK column list + a (name -> user type) lookup.
|
||||
// A self-reference reads the in-statement specs (the table does
|
||||
// not exist yet); any other parent must already exist.
|
||||
let (parent_pk, parent_cols): (Vec<String>, Vec<(String, Option<Type>)>) =
|
||||
if fk.parent_table == table_name {
|
||||
(
|
||||
primary_key.to_vec(),
|
||||
columns.iter().map(|c| (c.name.clone(), Some(c.ty))).collect(),
|
||||
)
|
||||
} else {
|
||||
let ps = read_schema(conn, &fk.parent_table)?;
|
||||
(
|
||||
ps.primary_key.clone(),
|
||||
ps.columns
|
||||
.iter()
|
||||
.map(|c| (c.name.clone(), c.user_type))
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
// Explicit referenced column, or the parent's single-column PK
|
||||
// for the bare `REFERENCES <parent>` form.
|
||||
let parent_column = match &fk.parent_column {
|
||||
Some(c) => c.clone(),
|
||||
None => {
|
||||
if parent_pk.len() == 1 {
|
||||
parent_pk[0].clone()
|
||||
} else {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"`{parent}` has a composite primary key, so a bare \
|
||||
reference is ambiguous — name the referenced column, \
|
||||
e.g. `REFERENCES {parent}(<col>)`.",
|
||||
parent = fk.parent_table,
|
||||
)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The referenced column must be a primary key (ADR-0011/0013).
|
||||
if !parent_pk.contains(&parent_column) {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"column `{}.{}` is not a primary key. Foreign keys must \
|
||||
reference a primary key (UNIQUE-target FKs land in a later \
|
||||
iteration).",
|
||||
fk.parent_table, parent_column
|
||||
)));
|
||||
}
|
||||
let parent_type = parent_cols
|
||||
.iter()
|
||||
.find(|(n, _)| n == &parent_column)
|
||||
.and_then(|(_, t)| *t)
|
||||
.ok_or_else(|| DbError::Sqlite {
|
||||
message: format!("no such column: {}.{}", fk.parent_table, parent_column),
|
||||
kind: SqliteErrorKind::NoSuchColumn,
|
||||
})?;
|
||||
|
||||
// The child column must be one of the columns being defined.
|
||||
let child = columns
|
||||
.iter()
|
||||
.find(|c| c.name == fk.child_column)
|
||||
.ok_or_else(|| DbError::Sqlite {
|
||||
message: format!("no such column: {}.{}", table_name, fk.child_column),
|
||||
kind: SqliteErrorKind::NoSuchColumn,
|
||||
})?;
|
||||
check_fk_type_compat(
|
||||
&fk.parent_table,
|
||||
&parent_column,
|
||||
parent_type,
|
||||
table_name,
|
||||
&fk.child_column,
|
||||
child.ty,
|
||||
)?;
|
||||
|
||||
let resolved_name = resolve_relationship_name(
|
||||
fk.name.as_deref(),
|
||||
&fk.parent_table,
|
||||
&parent_column,
|
||||
table_name,
|
||||
&fk.child_column,
|
||||
);
|
||||
ensure_relationship_name_unique(conn, &resolved_name)?;
|
||||
|
||||
out.push(ResolvedFk {
|
||||
name: resolved_name,
|
||||
child_column: fk.child_column.clone(),
|
||||
parent_table: fk.parent_table.clone(),
|
||||
parent_column,
|
||||
on_delete: fk.on_delete,
|
||||
on_update: fk.on_update,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn do_add_relationship(
|
||||
conn: &Connection,
|
||||
@@ -5388,38 +5649,21 @@ fn do_add_relationship(
|
||||
let actual = child_col.user_type.ok_or_else(|| DbError::Unsupported(
|
||||
"child column has no user type metadata".to_string(),
|
||||
))?;
|
||||
if actual != expected_child_type {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"type mismatch: `{child_table}.{child_column}` is `{actual}` but \
|
||||
a foreign key referencing `{parent_table}.{parent_column}` \
|
||||
(`{parent_user_type}`) requires `{expected_child_type}`. \
|
||||
Either change the column type, or pick a different FK column."
|
||||
)));
|
||||
}
|
||||
check_fk_type_compat(
|
||||
parent_table,
|
||||
parent_column,
|
||||
parent_user_type,
|
||||
child_table,
|
||||
child_column,
|
||||
actual,
|
||||
)?;
|
||||
}
|
||||
|
||||
// 4. Determine relationship name (auto-gen or supplied) and
|
||||
// check uniqueness against the metadata table.
|
||||
let resolved_name = name.map_or_else(
|
||||
// Auto-name follows the user-typed `from <Parent>.<col>
|
||||
// to <Child>.<col>` direction so the name reads as the
|
||||
// grammar reads — see ADR-0013.
|
||||
|| format!("{parent_table}_{parent_column}_to_{child_table}_{child_column}"),
|
||||
ToString::to_string,
|
||||
);
|
||||
let collision: i64 = conn
|
||||
.query_row(
|
||||
&format!("SELECT COUNT(*) FROM {REL_TABLE} WHERE name = ?1;"),
|
||||
[&resolved_name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
if collision > 0 {
|
||||
return Err(DbError::Unsupported(format!(
|
||||
"a relationship named `{resolved_name}` already exists. \
|
||||
Pick a different name or drop the existing one first."
|
||||
)));
|
||||
}
|
||||
let resolved_name =
|
||||
resolve_relationship_name(name, parent_table, parent_column, child_table, child_column);
|
||||
ensure_relationship_name_unique(conn, &resolved_name)?;
|
||||
|
||||
// 5. Build the new schema with the FK appended.
|
||||
let mut new_schema = child_schema.clone();
|
||||
@@ -5432,10 +5676,7 @@ fn do_add_relationship(
|
||||
});
|
||||
|
||||
// 6. Rebuild, with metadata updates inside the transaction.
|
||||
let on_delete_kw = on_delete.keyword();
|
||||
let on_update_kw = on_update.keyword();
|
||||
let column_user_type_kw = expected_child_type.keyword();
|
||||
let resolved_name_for_meta = resolved_name.as_str();
|
||||
rebuild_table(conn, child_table, &child_schema, &new_schema, |tx| {
|
||||
if needs_create_column {
|
||||
tx.execute(
|
||||
@@ -5447,23 +5688,16 @@ fn do_add_relationship(
|
||||
)
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
}
|
||||
tx.execute(
|
||||
&format!(
|
||||
"INSERT INTO {REL_TABLE} \
|
||||
(name, parent_table, parent_column, child_table, child_column, on_delete, on_update) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);"
|
||||
),
|
||||
[
|
||||
resolved_name_for_meta,
|
||||
parent_table,
|
||||
parent_column,
|
||||
child_table,
|
||||
child_column,
|
||||
on_delete_kw,
|
||||
on_update_kw,
|
||||
],
|
||||
)
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
insert_relationship_metadata(
|
||||
tx,
|
||||
&resolved_name,
|
||||
parent_table,
|
||||
parent_column,
|
||||
child_table,
|
||||
child_column,
|
||||
on_delete,
|
||||
on_update,
|
||||
)?;
|
||||
// Persistence runs inside the same tx so a write
|
||||
// failure rolls back both the schema and the metadata
|
||||
// (commit-db-last per ADR-0015 §6).
|
||||
|
||||
@@ -15,6 +15,33 @@ use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::types::Type;
|
||||
use crate::dsl::value::Value;
|
||||
|
||||
/// A foreign key declared in an advanced-mode SQL `CREATE TABLE`.
|
||||
///
|
||||
/// The SQL spelling of an ADR-0013 named relationship (ADR-0035 §5,
|
||||
/// sub-phase 4b). Produced by both the inline
|
||||
/// `<col> … REFERENCES <parent>[(<pcol>)] …` form (always auto-named)
|
||||
/// and the table-level `[CONSTRAINT <name>] FOREIGN KEY (<ccol>)
|
||||
/// REFERENCES <parent>[(<pcol>)] …` form. The relationship is created
|
||||
/// together with the table, in one transaction = one undo step.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SqlForeignKey {
|
||||
/// `CONSTRAINT <name>` on a table-level FK; `None` for an inline
|
||||
/// FK or an unnamed table FK (auto-named at execution per
|
||||
/// ADR-0013).
|
||||
pub name: Option<String>,
|
||||
/// The column in the table being created that holds the FK.
|
||||
pub child_column: String,
|
||||
/// The referenced (parent) table — may be the table being created
|
||||
/// (a self-referencing FK).
|
||||
pub parent_table: String,
|
||||
/// The referenced parent column. `None` for the bare
|
||||
/// `REFERENCES <parent>` form, resolved at execution to the
|
||||
/// parent's single-column primary key (ADR-0035 §4b, user-confirmed).
|
||||
pub parent_column: Option<String>,
|
||||
pub on_delete: ReferentialAction,
|
||||
pub on_update: ReferentialAction,
|
||||
}
|
||||
|
||||
/// A column at table-creation time: a name, a user-facing
|
||||
/// type, and its column-level constraints (ADR-0029).
|
||||
///
|
||||
@@ -153,6 +180,11 @@ pub enum Command {
|
||||
/// CHECK has no column to hang on and the engine reports no
|
||||
/// CHECKs, so it round-trips via a dedicated metadata table.
|
||||
check_constraints: Vec<String>,
|
||||
/// Foreign keys (ADR-0035 §5, sub-phase 4b) — inline
|
||||
/// `REFERENCES` and table-level `FOREIGN KEY`, each created as
|
||||
/// an ADR-0013 named relationship in the same transaction as
|
||||
/// the table (one undo step).
|
||||
foreign_keys: Vec<SqlForeignKey>,
|
||||
if_not_exists: bool,
|
||||
},
|
||||
/// Add a column to an existing table. The column carries
|
||||
|
||||
+123
-4
@@ -14,7 +14,7 @@
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{
|
||||
ChangeColumnMode, ColumnSpec, Command, Constraint, ConstraintKind, Expr, IndexSelector,
|
||||
RelationshipSelector,
|
||||
RelationshipSelector, SqlForeignKey,
|
||||
};
|
||||
use crate::dsl::value::Value;
|
||||
use crate::dsl::grammar::{
|
||||
@@ -1321,6 +1321,7 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
let mut primary_key: Vec<String> = Vec::new();
|
||||
let mut unique_constraints: Vec<Vec<String>> = Vec::new();
|
||||
let mut check_constraints: Vec<String> = Vec::new();
|
||||
let mut foreign_keys: Vec<SqlForeignKey> = Vec::new();
|
||||
let mut pending_name: Option<String> = None;
|
||||
// Distinguish a table-level `CHECK (…)` from a column-level one
|
||||
// (ADR-0035 §4a.3): both are spelled `check (`, and `Word` matches
|
||||
@@ -1334,6 +1335,9 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
// comma at the column-list interior, `depth == 1`.
|
||||
let mut column_open = false;
|
||||
let mut depth = 0usize;
|
||||
// A `CONSTRAINT <name>` prefix stashes the name until the following
|
||||
// table-level `FOREIGN KEY` consumes it (ADR-0035 §5, 4b).
|
||||
let mut pending_fk_name: Option<String> = None;
|
||||
let mut items = path.items.iter().peekable();
|
||||
while let Some(item) = items.next() {
|
||||
match &item.kind {
|
||||
@@ -1463,10 +1467,48 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
}
|
||||
}
|
||||
}
|
||||
// `constraint <name>` — stash the name for the table-level
|
||||
// `foreign key` that follows (ADR-0035 §5, 4b).
|
||||
MatchedKind::Word("constraint") => {
|
||||
if let Some(it) = items.next() {
|
||||
pending_fk_name = Some(it.text.clone());
|
||||
}
|
||||
}
|
||||
// Inline `references <parent> [(<col>)] [on …]` — a
|
||||
// column-level FK on the current column (ADR-0035 §5, 4b).
|
||||
// Auto-named at execution; the FK clause's own parens are
|
||||
// consumed in `consume_fk_reference`, so they don't perturb
|
||||
// the element-boundary `depth` tracker.
|
||||
MatchedKind::Word("references") => {
|
||||
let child_column = columns.last().map_or_else(String::new, |c| c.name.clone());
|
||||
foreign_keys.push(consume_fk_reference(&mut items, None, child_column));
|
||||
}
|
||||
// Table-level `[constraint <name>] foreign key (<col>)
|
||||
// references <parent> [(<col>)] [on …]` (ADR-0035 §5, 4b).
|
||||
MatchedKind::Word("foreign") => {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("key"))) {
|
||||
items.next(); // `key`
|
||||
}
|
||||
// `( <child column> )`
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct('('))) {
|
||||
items.next();
|
||||
}
|
||||
let child_column = items.next().map_or_else(String::new, |it| it.text.clone());
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct(')'))) {
|
||||
items.next();
|
||||
}
|
||||
// `references <parent> …`
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("references"))) {
|
||||
items.next();
|
||||
}
|
||||
let fk = consume_fk_reference(&mut items, pending_fk_name.take(), child_column);
|
||||
foreign_keys.push(fk);
|
||||
}
|
||||
// Track paren depth for element-boundary detection. The
|
||||
// column/`check`/`default`/table-`unique` arms consume their
|
||||
// own parens, so the only parens reaching here are the outer
|
||||
// column list, type length-args, and table-`PRIMARY KEY (…)`.
|
||||
// column/`check`/`default`/table-`unique`/FK arms consume
|
||||
// their own parens, so the only parens reaching here are the
|
||||
// outer column list, type length-args, and
|
||||
// table-`PRIMARY KEY (…)`.
|
||||
MatchedKind::Punct('(') => depth += 1,
|
||||
MatchedKind::Punct(')') => depth = depth.saturating_sub(1),
|
||||
// A comma at the column-list interior ends the current
|
||||
@@ -1494,6 +1536,7 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result<Command, V
|
||||
primary_key,
|
||||
unique_constraints,
|
||||
check_constraints,
|
||||
foreign_keys,
|
||||
if_not_exists,
|
||||
})
|
||||
}
|
||||
@@ -1564,6 +1607,82 @@ where
|
||||
Some((inner_start, inner_end))
|
||||
}
|
||||
|
||||
/// Consume the tail of a foreign-key reference from the matched-item
|
||||
/// stream (ADR-0035 §5, sub-phase 4b): the parent table ident, an
|
||||
/// optional `( <parent col> )`, and any `on <delete|update> <action>`
|
||||
/// clauses. The next item must be the parent-table ident (the
|
||||
/// `references` keyword was already consumed by the caller). The
|
||||
/// reference's own parens are consumed here, so they never reach the
|
||||
/// builder's element-boundary `depth` tracker.
|
||||
fn consume_fk_reference<'a, I>(
|
||||
items: &mut std::iter::Peekable<I>,
|
||||
name: Option<String>,
|
||||
child_column: String,
|
||||
) -> SqlForeignKey
|
||||
where
|
||||
I: Iterator<Item = &'a crate::dsl::walker::outcome::MatchedItem>,
|
||||
{
|
||||
let parent_table = items.next().map_or_else(String::new, |it| it.text.clone());
|
||||
// Optional `( <parent column> )`.
|
||||
let mut parent_column = None;
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct('('))) {
|
||||
items.next(); // `(`
|
||||
if let Some(it) = items.next() {
|
||||
parent_column = Some(it.text.clone());
|
||||
}
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct(')'))) {
|
||||
items.next(); // `)`
|
||||
}
|
||||
}
|
||||
// `on <delete|update> <action>` clauses, in either order, 0..2.
|
||||
let mut on_delete = ReferentialAction::default_action();
|
||||
let mut on_update = ReferentialAction::default_action();
|
||||
while matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("on"))) {
|
||||
items.next(); // `on`
|
||||
let target = items.next().map(|it| it.kind.clone());
|
||||
let action = consume_referential_action(items);
|
||||
match target {
|
||||
Some(MatchedKind::Word("delete")) => on_delete = action,
|
||||
Some(MatchedKind::Word("update")) => on_update = action,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
SqlForeignKey {
|
||||
name,
|
||||
child_column,
|
||||
parent_table,
|
||||
parent_column,
|
||||
on_delete,
|
||||
on_update,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a single referential action (`cascade` / `restrict` /
|
||||
/// `set null` / `no action`) from the matched-item stream — the
|
||||
/// two-word forms (`set null`, `no action`) consume their second word.
|
||||
fn consume_referential_action<'a, I>(items: &mut std::iter::Peekable<I>) -> ReferentialAction
|
||||
where
|
||||
I: Iterator<Item = &'a crate::dsl::walker::outcome::MatchedItem>,
|
||||
{
|
||||
match items.next().map(|it| it.kind.clone()) {
|
||||
Some(MatchedKind::Word("cascade")) => ReferentialAction::Cascade,
|
||||
Some(MatchedKind::Word("restrict")) => ReferentialAction::Restrict,
|
||||
Some(MatchedKind::Word("set")) => {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("null"))) {
|
||||
items.next();
|
||||
}
|
||||
ReferentialAction::SetNull
|
||||
}
|
||||
Some(MatchedKind::Word("no")) => {
|
||||
if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("action"))) {
|
||||
items.next();
|
||||
}
|
||||
ReferentialAction::NoAction
|
||||
}
|
||||
_ => ReferentialAction::default_action(),
|
||||
}
|
||||
}
|
||||
|
||||
pub static SQL_CREATE_TABLE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("create"),
|
||||
shape: Node::Subgrammar(&super::sql_create_table::SQL_CREATE_TABLE_SHAPE),
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
//! `sql_insert::SQL_INSERT_SHAPE`, which starts at `INTO`).
|
||||
|
||||
use crate::dsl::grammar::sql_select::reject_internal_table;
|
||||
use crate::dsl::grammar::{IdentSource, Node, ValidationError, Word, sql_expr};
|
||||
use crate::dsl::grammar::{IdentSource, Node, ValidationError, Word, shared, sql_expr};
|
||||
use crate::dsl::types::Type;
|
||||
|
||||
static COMMA: Node = Node::Punct(',');
|
||||
@@ -139,6 +139,63 @@ static CHECK_NODES: &[Node] = &[
|
||||
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||
Node::Punct(')'),
|
||||
];
|
||||
|
||||
// --- Foreign keys (ADR-0035 §5, sub-phase 4b) ---------------------
|
||||
//
|
||||
// Inline `REFERENCES <parent>[(<col>)] [ON DELETE/UPDATE …]` and
|
||||
// table-level `[CONSTRAINT <name>] FOREIGN KEY (<col>) REFERENCES …`.
|
||||
// Each is the SQL spelling of an ADR-0013 named relationship. The
|
||||
// referenced parent table/column use the `Tables`/`Columns` sources
|
||||
// (completion + existence hints), matching the `add relationship`
|
||||
// endpoints; the `( <col> )` is optional (the bare `REFERENCES
|
||||
// <parent>` form resolves to the parent's PK at execution).
|
||||
|
||||
// NOTE (4i): `IdentSource::Tables` existence-checks the parent — good
|
||||
// for the common case (a typo'd parent shows a pre-submit hint), but a
|
||||
// self-referencing FK (`references <self>` while creating `<self>`)
|
||||
// false-flags the not-yet-created table as unknown. Parse + execution
|
||||
// are correct (the self-ref is validated against the in-statement
|
||||
// columns); only the live typing indicator is briefly wrong. ADR-0035
|
||||
// §13 4i: teach the schema-existence diagnostic about the CREATE TABLE
|
||||
// target so the self-ref indicator stops lying.
|
||||
const FK_PARENT_TABLE: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
role: "fk_parent_table",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const FK_PARENT_COLUMN: Node = Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "fk_parent_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
static FK_PARENT_COL_NODES: &[Node] = &[Node::Punct('('), FK_PARENT_COLUMN, Node::Punct(')')];
|
||||
const FK_PARENT_COL_OPT: Node = Node::Optional(&Node::Seq(FK_PARENT_COL_NODES));
|
||||
|
||||
// `REFERENCES <parent> [ ( <col> ) ] [on delete/update …]` — the inline
|
||||
// column-FK constraint. The referential clauses reuse the shared
|
||||
// `on <delete|update> <action>` grammar (the DSL `add relationship`
|
||||
// keywords are the SQL keywords).
|
||||
static REFERENCES_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("references")),
|
||||
FK_PARENT_TABLE,
|
||||
FK_PARENT_COL_OPT,
|
||||
shared::REFERENTIAL_CLAUSES,
|
||||
];
|
||||
const REFERENCES_CLAUSE: Node = Node::Seq(REFERENCES_NODES);
|
||||
// `NOT NULL` | `UNIQUE` | `PRIMARY KEY` | `DEFAULT <expr>` |
|
||||
// `CHECK (<expr>)`. Each branch starts on a distinct keyword, so the
|
||||
// `Choice` never ambiguously commits.
|
||||
@@ -148,6 +205,7 @@ static COL_CONSTRAINT_CHOICES: &[Node] = &[
|
||||
Node::Seq(PRIMARY_KEY_NODES),
|
||||
Node::Seq(DEFAULT_NODES),
|
||||
Node::Seq(CHECK_NODES),
|
||||
REFERENCES_CLAUSE,
|
||||
];
|
||||
const COL_CONSTRAINT: Node = Node::Choice(COL_CONSTRAINT_CHOICES);
|
||||
/// Zero-or-more column constraints after the type (`min: 0`).
|
||||
@@ -250,13 +308,80 @@ static TABLE_CHECK_NODES: &[Node] = &[
|
||||
];
|
||||
const TABLE_CHECK: Node = Node::Seq(TABLE_CHECK_NODES);
|
||||
|
||||
// Table-level foreign key (ADR-0035 §5, sub-phase 4b):
|
||||
// `[CONSTRAINT <name>] FOREIGN KEY ( <child col> ) REFERENCES
|
||||
// <parent> [ ( <col> ) ] [on delete/update …]`. The child column is
|
||||
// being defined in this statement (`NewName`); the optional
|
||||
// `CONSTRAINT <name>` names the relationship (an inline `REFERENCES`
|
||||
// is always auto-named instead).
|
||||
const FK_CHILD_COLUMN: Node = Node::Ident {
|
||||
source: IdentSource::NewName,
|
||||
role: "fk_child_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
const FK_NAME: Node = Node::Ident {
|
||||
source: IdentSource::NewName,
|
||||
role: "fk_name",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
// The `FOREIGN KEY (col) REFERENCES …` body, shared by the named and
|
||||
// unnamed table-FK element branches. Each branch starts with a concrete
|
||||
// keyword (`foreign` / `constraint`) — never a leading `Optional`,
|
||||
// which would advance the Seq index and turn a later mismatch into a
|
||||
// hard failure that aborts the enclosing element `Choice`.
|
||||
static FOREIGN_KEY_BODY_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("foreign")),
|
||||
Node::Word(Word::keyword("key")),
|
||||
Node::Punct('('),
|
||||
FK_CHILD_COLUMN,
|
||||
Node::Punct(')'),
|
||||
Node::Word(Word::keyword("references")),
|
||||
FK_PARENT_TABLE,
|
||||
FK_PARENT_COL_OPT,
|
||||
shared::REFERENTIAL_CLAUSES,
|
||||
];
|
||||
const FOREIGN_KEY_BODY: Node = Node::Seq(FOREIGN_KEY_BODY_NODES);
|
||||
// `FOREIGN KEY (…) …` — the unnamed table-level FK (auto-named).
|
||||
const TABLE_FK: Node = FOREIGN_KEY_BODY;
|
||||
// `CONSTRAINT <name> FOREIGN KEY (…) …` — the named table-level FK.
|
||||
static TABLE_FK_NAMED_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("constraint")),
|
||||
FK_NAME,
|
||||
Node::Word(Word::keyword("foreign")),
|
||||
Node::Word(Word::keyword("key")),
|
||||
Node::Punct('('),
|
||||
FK_CHILD_COLUMN,
|
||||
Node::Punct(')'),
|
||||
Node::Word(Word::keyword("references")),
|
||||
FK_PARENT_TABLE,
|
||||
FK_PARENT_COL_OPT,
|
||||
shared::REFERENTIAL_CLAUSES,
|
||||
];
|
||||
const TABLE_FK_NAMED: Node = Node::Seq(TABLE_FK_NAMED_NODES);
|
||||
|
||||
// One element of the column list: a table-level `PRIMARY KEY (…)` /
|
||||
// `UNIQUE (…)` / `CHECK (…)`, or a column definition. The table-level
|
||||
// forms are tried first — each starts with a keyword (`primary` /
|
||||
// `unique` / `check`) that disambiguates it from a column name. (A
|
||||
// column literally named `primary`/`unique`/`check` is therefore
|
||||
// unavailable, the same trade real SQL makes with its reserved words.)
|
||||
static ELEMENT_CHOICES: &[Node] = &[TABLE_PK, TABLE_UNIQUE, TABLE_CHECK, COLUMN_DEF];
|
||||
// `UNIQUE (…)` / `CHECK (…)` / `[CONSTRAINT <name>] FOREIGN KEY (…)`,
|
||||
// or a column definition. The table-level forms are tried first — each
|
||||
// starts with a keyword (`primary` / `unique` / `check` / `constraint`
|
||||
// / `foreign`) that disambiguates it from a column name. (A column
|
||||
// literally named with one of those keywords is therefore unavailable,
|
||||
// the same trade real SQL makes with its reserved words.)
|
||||
static ELEMENT_CHOICES: &[Node] =
|
||||
&[TABLE_PK, TABLE_UNIQUE, TABLE_CHECK, TABLE_FK_NAMED, TABLE_FK, COLUMN_DEF];
|
||||
const ELEMENT: Node = Node::Choice(ELEMENT_CHOICES);
|
||||
|
||||
static COLUMN_LIST_NODES: &[Node] = &[
|
||||
@@ -471,11 +596,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_key_still_rejected() {
|
||||
// FK in CREATE TABLE is 4b — neither inline `REFERENCES` nor a
|
||||
// table-level `FOREIGN KEY` shape exists in the grammar yet.
|
||||
bad("table t (id int, ref int references other(id))");
|
||||
bad("table t (id int, foreign key (id) references other(id))");
|
||||
fn foreign_keys_accepted() {
|
||||
// 4b: inline `REFERENCES` and table-level `FOREIGN KEY`, with
|
||||
// optional `(col)`, `ON DELETE`/`ON UPDATE`, and `CONSTRAINT`.
|
||||
good("table t (id int, ref int references other(id))");
|
||||
good("table t (id int, ref int references other)"); // bare ref
|
||||
good("table t (id int, ref int references other(id) on delete cascade)");
|
||||
good("table t (id int, ref int references other(id) on update set null on delete restrict)");
|
||||
good("table t (id int, ref int, foreign key (ref) references other(id))");
|
||||
good("table t (id int, ref int, constraint fk_x foreign key (ref) references other(id))");
|
||||
good(
|
||||
"table t (id int, a int, b int, foreign key (a) references p(id), \
|
||||
foreign key (b) references q(id))",
|
||||
);
|
||||
// FK alongside the other table elements (coexistence).
|
||||
good("table t (id int primary key, ref int references other(id), check (id > 0))");
|
||||
// self-reference (parent is the table being created).
|
||||
good("table emp (id int primary key, mgr int references emp(id))");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +624,8 @@ mod tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod builder_tests {
|
||||
use crate::dsl::command::{ColumnSpec, Command};
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{ColumnSpec, Command, SqlForeignKey};
|
||||
use crate::dsl::parser::{parse_command, parse_command_in_mode};
|
||||
use crate::dsl::types::Type;
|
||||
use crate::mode::Mode;
|
||||
@@ -808,4 +946,129 @@ mod builder_tests {
|
||||
assert_eq!(checks, vec!["a > 0".to_string()]);
|
||||
assert!(col(&cols, "a").check_sql.is_none() && col(&cols, "b").check_sql.is_none());
|
||||
}
|
||||
|
||||
// --- 4b: foreign keys (inline + table-level) ---
|
||||
|
||||
/// Parse and return the foreign keys.
|
||||
fn parse_sct_fks(input: &str) -> Vec<SqlForeignKey> {
|
||||
match parse_command(input).expect("should parse") {
|
||||
Command::SqlCreateTable { foreign_keys, .. } => foreign_keys,
|
||||
other => panic!("expected SqlCreateTable, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_reference_captured() {
|
||||
let fks = parse_sct_fks("create table t (id int, pid int references parent(id))");
|
||||
assert_eq!(fks.len(), 1);
|
||||
let fk = &fks[0];
|
||||
assert_eq!(fk.name, None, "inline FK is auto-named at execution");
|
||||
assert_eq!(fk.child_column, "pid");
|
||||
assert_eq!(fk.parent_table, "parent");
|
||||
assert_eq!(fk.parent_column.as_deref(), Some("id"));
|
||||
assert_eq!(fk.on_delete, ReferentialAction::NoAction);
|
||||
assert_eq!(fk.on_update, ReferentialAction::NoAction);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_inline_reference_has_no_parent_column() {
|
||||
let fks = parse_sct_fks("create table t (id int, pid int references parent)");
|
||||
assert_eq!(fks[0].parent_column, None, "bare REFERENCES — resolved at execution");
|
||||
assert_eq!(fks[0].parent_table, "parent");
|
||||
assert_eq!(fks[0].child_column, "pid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_reference_with_referential_actions() {
|
||||
let fks = parse_sct_fks(
|
||||
"create table t (id int, pid int references parent(id) \
|
||||
on delete cascade on update set null)",
|
||||
);
|
||||
assert_eq!(fks[0].on_delete, ReferentialAction::Cascade);
|
||||
assert_eq!(fks[0].on_update, ReferentialAction::SetNull);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referential_action_order_is_flexible() {
|
||||
// `on update` before `on delete` — either order is accepted.
|
||||
let fks = parse_sct_fks(
|
||||
"create table t (id int, pid int references parent(id) \
|
||||
on update restrict on delete no action)",
|
||||
);
|
||||
assert_eq!(fks[0].on_update, ReferentialAction::Restrict);
|
||||
assert_eq!(fks[0].on_delete, ReferentialAction::NoAction);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_level_foreign_key_captured() {
|
||||
let fks =
|
||||
parse_sct_fks("create table t (id int, pid int, foreign key (pid) references parent(id))");
|
||||
assert_eq!(fks.len(), 1);
|
||||
assert_eq!(fks[0].name, None);
|
||||
assert_eq!(fks[0].child_column, "pid");
|
||||
assert_eq!(fks[0].parent_table, "parent");
|
||||
assert_eq!(fks[0].parent_column.as_deref(), Some("id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_level_foreign_key_with_constraint_name() {
|
||||
let fks = parse_sct_fks(
|
||||
"create table t (id int, pid int, \
|
||||
constraint fk_parent foreign key (pid) references parent(id))",
|
||||
);
|
||||
assert_eq!(fks[0].name.as_deref(), Some("fk_parent"));
|
||||
assert_eq!(fks[0].child_column, "pid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_foreign_keys_collected_in_order() {
|
||||
let fks = parse_sct_fks(
|
||||
"create table t (id int, a int, b int, \
|
||||
foreign key (a) references p(id), foreign key (b) references q(id))",
|
||||
);
|
||||
assert_eq!(fks.len(), 2);
|
||||
assert_eq!((fks[0].child_column.as_str(), fks[0].parent_table.as_str()), ("a", "p"));
|
||||
assert_eq!((fks[1].child_column.as_str(), fks[1].parent_table.as_str()), ("b", "q"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_referencing_foreign_key_captured() {
|
||||
let fks =
|
||||
parse_sct_fks("create table emp (id int primary key, mgr int references emp(id))");
|
||||
assert_eq!(fks[0].parent_table, "emp", "self-reference");
|
||||
assert_eq!(fks[0].child_column, "mgr");
|
||||
assert_eq!(fks[0].parent_column.as_deref(), Some("id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_fk_coexists_with_check_and_pk() {
|
||||
// FK clause must not be confused with the column CHECK that
|
||||
// follows, nor disturb the table-level PK / CHECK detection.
|
||||
match parse_command(
|
||||
"create table t (id int primary key, pid int references parent(id) check (pid > 0), \
|
||||
check (id <> pid))",
|
||||
)
|
||||
.expect("parses")
|
||||
{
|
||||
Command::SqlCreateTable {
|
||||
primary_key,
|
||||
foreign_keys,
|
||||
check_constraints,
|
||||
columns,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(primary_key, vec!["id".to_string()]);
|
||||
assert_eq!(foreign_keys.len(), 1);
|
||||
assert_eq!(foreign_keys[0].child_column, "pid");
|
||||
// the column-level CHECK still attaches to `pid`
|
||||
assert_eq!(
|
||||
columns.iter().find(|c| c.name == "pid").unwrap().check_sql.as_deref(),
|
||||
Some("pid > 0")
|
||||
);
|
||||
// the table-level CHECK is captured separately
|
||||
assert_eq!(check_constraints, vec!["id <> pid".to_string()]);
|
||||
}
|
||||
other => panic!("expected SqlCreateTable, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ pub mod walker;
|
||||
pub use action::ReferentialAction;
|
||||
pub use command::{
|
||||
AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, Expr, IndexSelector,
|
||||
MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, RowFilter,
|
||||
MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, RowFilter, SqlForeignKey,
|
||||
};
|
||||
pub use parser::{ParseError, parse_command};
|
||||
pub use types::Type;
|
||||
|
||||
@@ -1926,6 +1926,7 @@ async fn execute_command_typed(
|
||||
primary_key,
|
||||
unique_constraints,
|
||||
check_constraints,
|
||||
foreign_keys,
|
||||
if_not_exists,
|
||||
} => database
|
||||
.sql_create_table(
|
||||
@@ -1934,6 +1935,7 @@ async fn execute_command_typed(
|
||||
primary_key,
|
||||
unique_constraints,
|
||||
check_constraints,
|
||||
foreign_keys,
|
||||
if_not_exists,
|
||||
src,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user