feat(types)!: drop the blob column type (ADR-0005 Amendment 2)
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:
+22
-14
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user