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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
/// 10–12 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),
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user