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
+1 -7
View File
@@ -263,11 +263,6 @@ const DATETIME_SLOT: Node = Node::TypedValueSlot {
column_name: None,
inner: &TEXT_SLOT_INNER,
};
const BLOB_SLOT: Node = Node::TypedValueSlot {
ty: Type::Blob,
column_name: None,
inner: &TEXT_SLOT_INNER,
};
// shortid columns store base58 text (ADR-0011 fk_target_type
// shortid → text); the slot accepts a quoted-text literal or
// null.
@@ -297,7 +292,6 @@ pub const fn slot_for_type(ty: Type) -> Node {
Type::Text => TEXT_SLOT,
Type::Date => DATE_SLOT,
Type::DateTime => DATETIME_SLOT,
Type::Blob => BLOB_SLOT,
}
}
@@ -310,7 +304,7 @@ const fn slot_inner_for_type(ty: Type) -> &'static Node {
Type::Real => &REAL_SLOT_INNER,
Type::Decimal => &DECIMAL_SLOT_INNER,
Type::Bool => &BOOL_SLOT_INNER,
Type::Text | Type::Date | Type::DateTime | Type::Blob | Type::ShortId => &TEXT_SLOT_INNER,
Type::Text | Type::Date | Type::DateTime | Type::ShortId => &TEXT_SLOT_INNER,
}
}
+2 -3
View File
@@ -567,7 +567,7 @@ mod tests {
fn standard_sql_type_aliases() {
good("table t (a integer, b varchar, c boolean, d timestamp)");
good("table t (e bigint, f smallint, g char, h numeric)");
good("table t (i binary, j varbinary, k float)");
good("table t (k float)");
}
#[test]
@@ -734,7 +734,7 @@ mod builder_tests {
fn standard_sql_aliases_map_to_playground_types() {
let (_, cols, _, _) = sct(
"create table t (a bigint, b varchar, c boolean, d timestamp, \
e numeric, f float, g binary)",
e numeric, f float)",
);
assert_eq!(
cols,
@@ -745,7 +745,6 @@ mod builder_tests {
("d".to_string(), Type::DateTime),
("e".to_string(), Type::Decimal),
("f".to_string(), Type::Real),
("g".to_string(), Type::Blob),
]
);
}
+17 -31
View File
@@ -1,7 +1,8 @@
//! User-facing column types and their mapping to SQLite STRICT.
//!
//! Implements the full ten-type vocabulary committed to in
//! ADR-0005. Storage choices for the text-backed types
//! Implements the nine-type vocabulary committed to in ADR-0005
//! (ten as originally decided; `blob` dropped by Amendment 2).
//! Storage choices for the text-backed types
//! (`decimal`, `date`, `datetime`) preserve precision and ISO
//! readability; comparisons rely on lexicographic ordering or
//! explicit casts at query time, which is acceptable for a
@@ -27,8 +28,6 @@ pub enum Type {
Date,
/// ISO 8601 datetime stored as `YYYY-MM-DDTHH:MM:SS[.fff][Z]` (TEXT).
DateTime,
/// Arbitrary binary data.
Blob,
/// Auto-incrementing integer; intended as a default primary key.
Serial,
/// 1012 character base58 random identifier (no ambiguous chars).
@@ -47,7 +46,6 @@ impl Type {
Self::Bool => "bool",
Self::Date => "date",
Self::DateTime => "datetime",
Self::Blob => "blob",
Self::Serial => "serial",
Self::ShortId => "shortid",
}
@@ -62,7 +60,6 @@ impl Type {
Self::Text | Self::ShortId | Self::Decimal | Self::Date | Self::DateTime => "TEXT",
Self::Int | Self::Serial | Self::Bool => "INTEGER",
Self::Real => "REAL",
Self::Blob => "BLOB",
}
}
@@ -79,8 +76,8 @@ impl Type {
/// All types known in this iteration, in stable order.
/// Ordering groups numeric types together, then boolean,
/// then temporal, then binary, then identity-flavoured
/// auto-generated types.
/// then temporal, then identity-flavoured auto-generated
/// types.
#[must_use]
pub const fn all() -> &'static [Self] {
&[
@@ -91,15 +88,14 @@ impl Type {
Self::Bool,
Self::Date,
Self::DateTime,
Self::Blob,
Self::Serial,
Self::ShortId,
]
}
/// True for the numeric types — `int`, `real`, `decimal`,
/// `serial`. `bool` (stored 0/1) and the text- / blob-backed
/// types are not numeric. Used to flag a `LIKE` text-pattern
/// `serial`. `bool` (stored 0/1) and the text-backed types
/// are not numeric. Used to flag a `LIKE` text-pattern
/// match against a numeric column (ADR-0027, Amendment 1).
#[must_use]
pub const fn is_numeric(self) -> bool {
@@ -129,7 +125,7 @@ impl Type {
}
/// Resolve a type name from the **advanced-mode SQL** type slot
/// (ADR-0035 §3). Accepts the ten playground keywords *and* the
/// (ADR-0035 §3). Accepts the nine playground keywords *and* the
/// standard-SQL aliases mapped onto them. Case-insensitive;
/// internal whitespace is collapsed so `double precision` resolves
/// regardless of spacing. Returns `None` for an unrecognised name —
@@ -137,7 +133,7 @@ impl Type {
/// diagnostic.
///
/// Deliberately distinct from [`FromStr`](std::str::FromStr), which
/// is the *simple-mode* parser and accepts only the ten keywords
/// is the *simple-mode* parser and accepts only the nine keywords
/// (no aliases), so simple mode teaches the playground's own
/// vocabulary. A length/precision argument (`varchar(255)`) is
/// stripped by the grammar before the name reaches this resolver.
@@ -157,8 +153,7 @@ impl Type {
"timestamp" => Some(Self::DateTime),
"numeric" => Some(Self::Decimal),
"float" | "double precision" => Some(Self::Real),
"binary" | "varbinary" => Some(Self::Blob),
// Fall through to the canonical ten keywords.
// Fall through to the canonical nine keywords.
other => other.parse::<Self>().ok(),
}
}
@@ -270,20 +265,19 @@ mod tests {
Type::Bool,
Type::Date,
Type::DateTime,
Type::Blob,
] {
assert_eq!(ty.fk_target_type(), ty);
}
}
#[test]
fn all_ten_types_are_present_and_distinct() {
fn all_nine_types_are_present_and_distinct() {
let kws: Vec<&'static str> = Type::all().iter().map(|t| t.keyword()).collect();
assert_eq!(kws.len(), 10);
assert_eq!(kws.len(), 9);
let mut sorted = kws.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), 10, "keywords must be unique");
assert_eq!(sorted.len(), 9, "keywords must be unique");
}
#[test]
@@ -294,16 +288,10 @@ mod tests {
}
#[test]
fn blob_type_maps_to_blob_storage() {
assert_eq!(Type::Blob.sqlite_strict_type(), "BLOB");
}
#[test]
fn unknown_type_message_lists_all_ten() {
fn unknown_type_message_lists_all_nine() {
let err = "varchar".parse::<Type>().unwrap_err();
for kw in [
"text", "int", "real", "decimal", "bool", "date", "datetime", "blob", "serial",
"shortid",
"text", "int", "real", "decimal", "bool", "date", "datetime", "serial", "shortid",
] {
assert!(
err.expected.contains(kw),
@@ -314,12 +302,12 @@ mod tests {
}
// --- ADR-0035 §3: advanced-mode SQL type-name resolution ---
// `from_sql_name` accepts the ten playground keywords *and* the
// `from_sql_name` accepts the nine playground keywords *and* the
// standard-SQL aliases. Simple-mode `FromStr` is unchanged (it
// still rejects aliases — see `unknown_type_lists_expected_alternatives`).
#[test]
fn sql_resolver_accepts_the_ten_canonical_keywords() {
fn sql_resolver_accepts_the_nine_canonical_keywords() {
for &ty in Type::all() {
assert_eq!(Type::from_sql_name(ty.keyword()), Some(ty));
}
@@ -338,8 +326,6 @@ mod tests {
("numeric", Type::Decimal),
("float", Type::Real),
("double precision", Type::Real),
("binary", Type::Blob),
("varbinary", Type::Blob),
] {
assert_eq!(
Type::from_sql_name(alias),
-11
View File
@@ -101,10 +101,6 @@ impl Value {
Type::Bool => self.bind_bool(column),
Type::Date => self.bind_date(column),
Type::DateTime => self.bind_datetime(column),
Type::Blob => Err(ValueError::Format {
column: column.to_string(),
message: "literal `blob` values are not supported in DSL yet".to_string(),
}),
}
}
@@ -320,7 +316,6 @@ mod tests {
#[test]
fn null_binds_to_null_for_any_type() {
for ty in Type::all() {
// Skip blob — null still works there too.
assert_eq!(Value::Null.bind_for_column("c", *ty).unwrap(), Bound::Null);
}
}
@@ -429,10 +424,4 @@ mod tests {
let err = n("3..14").bind_for_column("c", Type::Decimal).unwrap_err();
assert!(matches!(err, ValueError::Format { .. }));
}
#[test]
fn blob_inserts_are_explicitly_unsupported_for_now() {
let err = t("0xdead").bind_for_column("c", Type::Blob).unwrap_err();
assert!(matches!(err, ValueError::Format { message, .. } if message.contains("blob")));
}
}
-2
View File
@@ -240,7 +240,6 @@ const fn catalog_key_for_value_type(ty: crate::dsl::types::Type) -> &'static str
Type::Text => "hint.value_slot_text",
Type::Date => "hint.value_slot_date",
Type::DateTime => "hint.value_slot_datetime",
Type::Blob => "hint.value_slot_blob",
Type::Serial => "hint.value_slot_serial",
Type::ShortId => "hint.value_slot_shortid",
}
@@ -4745,7 +4744,6 @@ mod tests {
(Type::Text, "hint.value_slot_text"),
(Type::Date, "hint.value_slot_date"),
(Type::DateTime, "hint.value_slot_datetime"),
(Type::Blob, "hint.value_slot_blob"),
] {
let schema = schema_with("T", &[("c", ty)]);
let mode = hint_mode_at_input_with_schema("insert into T values (", &schema);