refactor: ColumnSpec / AddColumn carry constraint fields (ADR-0029 scaffolding)

Expand ColumnSpec and Command::AddColumn with the four
ADR-0029 constraint slots (not_null, unique, default, check),
all defaulting off; `Database::add_column` now takes a
ColumnSpec. No behaviour change — the grammar to set the
fields and the DDL to enforce them land in the following
commits. Isolated here so those commits stay readable.

Adds ColumnSpec::new for the unconstrained case; 110 call
sites updated. 1172 tests pass; clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-19 14:04:36 +00:00
parent 7bfd213ab3
commit eff2ee8d14
15 changed files with 237 additions and 194 deletions
+6 -8
View File
@@ -2209,10 +2209,7 @@ mod tests {
command,
&Command::CreateTable {
name: "Customers".to_string(),
columns: vec![crate::dsl::ColumnSpec {
name: "id".to_string(),
ty: Type::Serial,
}],
columns: vec![crate::dsl::ColumnSpec::new("id", Type::Serial)],
primary_key: vec!["id".to_string()],
},
);
@@ -2419,10 +2416,7 @@ mod tests {
let mut app = App::new();
let cmd = Command::CreateTable {
name: "Customers".to_string(),
columns: vec![crate::dsl::ColumnSpec {
name: "id".to_string(),
ty: Type::Serial,
}],
columns: vec![crate::dsl::ColumnSpec::new("id", Type::Serial)],
primary_key: vec!["id".to_string()],
};
let desc = sample_description("Customers");
@@ -3174,6 +3168,10 @@ mod tests {
table: "T".to_string(),
column: "Name".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
},
);
}
+63 -98
View File
@@ -446,8 +446,7 @@ enum Request {
},
AddColumn {
table: String,
column: String,
ty: Type,
column: ColumnSpec,
source: Option<String>,
reply: oneshot::Sender<Result<AddColumnResult, DbError>>,
},
@@ -670,15 +669,13 @@ impl Database {
pub async fn add_column(
&self,
table: String,
column: String,
ty: Type,
column: ColumnSpec,
source: Option<String>,
) -> Result<AddColumnResult, DbError> {
let (reply, recv) = oneshot::channel();
self.send(Request::AddColumn {
table,
column,
ty,
source,
reply,
})
@@ -1142,7 +1139,6 @@ fn handle_request(conn: &Connection, persistence: Option<&Persistence>, req: Req
Request::AddColumn {
table,
column,
ty,
source,
reply,
} => {
@@ -1152,7 +1148,6 @@ fn handle_request(conn: &Connection, persistence: Option<&Persistence>, req: Req
source.as_deref(),
&table,
&column,
ty,
));
}
Request::DropColumn {
@@ -1932,14 +1927,13 @@ fn do_add_column(
persistence: Option<&Persistence>,
source: Option<&str>,
table: &str,
column: &str,
ty: Type,
column: &ColumnSpec,
) -> Result<AddColumnResult, DbError> {
let auto_generated = matches!(ty, Type::Serial | Type::ShortId);
let auto_generated = matches!(column.ty, Type::Serial | Type::ShortId);
if !auto_generated {
return do_add_plain_column(conn, persistence, source, table, column, ty);
return do_add_plain_column(conn, persistence, source, table, column);
}
do_add_auto_generated_column(conn, persistence, source, table, column, ty)
do_add_auto_generated_column(conn, persistence, source, table, column)
}
/// Plain ALTER-TABLE path for non-auto-generated types.
@@ -1948,9 +1942,12 @@ fn do_add_plain_column(
persistence: Option<&Persistence>,
source: Option<&str>,
table: &str,
column: &str,
ty: Type,
spec: &ColumnSpec,
) -> Result<AddColumnResult, DbError> {
// ADR-0029: the constraint DDL suffix is emitted here once
// the constraint grammar lands; for now name + type only.
let ty = spec.ty;
let column = spec.name.as_str();
let ddl = format!(
"ALTER TABLE {tbl} ADD COLUMN {col} {sqlite_type};",
tbl = quote_ident(table),
@@ -1994,11 +1991,12 @@ fn do_add_auto_generated_column(
persistence: Option<&Persistence>,
source: Option<&str>,
table: &str,
column: &str,
ty: Type,
spec: &ColumnSpec,
) -> Result<AddColumnResult, DbError> {
use rusqlite::types::Value as RV;
let ty = spec.ty;
let column = spec.name.as_str();
let old_schema = read_schema(conn, table)?;
if old_schema.columns.iter().any(|c| c.name == column) {
return Err(DbError::Unsupported(format!(
@@ -5473,10 +5471,7 @@ mod tests {
}
fn col(name: &str, ty: Type) -> ColumnSpec {
ColumnSpec {
name: name.to_string(),
ty,
}
ColumnSpec::new(name, ty)
}
/// Convenience: a `serial`-PK table with a single `id` column.
@@ -5589,7 +5584,7 @@ mod tests {
let db = db();
make_id_table(&db, "Customers").await;
let result = db
.add_column("Customers".to_string(), "Name".to_string(), Type::Text, None)
.add_column("Customers".to_string(), ColumnSpec::new("Name".to_string(), Type::Text), None)
.await
.unwrap();
let desc = &result.description;
@@ -5609,7 +5604,7 @@ mod tests {
// datetime, decimal — all backed by TEXT).
make_id_table(&db, "T").await;
for ty in [Type::Date, Type::DateTime, Type::Decimal, Type::ShortId] {
db.add_column("T".to_string(), format!("c_{ty}"), ty, None)
db.add_column("T".to_string(), ColumnSpec::new(format!("c_{ty}"), ty), None)
.await
.unwrap();
}
@@ -5671,7 +5666,7 @@ mod tests {
make_id_table(&db, "T").await;
for ty in [Type::Text, Type::Int, Type::Real, Type::Bool, Type::ShortId] {
let col_name = format!("c_{ty}");
db.add_column("T".to_string(), col_name.clone(), ty, None)
db.add_column("T".to_string(), ColumnSpec::new(col_name.clone(), ty), None)
.await
.unwrap_or_else(|e| panic!("type {ty} failed: {e}"));
}
@@ -5688,7 +5683,7 @@ mod tests {
let db = db();
make_id_table(&db, "T").await;
let result = db
.add_column("T".to_string(), "code".to_string(), Type::Serial, None)
.add_column("T".to_string(), ColumnSpec::new("code".to_string(), Type::Serial), None)
.await
.unwrap();
let code = result
@@ -5712,7 +5707,7 @@ mod tests {
/// (text) and insert N rows, populating just `Name`.
async fn make_table_with_n_rows(db: &Database, table: &str, count: usize) {
make_id_table(db, table).await;
db.add_column(table.to_string(), "Name".to_string(), Type::Text, None)
db.add_column(table.to_string(), ColumnSpec::new("Name".to_string(), Type::Text), None)
.await
.unwrap();
for i in 0..count {
@@ -5732,7 +5727,7 @@ mod tests {
let db = db();
make_table_with_n_rows(&db, "T", 3).await;
let result = db
.add_column("T".to_string(), "seq".to_string(), Type::Serial, None)
.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None)
.await
.unwrap();
let seq = result
@@ -5765,7 +5760,7 @@ mod tests {
let db = db();
make_table_with_n_rows(&db, "T", 3).await;
let result = db
.add_column("T".to_string(), "tag".to_string(), Type::ShortId, None)
.add_column("T".to_string(), ColumnSpec::new("tag".to_string(), Type::ShortId), None)
.await
.unwrap();
let tag = result
@@ -5800,7 +5795,7 @@ mod tests {
// columns continue to use SQLite's rowid alias.
let db = db();
make_table_with_n_rows(&db, "T", 0).await;
db.add_column("T".to_string(), "seq".to_string(), Type::Serial, None)
db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None)
.await
.unwrap();
// Insert three rows providing only `Name`. The seq
@@ -5833,7 +5828,7 @@ mod tests {
// accepted (ADR-0018 Resolution 2).
let db = db();
make_table_with_n_rows(&db, "T", 0).await;
db.add_column("T".to_string(), "seq".to_string(), Type::Serial, None)
db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None)
.await
.unwrap();
// Insert with explicit seq=100.
@@ -5872,7 +5867,7 @@ mod tests {
// confirming the engine refuses.
let db = db();
make_table_with_n_rows(&db, "T", 2).await;
db.add_column("T".to_string(), "seq".to_string(), Type::Serial, None)
db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None)
.await
.unwrap();
// Attempt to UPDATE one row to have the same `seq` value
@@ -5928,7 +5923,7 @@ mod tests {
async fn add_column_to_missing_table_returns_no_such_table() {
let db = db();
let err = db
.add_column("Ghost".to_string(), "x".to_string(), Type::Text, None)
.add_column("Ghost".to_string(), ColumnSpec::new("x".to_string(), Type::Text), None)
.await
.unwrap_err();
match err {
@@ -5943,7 +5938,7 @@ mod tests {
async fn drop_column_removes_column_and_data() {
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Score".to_string(), Type::Int, None)
db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Int), None)
.await
.unwrap();
db.insert(
@@ -5993,12 +5988,7 @@ mod tests {
// Customers(id PK) ← Orders(cust_id FK)
make_id_table(&db, "Customers").await;
make_id_table(&db, "Orders").await;
db.add_column(
"Orders".to_string(),
"cust_id".to_string(),
Type::Int,
None,
)
db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None)
.await
.unwrap();
db.add_relationship(
@@ -6043,7 +6033,7 @@ mod tests {
/// something indexable.
async fn make_indexable_table(db: &Database, name: &str) {
make_id_table(db, name).await;
db.add_column(name.to_string(), "Email".to_string(), Type::Text, None)
db.add_column(name.to_string(), ColumnSpec::new("Email".to_string(), Type::Text), None)
.await
.expect("add Email column");
}
@@ -6081,10 +6071,10 @@ mod tests {
async fn add_index_composite_auto_name_joins_columns() {
let db = db();
make_id_table(&db, "Orders").await;
db.add_column("Orders".to_string(), "CustId".to_string(), Type::Int, None)
db.add_column("Orders".to_string(), ColumnSpec::new("CustId".to_string(), Type::Int), None)
.await
.unwrap();
db.add_column("Orders".to_string(), "Day".to_string(), Type::Date, None)
db.add_column("Orders".to_string(), ColumnSpec::new("Day".to_string(), Type::Date), None)
.await
.unwrap();
let desc = db
@@ -6107,7 +6097,7 @@ mod tests {
async fn add_index_rejects_duplicate_name() {
let db = db();
make_indexable_table(&db, "Customers").await;
db.add_column("Customers".to_string(), "Nick".to_string(), Type::Text, None)
db.add_column("Customers".to_string(), ColumnSpec::new("Nick".to_string(), Type::Text), None)
.await
.unwrap();
db.add_index(
@@ -6306,7 +6296,7 @@ mod tests {
// unrelated column must survive the rebuild (ADR-0025).
let db = db();
make_indexable_table(&db, "T").await;
db.add_column("T".to_string(), "Score".to_string(), Type::Int, None)
db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Int), None)
.await
.unwrap();
db.add_index(
@@ -6339,7 +6329,7 @@ mod tests {
async fn rename_column_updates_schema_and_metadata() {
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Old".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("Old".to_string(), Type::Text), None)
.await
.unwrap();
let desc = db
@@ -6358,12 +6348,7 @@ mod tests {
let db = db();
make_id_table(&db, "Customers").await;
make_id_table(&db, "Orders").await;
db.add_column(
"Orders".to_string(),
"cust_id".to_string(),
Type::Int,
None,
)
db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None)
.await
.unwrap();
db.add_relationship(
@@ -6423,10 +6408,10 @@ mod tests {
async fn rename_column_refuses_collision() {
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "A".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("A".to_string(), Type::Text), None)
.await
.unwrap();
db.add_column("T".to_string(), "B".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("B".to_string(), Type::Text), None)
.await
.unwrap();
let err = db
@@ -6440,7 +6425,7 @@ mod tests {
async fn rename_column_refuses_identity_rename() {
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "A".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("A".to_string(), Type::Text), None)
.await
.unwrap();
let err = db
@@ -6454,7 +6439,7 @@ mod tests {
async fn change_column_type_works_for_compatible_data() {
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Score".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Text), None)
.await
.unwrap();
// Insert numeric-looking strings.
@@ -6523,12 +6508,7 @@ mod tests {
let db = db();
make_id_table(&db, "Customers").await;
make_id_table(&db, "Orders").await;
db.add_column(
"Orders".to_string(),
"cust_id".to_string(),
Type::Int,
None,
)
db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None)
.await
.unwrap();
db.add_relationship(
@@ -6572,12 +6552,7 @@ mod tests {
let db = db();
make_id_table(&db, "Customers").await;
make_id_table(&db, "Orders").await;
db.add_column(
"Orders".to_string(),
"cust_id".to_string(),
Type::Int,
None,
)
db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None)
.await
.unwrap();
db.add_relationship(
@@ -6613,7 +6588,7 @@ mod tests {
// table.
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Score".to_string(), Type::Real, None)
db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Real), None)
.await
.unwrap();
for v in ["3.14", "2.71"] {
@@ -6664,7 +6639,7 @@ mod tests {
// the lossy count.
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Score".to_string(), Type::Real, None)
db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Real), None)
.await
.unwrap();
for v in ["3.14", "2.71", "5.0"] {
@@ -6699,7 +6674,7 @@ mod tests {
// does NOT help (per ADR-0017 §5 / §2 step 3).
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Note".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("Note".to_string(), Type::Text), None)
.await
.unwrap();
for v in ["abc", "123", "xyz"] {
@@ -6748,7 +6723,7 @@ mod tests {
// [client-side] note is expected.
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Flag".to_string(), Type::Int, None)
db.add_column("T".to_string(), ColumnSpec::new("Flag".to_string(), Type::Int), None)
.await
.unwrap();
for v in ["0", "1", "0"] {
@@ -6796,7 +6771,7 @@ mod tests {
// help (incompatible is not lossy).
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Flag".to_string(), Type::Int, None)
db.add_column("T".to_string(), ColumnSpec::new("Flag".to_string(), Type::Int), None)
.await
.unwrap();
for v in ["0", "1", "2"] {
@@ -6848,7 +6823,7 @@ mod tests {
// engine coercion; the note is suppressed.
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Score".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Text), None)
.await
.unwrap();
for v in ["1", "2", "3"] {
@@ -6933,7 +6908,7 @@ mod tests {
async fn change_column_type_blob_target_refused_statically() {
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Note".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("Note".to_string(), Type::Text), None)
.await
.unwrap();
let err = db
@@ -6956,12 +6931,7 @@ mod tests {
let db = db();
make_id_table(&db, "Customers").await;
make_id_table(&db, "Orders").await;
db.add_column(
"Orders".to_string(),
"cust_id".to_string(),
Type::Int,
None,
)
db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None)
.await
.unwrap();
db.add_relationship(
@@ -7004,7 +6974,7 @@ mod tests {
// note, and the structural change goes through.
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "Note".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("Note".to_string(), Type::Text), None)
.await
.unwrap();
let result = db
@@ -7037,7 +7007,7 @@ mod tests {
// existing values are preserved.
let db = db();
make_table_with_n_rows(&db, "T", 0).await;
db.add_column("T".to_string(), "code".to_string(), Type::Int, None)
db.add_column("T".to_string(), ColumnSpec::new("code".to_string(), Type::Int), None)
.await
.unwrap();
// Insert a few rows with explicit code values.
@@ -7087,7 +7057,7 @@ mod tests {
// uniqueness-collision diagnostic.
let db = db();
make_table_with_n_rows(&db, "T", 0).await;
db.add_column("T".to_string(), "code".to_string(), Type::Int, None)
db.add_column("T".to_string(), ColumnSpec::new("code".to_string(), Type::Int), None)
.await
.unwrap();
// Two rows with the same code.
@@ -7129,7 +7099,7 @@ mod tests {
// to route through int.
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "A".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("A".to_string(), Type::Text), None)
.await
.unwrap();
let err = db
@@ -7160,7 +7130,7 @@ mod tests {
// [client-side] note reports the auto-fill count.
let db = db();
make_table_with_n_rows(&db, "T", 0).await;
db.add_column("T".to_string(), "code".to_string(), Type::Int, None)
db.add_column("T".to_string(), ColumnSpec::new("code".to_string(), Type::Int), None)
.await
.unwrap();
// Three rows: one with code=5, two with NULL.
@@ -7216,7 +7186,7 @@ mod tests {
// text column get fresh shortids (ADR-0018 §3).
let db = db();
make_table_with_n_rows(&db, "T", 0).await;
db.add_column("T".to_string(), "tag".to_string(), Type::Text, None)
db.add_column("T".to_string(), ColumnSpec::new("tag".to_string(), Type::Text), None)
.await
.unwrap();
// One row with a valid shortid value, two with NULL.
@@ -7270,12 +7240,7 @@ mod tests {
let db = db();
make_id_table(&db, "Customers").await;
make_id_table(&db, "Orders").await;
db.add_column(
"Orders".to_string(),
"cust_id".to_string(),
Type::Int,
None,
)
db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None)
.await
.unwrap();
db.add_relationship(
@@ -7308,7 +7273,7 @@ mod tests {
async fn change_column_type_no_op_to_same_type_errors() {
let db = db();
make_id_table(&db, "T").await;
db.add_column("T".to_string(), "A".to_string(), Type::Int, None)
db.add_column("T".to_string(), ColumnSpec::new("A".to_string(), Type::Int), None)
.await
.unwrap();
let err = db
@@ -7345,7 +7310,7 @@ mod tests {
None)
.await
.unwrap();
db.add_column("Orders".to_string(), "CustId".to_string(), Type::Int, None)
db.add_column("Orders".to_string(), ColumnSpec::new("CustId".to_string(), Type::Int), None)
.await
.unwrap();
}
@@ -7517,7 +7482,7 @@ mod tests {
.await
.unwrap();
// Wrong type — text instead of int.
db.add_column("Orders".to_string(), "CustId".to_string(), Type::Text, None)
db.add_column("Orders".to_string(), ColumnSpec::new("CustId".to_string(), Type::Text), None)
.await
.unwrap();
@@ -7561,7 +7526,7 @@ mod tests {
None)
.await
.unwrap();
db.add_column("Orders".to_string(), "CustName".to_string(), Type::Text, None)
db.add_column("Orders".to_string(), ColumnSpec::new("CustName".to_string(), Type::Text), None)
.await
.unwrap();
let err = db
@@ -7690,7 +7655,7 @@ mod tests {
async fn add_relationship_with_duplicate_name_errors() {
let db = db();
customers_orders_setup(&db).await;
db.add_column("Orders".to_string(), "OtherCust".to_string(), Type::Int, None)
db.add_column("Orders".to_string(), ColumnSpec::new("OtherCust".to_string(), Type::Int), None)
.await
.unwrap();
db.add_relationship(
@@ -8604,7 +8569,7 @@ mod tests {
)
.await
.unwrap();
db.add_column("T".to_string(), "seq".to_string(), Type::Serial, None)
db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None)
.await
.unwrap();
// Read the persisted YAML straight from disk.
@@ -8647,7 +8612,7 @@ mod tests {
)
.await
.unwrap();
db.add_column("T".to_string(), "seq".to_string(), Type::Serial, None)
db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None)
.await
.unwrap();
// Tear down the .db file and rebuild from yaml + csvs.
+40 -3
View File
@@ -15,13 +15,42 @@ use crate::dsl::action::ReferentialAction;
use crate::dsl::types::Type;
use crate::dsl::value::Value;
/// A column at table-creation time: a name and a user-facing
/// type. Constraints beyond `PRIMARY KEY` (NOT NULL, UNIQUE,
/// CHECK, DEFAULT) come in later iterations.
/// A column at table-creation time: a name, a user-facing
/// type, and its column-level constraints (ADR-0029).
///
/// `PRIMARY KEY` is not represented here — it is a table-level
/// property carried separately in `Command::CreateTable`'s
/// `primary_key` list, since it may span columns.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnSpec {
pub name: String,
pub ty: Type,
/// `NOT NULL` — the column rejects `NULL` (ADR-0029).
pub not_null: bool,
/// `UNIQUE` — non-`NULL` values must be distinct (ADR-0029).
pub unique: bool,
/// `DEFAULT <literal>` — the value used when an `insert`
/// omits this column (ADR-0029).
pub default: Option<Value>,
/// `CHECK (<expr>)` — every row must satisfy this boolean
/// expression (ADR-0029).
pub check: Option<Expr>,
}
impl ColumnSpec {
/// A column spec carrying no constraints — the common case
/// for callers and tests that do not exercise ADR-0029.
#[must_use]
pub fn new(name: impl Into<String>, ty: Type) -> Self {
Self {
name: name.into(),
ty,
not_null: false,
unique: false,
default: None,
check: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -39,10 +68,18 @@ pub enum Command {
DropTable {
name: String,
},
/// Add a column to an existing table. The column carries
/// its constraints from the same suffix grammar as
/// `create table` (ADR-0029); `check` is `None` until the
/// CHECK grammar lands.
AddColumn {
table: String,
column: String,
ty: Type,
not_null: bool,
unique: bool,
default: Option<Value>,
check: Option<Expr>,
},
/// Remove a column from a table. Refused if the column is
/// part of the primary key or is involved in a declared
+7 -4
View File
@@ -616,6 +616,12 @@ fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
table: require_ident(path, "table_name")?,
column: require_ident(path, "column_name")?,
ty,
// Constraint suffix is wired in once the
// constraint grammar lands (ADR-0029).
not_null: false,
unique: false,
default: None,
check: None,
})
}
Some("1") => build_add_relationship(path),
@@ -907,10 +913,7 @@ fn build_create_table(path: &MatchedPath) -> Result<Command, ValidationError> {
let columns = pk_specs
.iter()
.map(|(n, t)| ColumnSpec {
name: n.clone(),
ty: *t,
})
.map(|(n, t)| ColumnSpec::new(n.clone(), *t))
.collect();
let primary_key = pk_specs.into_iter().map(|(n, _)| n).collect();
+25 -4
View File
@@ -358,10 +358,7 @@ mod tests {
}
fn col(name: &str, ty: Type) -> ColumnSpec {
ColumnSpec {
name: name.to_string(),
ty,
}
ColumnSpec::new(name, ty)
}
#[test]
@@ -657,6 +654,10 @@ mod tests {
table: "Customers".to_string(),
column: "Name".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
}
);
}
@@ -671,6 +672,10 @@ mod tests {
table: "T".to_string(),
column: "C".to_string(),
ty: *ty,
not_null: false,
unique: false,
default: None,
check: None,
}
);
}
@@ -684,6 +689,10 @@ mod tests {
table: "Customers".to_string(),
column: "Name".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
}
);
}
@@ -696,6 +705,10 @@ mod tests {
table: "Customers".to_string(),
column: "Name".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
}
);
}
@@ -708,6 +721,10 @@ mod tests {
table: "Customers".to_string(),
column: "Name".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
}
);
}
@@ -720,6 +737,10 @@ mod tests {
table: "T".to_string(),
column: "Name".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
}
);
}
+5 -4
View File
@@ -1346,6 +1346,10 @@ mod tests {
table: "Customers".to_string(),
column: "Email".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
}
);
}
@@ -1474,10 +1478,7 @@ mod tests {
use crate::dsl::command::ColumnSpec;
fn col(name: &str, ty: Type) -> ColumnSpec {
ColumnSpec {
name: name.to_string(),
ty,
}
ColumnSpec::new(name, ty)
}
#[test]
+22 -3
View File
@@ -32,7 +32,7 @@ use crate::db::{
AddColumnResult, ChangeColumnTypeResult, DataResult, Database, DbError, DeleteResult,
DropColumnResult, InsertResult, QueryPlan, TableDescription, UpdateResult,
};
use crate::dsl::Command;
use crate::dsl::{Command, ColumnSpec};
use crate::dsl::walker::Severity;
use crate::event::AppEvent;
use crate::project::{
@@ -1706,8 +1706,27 @@ async fn execute_command_typed(
.drop_table(name, src)
.await
.map(|()| CommandOutcome::Schema(None)),
Command::AddColumn { table, column, ty } => database
.add_column(table, column, ty, src)
Command::AddColumn {
table,
column,
ty,
not_null,
unique,
default,
check,
} => database
.add_column(
table,
ColumnSpec {
name: column,
ty,
not_null,
unique,
default,
check,
},
src,
)
.await
.map(CommandOutcome::AddColumn),
Command::DropColumn {
+17 -17
View File
@@ -45,8 +45,8 @@ fn enrich_unique_insert_resolves_table_column_value_and_pinpoint() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Int },
ColumnSpec { name: "name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Int),
ColumnSpec::new("name".to_string(), Type::Text),
],
vec!["id".to_string()],
None,
@@ -109,7 +109,7 @@ fn enrich_unique_insert_natural_order_short_form_resolves_value_via_schema() {
rt().block_on(async {
db.create_table(
"thing".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Int }],
vec![ColumnSpec::new("id".to_string(), Type::Int)],
vec!["id".to_string()],
None,
)
@@ -152,8 +152,8 @@ fn enrich_unique_update_resolves_value_from_assignments() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Int },
ColumnSpec { name: "name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Int),
ColumnSpec::new("name".to_string(), Type::Text),
],
vec!["id".to_string()],
None,
@@ -215,8 +215,8 @@ fn enrich_not_null_resolves_table_and_column() {
db.create_table(
"T".to_string(),
vec![
ColumnSpec { name: "a".to_string(), ty: Type::Int },
ColumnSpec { name: "b".to_string(), ty: Type::Text },
ColumnSpec::new("a".to_string(), Type::Int),
ColumnSpec::new("b".to_string(), Type::Text),
],
vec!["a".to_string(), "b".to_string()],
None,
@@ -258,7 +258,7 @@ fn enrich_fk_insert_resolves_parent_table_column_and_value() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Int }],
vec![ColumnSpec::new("id".to_string(), Type::Int)],
vec!["id".to_string()],
None,
)
@@ -267,8 +267,8 @@ fn enrich_fk_insert_resolves_parent_table_column_and_value() {
db.create_table(
"Orders".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Int },
ColumnSpec { name: "CustId".to_string(), ty: Type::Int },
ColumnSpec::new("id".to_string(), Type::Int),
ColumnSpec::new("CustId".to_string(), Type::Int),
],
vec!["id".to_string()],
None,
@@ -333,7 +333,7 @@ fn enrich_fk_insert_natural_order_multi_value_resolves_via_schema() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Int }],
vec![ColumnSpec::new("id".to_string(), Type::Int)],
vec!["id".to_string()],
None,
)
@@ -342,9 +342,9 @@ fn enrich_fk_insert_natural_order_multi_value_resolves_via_schema() {
db.create_table(
"Orders".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "CustId".to_string(), ty: Type::Int },
ColumnSpec { name: "Total".to_string(), ty: Type::Real },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("CustId".to_string(), Type::Int),
ColumnSpec::new("Total".to_string(), Type::Real),
],
vec!["id".to_string()],
None,
@@ -408,7 +408,7 @@ fn enrich_fk_delete_resolves_child_table() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Int }],
vec![ColumnSpec::new("id".to_string(), Type::Int)],
vec!["id".to_string()],
None,
)
@@ -417,8 +417,8 @@ fn enrich_fk_delete_resolves_child_table() {
db.create_table(
"Orders".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Int },
ColumnSpec { name: "CustId".to_string(), ty: Type::Int },
ColumnSpec::new("id".to_string(), Type::Int),
ColumnSpec::new("CustId".to_string(), Type::Int),
],
vec!["id".to_string()],
None,
+18 -18
View File
@@ -66,8 +66,8 @@ fn create_table_writes_yaml_and_history() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
@@ -96,8 +96,8 @@ fn insert_writes_csv_and_history() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
@@ -134,7 +134,7 @@ fn drop_table_removes_its_csv() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
)
@@ -172,7 +172,7 @@ fn delete_with_cascade_rewrites_both_csvs() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
)
@@ -181,8 +181,8 @@ fn delete_with_cascade_rewrites_both_csvs() {
db.create_table(
"Orders".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "CustId".to_string(), ty: Type::Int },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("CustId".to_string(), Type::Int),
],
vec!["id".to_string()],
Some("create table Orders with pk id(serial), CustId(int)".to_string()),
@@ -259,8 +259,8 @@ fn create_table_does_not_write_csv_for_empty_table() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
@@ -288,8 +288,8 @@ fn delete_all_rows_removes_csv() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
@@ -330,7 +330,7 @@ fn show_table_appends_history_only() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
)
@@ -363,7 +363,7 @@ fn failed_command_does_not_append_history_or_change_yaml() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
)
@@ -375,7 +375,7 @@ fn failed_command_does_not_append_history_or_change_yaml() {
let err = db
.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
)
@@ -404,7 +404,7 @@ fn project_yaml_carries_relationship_after_add() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
None,
)
@@ -413,8 +413,8 @@ fn project_yaml_carries_relationship_after_add() {
db.create_table(
"Orders".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "CustId".to_string(), ty: Type::Int },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("CustId".to_string(), Type::Int),
],
vec!["id".to_string()],
None,
+13 -14
View File
@@ -44,8 +44,8 @@ fn rebuild_restores_schema_only_project() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
@@ -98,8 +98,8 @@ fn rebuild_restores_rows_from_csv() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create".to_string()),
@@ -165,7 +165,7 @@ fn rebuild_restores_relationships_and_cascade_behaviour() {
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
Some("create".to_string()),
)
@@ -174,8 +174,8 @@ fn rebuild_restores_relationships_and_cascade_behaviour() {
db.create_table(
"Orders".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "CustId".to_string(), ty: Type::Int },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("CustId".to_string(), Type::Int),
],
vec!["id".to_string()],
Some("create".to_string()),
@@ -265,8 +265,8 @@ fn rebuild_reports_fatal_error_on_bad_csv_row() {
db.create_table(
"Numbers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "n".to_string(), ty: Type::Int },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("n".to_string(), Type::Int),
],
vec!["id".to_string()],
Some("create".to_string()),
@@ -326,7 +326,7 @@ fn rebuild_preserves_created_at_from_yaml() {
rt().block_on(async {
db.create_table(
"T".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
Some("create".to_string()),
)
@@ -377,8 +377,7 @@ fn rebuild_preserves_created_at_from_yaml() {
// describe is read-only; force a rewrite by adding a column.
db.add_column(
"T".to_string(),
"Note".to_string(),
Type::Text,
ColumnSpec::new("Note", Type::Text),
Some("add column".to_string()),
)
.await
@@ -410,8 +409,8 @@ fn rebuild_restores_indexes() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Email".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Email".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
+2 -2
View File
@@ -131,8 +131,8 @@ fn rebuild_against_populated_db_wipes_and_reloads() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create".to_string()),
+1 -1
View File
@@ -388,7 +388,7 @@ fn temp_with_a_table_is_no_longer_unmodified() {
rt.block_on(async {
db.create_table(
"T".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
vec!["id".to_string()],
Some("create".to_string()),
)
+2 -2
View File
@@ -299,8 +299,8 @@ fn end_to_end_export_then_import_real_project() {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create table Customers with pk id(serial)".to_string()),
+2 -8
View File
@@ -165,14 +165,8 @@ fn db_persists_across_open_close_cycles() {
db.create_table(
"Customers".to_string(),
vec![
rdbms_playground::dsl::ColumnSpec {
name: "id".to_string(),
ty: rdbms_playground::dsl::Type::Serial,
},
rdbms_playground::dsl::ColumnSpec {
name: "Name".to_string(),
ty: rdbms_playground::dsl::Type::Text,
},
rdbms_playground::dsl::ColumnSpec::new("id".to_string(), rdbms_playground::dsl::Type::Serial),
rdbms_playground::dsl::ColumnSpec::new("Name".to_string(), rdbms_playground::dsl::Type::Text),
],
vec!["id".to_string()],
None)
+14 -8
View File
@@ -90,10 +90,7 @@ fn typing_then_submitting_a_dsl_command_emits_execute_action() {
&actions,
&Command::CreateTable {
name: "Customers".to_string(),
columns: vec![ColumnSpec {
name: "id".to_string(),
ty: Type::Serial,
}],
columns: vec![ColumnSpec::new("id".to_string(), Type::Serial)],
primary_key: vec!["id".to_string()],
},
);
@@ -271,10 +268,7 @@ fn create_table_flow_updates_tables_list_and_structure_view() {
let actions = submit(&mut app);
let expected_cmd = Command::CreateTable {
name: "Customers".to_string(),
columns: vec![ColumnSpec {
name: "id".to_string(),
ty: Type::Serial,
}],
columns: vec![ColumnSpec::new("id".to_string(), Type::Serial)],
primary_key: vec!["id".to_string()],
};
assert_one_execute_dsl(&actions, &expected_cmd);
@@ -326,6 +320,10 @@ fn add_column_flow_updates_structure_view() {
table: "Customers".to_string(),
column: "Name".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
},
);
@@ -338,6 +336,10 @@ fn add_column_flow_updates_structure_view() {
table: "Customers".to_string(),
column: "Name".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
},
description: Some(updated.clone()),
});
@@ -479,6 +481,10 @@ fn add_relationship_flow_shows_inbound_section_on_parent() {
table: "Customers".to_string(),
column: "extra".to_string(),
ty: Type::Text,
not_null: false,
unique: false,
default: None,
check: None,
},
description: Some(customers),
});