From 58386d77e92ca6ff769bb5784353d7814223f649 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Mon, 25 May 2026 07:55:26 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20ADR-0035=204a=20=E2=80=94=20SQL=20type-?= =?UTF-8?q?alias=20resolver=20(Type::from=5Fsql=5Fname)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/dsl/types.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/dsl/types.rs b/src/dsl/types.rs index ebe3914..9ed4ed2 100644 --- a/src/dsl/types.rs +++ b/src/dsl/types.rs @@ -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 { + // Collapse internal whitespace (for the two-word + // `double precision`) and lowercase for case-insensitive match. + let normalised = name + .split_whitespace() + .collect::>() + .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::().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); + } }