feat: ADR-0035 4a — SQL type-alias resolver (Type::from_sql_name)

Advanced-mode SQL type slot accepts the ten playground keywords plus the
standard-SQL aliases (integer/varchar/timestamp/numeric/float/double
precision/binary/..., case-insensitive). Simple-mode FromStr is unchanged
(rejects aliases). Unknown names -> None for the friendly diagnostic.
This commit is contained in:
claude@clouddev1
2026-05-25 07:55:26 +00:00
parent 94ec87b2ff
commit 58386d77e9
+95
View File
@@ -134,6 +134,41 @@ impl Type {
other => other,
}
}
/// Resolve a type name from the **advanced-mode SQL** type slot
/// (ADR-0035 §3). Accepts the ten 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 —
/// the caller turns that into the friendly "unknown type"
/// diagnostic.
///
/// Deliberately distinct from [`FromStr`](std::str::FromStr), which
/// is the *simple-mode* parser and accepts only the ten 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.
#[must_use]
pub fn from_sql_name(name: &str) -> Option<Self> {
// Collapse internal whitespace (for the two-word
// `double precision`) and lowercase for case-insensitive match.
let normalised = name
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase();
match normalised.as_str() {
"integer" | "smallint" | "bigint" => Some(Self::Int),
"varchar" | "char" => Some(Self::Text),
"boolean" => Some(Self::Bool),
"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.
other => other.parse::<Self>().ok(),
}
}
}
impl fmt::Display for Type {
@@ -284,4 +319,64 @@ mod tests {
);
}
}
// --- ADR-0035 §3: advanced-mode SQL type-name resolution ---
// `from_sql_name` accepts the ten 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() {
for &ty in Type::all() {
assert_eq!(Type::from_sql_name(ty.keyword()), Some(ty));
}
}
#[test]
fn sql_resolver_maps_standard_sql_aliases() {
for (alias, expected) in [
("integer", Type::Int),
("smallint", Type::Int),
("bigint", Type::Int),
("varchar", Type::Text),
("char", Type::Text),
("boolean", Type::Bool),
("timestamp", Type::DateTime),
("numeric", Type::Decimal),
("float", Type::Real),
("double precision", Type::Real),
("binary", Type::Blob),
("varbinary", Type::Blob),
] {
assert_eq!(
Type::from_sql_name(alias),
Some(expected),
"alias `{alias}` should map to {expected:?}"
);
}
}
#[test]
fn sql_resolver_is_case_insensitive() {
assert_eq!(Type::from_sql_name("INTEGER"), Some(Type::Int));
assert_eq!(Type::from_sql_name("VarChar"), Some(Type::Text));
assert_eq!(Type::from_sql_name("Double Precision"), Some(Type::Real));
assert_eq!(Type::from_sql_name("TEXT"), Some(Type::Text));
}
#[test]
fn sql_resolver_rejects_genuinely_unknown_names() {
assert_eq!(Type::from_sql_name("money"), None);
assert_eq!(Type::from_sql_name("json"), None);
assert_eq!(Type::from_sql_name(""), None);
}
#[test]
fn sql_resolver_does_not_accept_serial_aliases() {
// `serial`/`shortid` are playground-only; there is no standard
// alias for them, and INTEGER PRIMARY KEY does NOT become serial
// (ADR-0035 §3 — that is enforced at the PK layer, not here).
assert_eq!(Type::from_sql_name("serial"), Some(Type::Serial));
assert_eq!(Type::from_sql_name("autoincrement"), None);
}
}