feat(types)!: drop the blob column type (ADR-0005 Amendment 2)
ci / gate (push) Failing after 35s
ci / manifests (push) Successful in 3s
website / deploy (push) Successful in 35s

blob was a dead-end: declarable but never fillable (no literal in either
mode, seed-unsupported), so a blob column could only ever hold NULL. Remove
Type::Blob + Value/CellValue::Blob + the base64 CSV path + the grammar slot
+ completion/render/type-change/seed handling + the binder refusal (whose
message also carried a user-facing "DSL" copy-rule bug). The vocabulary is
now nine types; base64 stays (clipboard OSC-52).

Backward compat is the ADR-0015 migration framework's first real use: a
v1->v2 format bump (CURRENT_SCHEMA_VERSION across serializer/parser/skeleton)
with a migrator that rewrites `type: blob` -> `type: text`, and a forced .db
rebuild from the migrated text when a blob column was actually converted
(the stale .db keeps a STRICT BLOB engine column). Conversion to text is
non-destructive and CSV-free. Covered by a full-stack integration test and a
Tier-4 PTY test that opens a real legacy v1-blob project.

Also sweeps the nine-type vocabulary through CLAUDE.md, requirements.md, the
website (type reference, seed doc, highlight grammar), and the ADR-0030/0033/
0035 cross-references; CHANGELOG Removed entry; handoff-79.

BREAKING CHANGE: the `blob` column type is removed. Existing projects that
declared a blob column are migrated on first open (the column becomes text;
the original project.yaml is kept as a .v1.bak).
This commit is contained in:
claude@clouddev1
2026-06-22 21:25:38 +00:00
parent 1a2002dbf6
commit 6b4c4dcea4
41 changed files with 506 additions and 390 deletions
+22 -14
View File
@@ -32,7 +32,11 @@ use super::{
#[must_use]
pub(super) fn serialize_schema(schema: &SchemaSnapshot) -> String {
let mut out = String::new();
let _ = writeln!(out, "version: 1");
let _ = writeln!(
out,
"version: {}",
crate::persistence::migrations::CURRENT_SCHEMA_VERSION
);
let _ = writeln!(out, "project:");
let _ = writeln!(out, " created_at: {}", quote_if_needed(&schema.created_at));
// ADR-0015 mode-restore amendment (issue #14): the input mode
@@ -283,7 +287,11 @@ const fn is_safe_yaml_char(c: char) -> bool {
pub(crate) fn parse_schema(body: &str) -> Result<SchemaSnapshot, YamlError> {
let raw: RawProject =
serde_norway::from_str(body).map_err(|e| YamlError::Syntax(e.to_string()))?;
if raw.version != 1 {
// The migration framework (ADR-0015) upgrades older project files to
// `CURRENT_SCHEMA_VERSION` before `parse_schema` ever runs, so the
// parser only accepts the current version (a stale version reaching
// here means migration was skipped — a bug worth surfacing).
if raw.version != crate::persistence::migrations::CURRENT_SCHEMA_VERSION {
return Err(YamlError::UnsupportedVersion(raw.version));
}
let mut tables: Vec<TableSchema> = Vec::with_capacity(raw.tables.len());
@@ -617,7 +625,7 @@ mod tests {
let body = serialize_schema(&snapshot());
// Spot-check structural lines rather than asserting on
// the whole blob — easier to read in failure output.
assert!(body.contains("version: 1"));
assert!(body.contains("version: 2"));
assert!(body.contains("created_at: 2026-05-07T14:30:12Z"));
assert!(body.contains("- name: Customers"));
assert!(body.contains("primary_key: [id]"));
@@ -752,7 +760,7 @@ mod tests {
// Older project files (written before unique indexes) omit the
// `unique` field; the `#[serde(default)]` makes it `false`.
let body = "\
version: 1
version: 2
project:
created_at: 2026-05-25T00:00:00Z
tables:
@@ -922,7 +930,7 @@ indexes:
// Back-compat: a project file written before §4g (bare-string
// check_constraints) parses with name = None.
let body = "\
version: 1
version: 2
project:
created_at: \"2026-05-25T00:00:00Z\"
tables:
@@ -949,7 +957,7 @@ indexes: []
// A project file written before table-level CHECK existed (no
// `check_constraints:` key) parses with an empty list.
let body = "\
version: 1
version: 2
project:
created_at: 2026-05-25T00:00:00Z
tables:
@@ -966,7 +974,7 @@ relationships: []
#[test]
fn parses_minimal_yaml_with_no_tables() {
let body = "\
version: 1
version: 2
project:
created_at: 2026-05-07T14:30:12Z
tables: []
@@ -993,7 +1001,7 @@ relationships: []
#[test]
fn rejects_unknown_column_type() {
let body = "\
version: 1
version: 2
project:
created_at: x
tables:
@@ -1012,7 +1020,7 @@ relationships: []
#[test]
fn rejects_unknown_action() {
let body = "\
version: 1
version: 2
project:
created_at: x
tables: []
@@ -1090,7 +1098,7 @@ relationships:
fn parse_schema_defaults_mode_to_simple_when_field_absent() {
// A pre-#14 project file carries no `mode:` field; it must
// parse with the default mode, not fail.
let body = "version: 1\nproject:\n created_at: x\ntables: []\nrelationships: []\n";
let body = "version: 2\nproject:\n created_at: x\ntables: []\nrelationships: []\n";
let parsed = parse_schema(body).expect("legacy file parses");
assert_eq!(parsed.mode, Mode::Simple);
}
@@ -1100,13 +1108,13 @@ relationships:
// `None` (no stored preference) must be distinct from an
// explicit `simple`, so restore-on-open precedence can tell
// "fall back to default" from "the user chose simple".
let absent = "version: 1\nproject:\n created_at: x\ntables: []\n";
let absent = "version: 2\nproject:\n created_at: x\ntables: []\n";
assert_eq!(parse_stored_mode(absent), None);
let explicit_simple = "version: 1\nproject:\n created_at: x\n mode: simple\ntables: []\n";
let explicit_simple = "version: 2\nproject:\n created_at: x\n mode: simple\ntables: []\n";
assert_eq!(parse_stored_mode(explicit_simple), Some(Mode::Simple));
let advanced = "version: 1\nproject:\n created_at: x\n mode: advanced\ntables: []\n";
let advanced = "version: 2\nproject:\n created_at: x\n mode: advanced\ntables: []\n";
assert_eq!(parse_stored_mode(advanced), Some(Mode::Advanced));
}
@@ -1114,7 +1122,7 @@ relationships:
fn parse_stored_mode_falls_back_to_none_on_unknown_value() {
// An unrecognised mode keyword degrades to "no preference"
// rather than rejecting the whole file over a UI hint.
let body = "version: 1\nproject:\n created_at: x\n mode: expert\ntables: []\n";
let body = "version: 2\nproject:\n created_at: x\n mode: expert\ntables: []\n";
assert_eq!(parse_stored_mode(body), None);
}
}