db: column-constraint infrastructure — NOT NULL / UNIQUE / DEFAULT (ADR-0029)
The database layer now honours the ColumnSpec constraint fields end to end, ahead of the grammar that lets users type them. - `do_create_table` emits ` NOT NULL` / ` UNIQUE` / ` DEFAULT <literal>` per column via the new `column_constraints_sql` helper (the default literal bound against the column's type). - `ReadColumn` gains `default_sql`, read from `pragma_table_info.dflt_value`; `schema_to_ddl` emits it, so the rebuild-table primitive preserves DEFAULT — it already preserved NOT NULL / UNIQUE. - `ColumnDescription` gains `unique` / `default`; `do_describe_table` now sources columns from `read_schema` (one source of per-column truth) and `constraints_display` lists PK / NOT NULL / UNIQUE / DEFAULT. No user-facing change yet — no grammar produces constrained columns. Tests exercise creation, enforcement, describe, and rebuild-preservation programmatically. 1177 tests pass (+5); clippy clean.
This commit is contained in:
@@ -2173,6 +2173,8 @@ mod tests {
|
|||||||
sqlite_type: "INTEGER".to_string(),
|
sqlite_type: "INTEGER".to_string(),
|
||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: true,
|
primary_key: true,
|
||||||
|
unique: false,
|
||||||
|
default: None,
|
||||||
}],
|
}],
|
||||||
outbound_relationships: Vec::new(),
|
outbound_relationships: Vec::new(),
|
||||||
inbound_relationships: Vec::new(),
|
inbound_relationships: Vec::new(),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use std::thread;
|
|||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::dsl::action::ReferentialAction;
|
use crate::dsl::action::ReferentialAction;
|
||||||
use crate::dsl::command::{
|
use crate::dsl::command::{
|
||||||
@@ -129,6 +129,13 @@ pub struct ColumnDescription {
|
|||||||
pub sqlite_type: String,
|
pub sqlite_type: String,
|
||||||
pub notnull: bool,
|
pub notnull: bool,
|
||||||
pub primary_key: bool,
|
pub primary_key: bool,
|
||||||
|
/// Carries a single-column `UNIQUE` constraint (ADR-0029).
|
||||||
|
/// A PK column is not flagged here — the `primary_key`
|
||||||
|
/// flag already conveys its implicit uniqueness.
|
||||||
|
pub unique: bool,
|
||||||
|
/// The column's `DEFAULT` expression as SQLite reports it,
|
||||||
|
/// or `None` (ADR-0029).
|
||||||
|
pub default: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -1769,6 +1776,32 @@ fn quote_ident(name: &str) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render a column's constraint-DDL suffix (ADR-0029) — the
|
||||||
|
/// ` NOT NULL` / ` UNIQUE` / ` DEFAULT <literal>` fragment that
|
||||||
|
/// follows the column's type in a `CREATE TABLE` or `ALTER
|
||||||
|
/// TABLE ADD COLUMN`. The default literal is bound against the
|
||||||
|
/// column's user-facing type, so `default 18` on an `int`
|
||||||
|
/// column emits `DEFAULT 18` and `default 'x'` on a `text`
|
||||||
|
/// column emits `DEFAULT 'x'`. (`CHECK` joins this in a later
|
||||||
|
/// ADR-0029 step.)
|
||||||
|
fn column_constraints_sql(spec: &ColumnSpec) -> Result<String, DbError> {
|
||||||
|
let mut sql = String::new();
|
||||||
|
if spec.not_null {
|
||||||
|
sql.push_str(" NOT NULL");
|
||||||
|
}
|
||||||
|
if spec.unique {
|
||||||
|
sql.push_str(" UNIQUE");
|
||||||
|
}
|
||||||
|
if let Some(value) = &spec.default {
|
||||||
|
let bound = value
|
||||||
|
.bind_for_column(&spec.name, spec.ty)
|
||||||
|
.map_err(|e| DbError::InvalidValue(e.to_string()))?;
|
||||||
|
sql.push_str(" DEFAULT ");
|
||||||
|
sql.push_str(&sql_literal(&bound_to_sqlite_value(&bound)));
|
||||||
|
}
|
||||||
|
Ok(sql)
|
||||||
|
}
|
||||||
|
|
||||||
fn do_create_table(
|
fn do_create_table(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
persistence: Option<&Persistence>,
|
persistence: Option<&Persistence>,
|
||||||
@@ -1806,6 +1839,11 @@ fn do_create_table(
|
|||||||
if single_inline_pk {
|
if single_inline_pk {
|
||||||
clause.push_str(" PRIMARY KEY");
|
clause.push_str(" PRIMARY KEY");
|
||||||
}
|
}
|
||||||
|
// ADR-0029 column constraints. A single-column PK is
|
||||||
|
// already NOT NULL + UNIQUE; the grammar rejects
|
||||||
|
// redundant declarations (ADR-0029 §9) so a PK column
|
||||||
|
// never carries them here.
|
||||||
|
clause.push_str(&column_constraints_sql(col)?);
|
||||||
column_clauses.push(clause);
|
column_clauses.push(clause);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2014,6 +2052,7 @@ fn do_add_auto_generated_column(
|
|||||||
notnull: true,
|
notnull: true,
|
||||||
primary_key: false,
|
primary_key: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
|
default_sql: None,
|
||||||
user_type: Some(ty),
|
user_type: Some(ty),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3243,6 +3282,11 @@ struct ReadColumn {
|
|||||||
/// avoids double-emitting. Compound UNIQUE is out of scope
|
/// avoids double-emitting. Compound UNIQUE is out of scope
|
||||||
/// for v1 (ADR-0018 OOS-6 / future C3 work).
|
/// for v1 (ADR-0018 OOS-6 / future C3 work).
|
||||||
unique: bool,
|
unique: bool,
|
||||||
|
/// The column's `DEFAULT` expression as SQLite reports it
|
||||||
|
/// (`pragma_table_info.dflt_value`) — already a SQL
|
||||||
|
/// literal, echoed verbatim by `schema_to_ddl` so the
|
||||||
|
/// rebuild dance preserves it (ADR-0029).
|
||||||
|
default_sql: Option<String>,
|
||||||
user_type: Option<Type>,
|
user_type: Option<Type>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3259,7 +3303,7 @@ fn read_schema(conn: &Connection, table: &str) -> Result<ReadSchema, DbError> {
|
|||||||
// Columns + PK from pragma_table_info, joined with our user-type metadata.
|
// Columns + PK from pragma_table_info, joined with our user-type metadata.
|
||||||
let mut col_stmt = conn
|
let mut col_stmt = conn
|
||||||
.prepare(&format!(
|
.prepare(&format!(
|
||||||
"SELECT pti.name, pti.type, pti.\"notnull\", pti.pk, m.user_type \
|
"SELECT pti.name, pti.type, pti.\"notnull\", pti.pk, m.user_type, pti.dflt_value \
|
||||||
FROM pragma_table_info(?1) AS pti \
|
FROM pragma_table_info(?1) AS pti \
|
||||||
LEFT JOIN {META_TABLE} AS m \
|
LEFT JOIN {META_TABLE} AS m \
|
||||||
ON m.table_name = ?1 AND m.column_name = pti.name \
|
ON m.table_name = ?1 AND m.column_name = pti.name \
|
||||||
@@ -3276,6 +3320,7 @@ fn read_schema(conn: &Connection, table: &str) -> Result<ReadSchema, DbError> {
|
|||||||
notnull: row.get::<_, i64>(2)? != 0,
|
notnull: row.get::<_, i64>(2)? != 0,
|
||||||
primary_key: row.get::<_, i64>(3)? != 0,
|
primary_key: row.get::<_, i64>(3)? != 0,
|
||||||
unique: false, // filled in below from pragma_index_list
|
unique: false, // filled in below from pragma_index_list
|
||||||
|
default_sql: row.get(5)?,
|
||||||
user_type,
|
user_type,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -3492,6 +3537,12 @@ fn schema_to_ddl(table: &str, schema: &ReadSchema) -> String {
|
|||||||
if col.unique && !col.primary_key {
|
if col.unique && !col.primary_key {
|
||||||
clause.push_str(" UNIQUE");
|
clause.push_str(" UNIQUE");
|
||||||
}
|
}
|
||||||
|
// ADR-0029 DEFAULT — echoed verbatim from the value
|
||||||
|
// SQLite reported, so the rebuild dance preserves it.
|
||||||
|
if let Some(default_sql) = &col.default_sql {
|
||||||
|
clause.push_str(" DEFAULT ");
|
||||||
|
clause.push_str(default_sql);
|
||||||
|
}
|
||||||
clauses.push(clause);
|
clauses.push(clause);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3742,6 +3793,7 @@ fn do_add_relationship(
|
|||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: false,
|
primary_key: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
|
default_sql: None,
|
||||||
user_type: Some(expected_child_type),
|
user_type: Some(expected_child_type),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -4099,47 +4151,25 @@ fn do_describe_table_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn do_describe_table(conn: &Connection, name: &str) -> Result<TableDescription, DbError> {
|
fn do_describe_table(conn: &Connection, name: &str) -> Result<TableDescription, DbError> {
|
||||||
// `pragma_table_info` is a table-valued function in modern
|
// Column info — including the ADR-0029 constraints — comes
|
||||||
// SQLite; using it as a SELECT lets us bind the table name
|
// from `read_schema`, the single source of per-column truth
|
||||||
// via ? rather than splicing it into a PRAGMA statement.
|
// (it joins `pragma_table_info` with our type metadata and
|
||||||
// We LEFT JOIN our metadata table to recover the user-facing
|
// detects single-column UNIQUE via `pragma_index_list`). A
|
||||||
// type each column was declared as.
|
// missing table surfaces as `read_schema`'s NoSuchTable.
|
||||||
let mut stmt = conn
|
let schema = read_schema(conn, name)?;
|
||||||
.prepare(&format!(
|
let columns: Vec<ColumnDescription> = schema
|
||||||
"SELECT pti.name, pti.type, pti.\"notnull\", pti.pk, m.user_type \
|
.columns
|
||||||
FROM pragma_table_info(?1) AS pti \
|
.iter()
|
||||||
LEFT JOIN {META_TABLE} AS m \
|
.map(|c| ColumnDescription {
|
||||||
ON m.table_name = ?1 AND m.column_name = pti.name \
|
name: c.name.clone(),
|
||||||
ORDER BY pti.cid;"
|
user_type: c.user_type,
|
||||||
))
|
sqlite_type: c.sqlite_type.clone(),
|
||||||
.map_err(DbError::from_rusqlite)?;
|
notnull: c.notnull,
|
||||||
let rows = stmt
|
primary_key: c.primary_key,
|
||||||
.query_map([name], |row| {
|
unique: c.unique,
|
||||||
let user_type_kw: Option<String> = row.get(4)?;
|
default: c.default_sql.clone(),
|
||||||
let user_type = user_type_kw.and_then(|kw| kw.parse::<Type>().ok());
|
|
||||||
Ok(ColumnDescription {
|
|
||||||
name: row.get(0)?,
|
|
||||||
user_type,
|
|
||||||
sqlite_type: row.get(1)?,
|
|
||||||
notnull: row.get::<_, i64>(2)? != 0,
|
|
||||||
primary_key: row.get::<_, i64>(3)? != 0,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.map_err(DbError::from_rusqlite)?;
|
.collect();
|
||||||
let mut columns = Vec::new();
|
|
||||||
for row in rows {
|
|
||||||
columns.push(row.map_err(DbError::from_rusqlite)?);
|
|
||||||
}
|
|
||||||
if columns.is_empty() {
|
|
||||||
// pragma_table_info returns no rows for a non-existent
|
|
||||||
// table, which we surface as a NoSuchTable error so
|
|
||||||
// describe_table is not silently empty.
|
|
||||||
warn!(name, "describe_table: no columns (table missing?)");
|
|
||||||
return Err(DbError::Sqlite {
|
|
||||||
message: format!("no such table: {name}"),
|
|
||||||
kind: SqliteErrorKind::NoSuchTable,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let outbound_relationships = read_relationships_outbound(conn, name)?;
|
let outbound_relationships = read_relationships_outbound(conn, name)?;
|
||||||
let inbound_relationships = read_relationships_inbound(conn, name)?;
|
let inbound_relationships = read_relationships_inbound(conn, name)?;
|
||||||
@@ -5334,6 +5364,7 @@ fn build_read_schema(table: &TableSchema, relationships: &[RelationshipSchema])
|
|||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: table.primary_key.contains(&c.name),
|
primary_key: table.primary_key.contains(&c.name),
|
||||||
unique: c.unique,
|
unique: c.unique,
|
||||||
|
default_sql: None,
|
||||||
user_type: Some(c.user_type),
|
user_type: Some(c.user_type),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -8243,6 +8274,185 @@ mod tests {
|
|||||||
assert!(result.is_err(), "explaining a missing table should fail");
|
assert!(result.is_err(), "explaining a missing table should fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- column constraints at create-table (ADR-0029) ------
|
||||||
|
|
||||||
|
/// A `ColumnSpec` carrying the four constraint slots.
|
||||||
|
fn col_c(
|
||||||
|
name: &str,
|
||||||
|
ty: Type,
|
||||||
|
not_null: bool,
|
||||||
|
unique: bool,
|
||||||
|
default: Option<Value>,
|
||||||
|
) -> ColumnSpec {
|
||||||
|
ColumnSpec {
|
||||||
|
name: name.to_string(),
|
||||||
|
ty,
|
||||||
|
not_null,
|
||||||
|
unique,
|
||||||
|
default,
|
||||||
|
check: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create `T(id serial pk, <extra>)`.
|
||||||
|
async fn table_with(db: &Database, extra: ColumnSpec) -> TableDescription {
|
||||||
|
db.create_table(
|
||||||
|
"T".to_string(),
|
||||||
|
vec![col("id", Type::Serial), extra],
|
||||||
|
vec!["id".to_string()],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("create table")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_table_not_null_column_rejects_a_null_insert() {
|
||||||
|
let db = db();
|
||||||
|
table_with(&db, col_c("name", Type::Text, true, false, None)).await;
|
||||||
|
let ok = db
|
||||||
|
.insert(
|
||||||
|
"T".to_string(),
|
||||||
|
Some(vec!["name".to_string()]),
|
||||||
|
vec![Value::Text("Alice".to_string())],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(ok.is_ok(), "a non-null value is accepted");
|
||||||
|
let bad = db
|
||||||
|
.insert(
|
||||||
|
"T".to_string(),
|
||||||
|
Some(vec!["name".to_string()]),
|
||||||
|
vec![Value::Null],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(bad.is_err(), "NULL into a NOT NULL column is refused");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_table_unique_column_rejects_a_duplicate_insert() {
|
||||||
|
let db = db();
|
||||||
|
table_with(&db, col_c("email", Type::Text, false, true, None)).await;
|
||||||
|
let insert_email = |v: &str| {
|
||||||
|
db.insert(
|
||||||
|
"T".to_string(),
|
||||||
|
Some(vec!["email".to_string()]),
|
||||||
|
vec![Value::Text(v.to_string())],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert!(insert_email("a@x.io").await.is_ok());
|
||||||
|
assert!(
|
||||||
|
insert_email("a@x.io").await.is_err(),
|
||||||
|
"a duplicate value violates UNIQUE",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
insert_email("b@x.io").await.is_ok(),
|
||||||
|
"a distinct value is still accepted",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_table_default_applies_when_the_column_is_omitted() {
|
||||||
|
let db = db();
|
||||||
|
db.create_table(
|
||||||
|
"T".to_string(),
|
||||||
|
vec![
|
||||||
|
col("id", Type::Serial),
|
||||||
|
col("name", Type::Text),
|
||||||
|
col_c("tier", Type::Int, false, false, Some(Value::Number("3".to_string()))),
|
||||||
|
],
|
||||||
|
vec!["id".to_string()],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// Insert names only `name` — `tier` is omitted and so
|
||||||
|
// takes its DEFAULT; `id` auto-fills as a serial.
|
||||||
|
db.insert(
|
||||||
|
"T".to_string(),
|
||||||
|
Some(vec!["name".to_string()]),
|
||||||
|
vec![Value::Text("Alice".to_string())],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let data = db
|
||||||
|
.query_data("T".to_string(), None, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let tier_idx = data.columns.iter().position(|c| c == "tier").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
data.rows[0][tier_idx].as_deref(),
|
||||||
|
Some("3"),
|
||||||
|
"the omitted column took its DEFAULT",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn describe_surfaces_the_column_constraints() {
|
||||||
|
let db = db();
|
||||||
|
let desc = table_with(
|
||||||
|
&db,
|
||||||
|
col_c(
|
||||||
|
"email",
|
||||||
|
Type::Text,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
Some(Value::Text("none".to_string())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let email = desc.columns.iter().find(|c| c.name == "email").unwrap();
|
||||||
|
assert!(email.notnull, "NOT NULL is described");
|
||||||
|
assert!(email.unique, "UNIQUE is described");
|
||||||
|
assert_eq!(
|
||||||
|
email.default.as_deref(),
|
||||||
|
Some("'none'"),
|
||||||
|
"the DEFAULT literal is described",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rebuild_preserves_column_constraints() {
|
||||||
|
// A type change on one column rebuilds the whole table
|
||||||
|
// through `schema_to_ddl`; the constraints on the other
|
||||||
|
// columns must survive that round-trip.
|
||||||
|
let db = db();
|
||||||
|
db.create_table(
|
||||||
|
"T".to_string(),
|
||||||
|
vec![
|
||||||
|
col("id", Type::Serial),
|
||||||
|
col_c("email", Type::Text, true, true, None),
|
||||||
|
col_c("tier", Type::Int, false, false, Some(Value::Number("1".to_string()))),
|
||||||
|
],
|
||||||
|
vec!["id".to_string()],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// Change `tier`'s type — int -> decimal — forcing a rebuild.
|
||||||
|
db.change_column_type(
|
||||||
|
"T".to_string(),
|
||||||
|
"tier".to_string(),
|
||||||
|
Type::Decimal,
|
||||||
|
ChangeColumnMode::Default,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let desc = db.describe_table("T".to_string(), None).await.unwrap();
|
||||||
|
let email = desc.columns.iter().find(|c| c.name == "email").unwrap();
|
||||||
|
assert!(email.notnull && email.unique, "email keeps NOT NULL + UNIQUE");
|
||||||
|
let tier = desc.columns.iter().find(|c| c.name == "tier").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
tier.default.as_deref(),
|
||||||
|
Some("1"),
|
||||||
|
"tier keeps its DEFAULT across the rebuild",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn update_with_all_rows_affects_everything() {
|
async fn update_with_all_rows_affects_everything() {
|
||||||
let db = db();
|
let db = db();
|
||||||
@@ -8513,6 +8723,7 @@ mod tests {
|
|||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: true,
|
primary_key: true,
|
||||||
unique: false,
|
unique: false,
|
||||||
|
default_sql: None,
|
||||||
user_type: Some(Type::Serial),
|
user_type: Some(Type::Serial),
|
||||||
},
|
},
|
||||||
ReadColumn {
|
ReadColumn {
|
||||||
@@ -8521,6 +8732,7 @@ mod tests {
|
|||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: false,
|
primary_key: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
|
default_sql: None,
|
||||||
user_type: Some(Type::Text),
|
user_type: Some(Type::Text),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+15
-3
@@ -354,12 +354,20 @@ fn type_display(c: &ColumnDescription) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn constraints_display(c: &ColumnDescription) -> String {
|
fn constraints_display(c: &ColumnDescription) -> String {
|
||||||
let mut parts: Vec<&str> = Vec::new();
|
let mut parts: Vec<String> = Vec::new();
|
||||||
if c.primary_key {
|
if c.primary_key {
|
||||||
parts.push("PK");
|
parts.push("PK".to_string());
|
||||||
}
|
}
|
||||||
if c.notnull {
|
if c.notnull {
|
||||||
parts.push("NOT NULL");
|
parts.push("NOT NULL".to_string());
|
||||||
|
}
|
||||||
|
// ADR-0029: a PK column's implicit uniqueness is already
|
||||||
|
// conveyed by `PK`; `unique` is only set for non-PK columns.
|
||||||
|
if c.unique {
|
||||||
|
parts.push("UNIQUE".to_string());
|
||||||
|
}
|
||||||
|
if let Some(default) = &c.default {
|
||||||
|
parts.push(format!("DEFAULT {default}"));
|
||||||
}
|
}
|
||||||
parts.join(", ")
|
parts.join(", ")
|
||||||
}
|
}
|
||||||
@@ -511,6 +519,8 @@ mod tests {
|
|||||||
},
|
},
|
||||||
notnull,
|
notnull,
|
||||||
primary_key: pk,
|
primary_key: pk,
|
||||||
|
unique: false,
|
||||||
|
default: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -978,6 +988,8 @@ mod tests {
|
|||||||
sqlite_type: "INTEGER".to_string(),
|
sqlite_type: "INTEGER".to_string(),
|
||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: false,
|
primary_key: false,
|
||||||
|
unique: false,
|
||||||
|
default: None,
|
||||||
}],
|
}],
|
||||||
outbound_relationships: Vec::new(),
|
outbound_relationships: Vec::new(),
|
||||||
inbound_relationships: Vec::new(),
|
inbound_relationships: Vec::new(),
|
||||||
|
|||||||
@@ -1115,6 +1115,8 @@ mod tests {
|
|||||||
sqlite_type: "INTEGER".to_string(),
|
sqlite_type: "INTEGER".to_string(),
|
||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: true,
|
primary_key: true,
|
||||||
|
unique: false,
|
||||||
|
default: None,
|
||||||
},
|
},
|
||||||
ColumnDescription {
|
ColumnDescription {
|
||||||
name: "Name".to_string(),
|
name: "Name".to_string(),
|
||||||
@@ -1122,6 +1124,8 @@ mod tests {
|
|||||||
sqlite_type: "TEXT".to_string(),
|
sqlite_type: "TEXT".to_string(),
|
||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: false,
|
primary_key: false,
|
||||||
|
unique: false,
|
||||||
|
default: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
outbound_relationships: Vec::new(),
|
outbound_relationships: Vec::new(),
|
||||||
|
|||||||
@@ -250,6 +250,8 @@ fn fake_table(name: &str, columns: &[(&str, Type, bool)]) -> TableDescription {
|
|||||||
sqlite_type: t.sqlite_strict_type().to_string(),
|
sqlite_type: t.sqlite_strict_type().to_string(),
|
||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: *pk,
|
primary_key: *pk,
|
||||||
|
unique: false,
|
||||||
|
default: None,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
outbound_relationships: Vec::new(),
|
outbound_relationships: Vec::new(),
|
||||||
@@ -415,6 +417,8 @@ fn add_relationship_flow_shows_parent_side_with_inbound_section() {
|
|||||||
sqlite_type: "INTEGER".to_string(),
|
sqlite_type: "INTEGER".to_string(),
|
||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: true,
|
primary_key: true,
|
||||||
|
unique: false,
|
||||||
|
default: None,
|
||||||
}],
|
}],
|
||||||
outbound_relationships: Vec::new(),
|
outbound_relationships: Vec::new(),
|
||||||
inbound_relationships: vec![RelationshipEnd {
|
inbound_relationships: vec![RelationshipEnd {
|
||||||
@@ -464,6 +468,8 @@ fn add_relationship_flow_shows_inbound_section_on_parent() {
|
|||||||
sqlite_type: "INTEGER".to_string(),
|
sqlite_type: "INTEGER".to_string(),
|
||||||
notnull: false,
|
notnull: false,
|
||||||
primary_key: true,
|
primary_key: true,
|
||||||
|
unique: false,
|
||||||
|
default: None,
|
||||||
}],
|
}],
|
||||||
outbound_relationships: Vec::new(),
|
outbound_relationships: Vec::new(),
|
||||||
inbound_relationships: vec![RelationshipEnd {
|
inbound_relationships: vec![RelationshipEnd {
|
||||||
|
|||||||
Reference in New Issue
Block a user