Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/sqlbuild-analysis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ sqlclean.workspace = true
sqlquack-syntax.workspace = true
sqlquack-syntax-bigquery.workspace = true
sqlquack-syntax-postgres.workspace = true
stacker.workspace = true

[lints]
workspace = true
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,11 @@ fn given_models_sharing_types_when_evaluating_together_then_each_matches_its_own
}

#[test]
fn given_a_dialect_polyglot_does_not_know_when_comparing_types_then_python_error_is_returned() {
fn given_a_dialect_no_adapter_ships_when_comparing_types_then_python_error_is_returned() {
let test_cases = [UnknownDialectTestCase {
description: "an enforced contract compared under an unknown dialect",
dialect: "motherduck",
expected_error: "Unknown dialect: motherduck",
dialect: "mysql",
expected_error: "Unknown dialect: mysql",
}];
for test_case in test_cases {
let request = ContractRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use crate::planner_strategy::_helpers::type_names::{argument, named_type};
use crate::planner_strategy::constants::{GENERIC_DIALECT, POLYGLOT_CUSTOM_TYPE_NAME};
use crate::planner_strategy::errors::TypeParseFailure;
use crate::planner_strategy::models::CursorType;
use crate::type_system::main::polyglot_type_args::polyglot_type_args;
use crate::type_system::main::type_args::type_args;

/// Timestamp or integer for supported cross-adapter types; `Err` when Polyglot cannot parse an unknown type.
/// Timestamp or integer for supported cross-adapter types; `Err` when an unknown type does not parse.
pub fn classify_cursor_sql_type(sql_type: &str) -> Result<Option<CursorType>, TypeParseFailure> {
let normalized: String = sql_type
.to_uppercase()
Expand All @@ -29,7 +29,7 @@ pub fn classify_cursor_sql_type(sql_type: &str) -> Result<Option<CursorType>, Ty
return Ok(Some(cursor_type));
}
let args: Map<String, Value> =
polyglot_type_args(&normalized, GENERIC_DIALECT).map_err(|message| TypeParseFailure {
type_args(&normalized, GENERIC_DIALECT).map_err(|message| TypeParseFailure {
normalized_type: normalized.clone(),
message,
})?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ fn given_project_metadata_when_checking_then_reports_python_errors() {
expected_outcome: Ok(expected_checked_outcome()),
},
MetadataTestCase {
description: "a dialect the all-dialects parser build added",
dialect: "mysql",
description: "a shipped dialect without a native type parser",
dialect: "tsql",
mart_shape: MART_SHAPE,
expected_outcome: Ok(expected_checked_outcome()),
},
Expand Down
3 changes: 2 additions & 1 deletion crates/sqlbuild-analysis/src/type_system/_helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ pub(crate) mod dialects;
pub(crate) mod fallback;
pub(crate) mod parsed;
pub(crate) mod python_text;
pub(crate) mod shallow_args;
pub(crate) mod spelling;
pub(crate) mod type_reader;
233 changes: 122 additions & 111 deletions crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs
Original file line number Diff line number Diff line change
@@ -1,97 +1,121 @@
//! Python's `_normalize_with_polyglot`: the parsed type's SQL and arguments.
//! A parsed type's normalization: its tag, parameters and dialect spelling.

use polyglot_sql::{
ComplexityGuardOptions, DataType, Dialect, DialectType, Expression, ParseOptions,
};
use serde_json::{Map, Value};
use serde_json::Value;
use sqlquack_syntax::tokens::models::Dialect;

use crate::type_system::_helpers::fallback::{
normalize_with_fallback, python_or_default, simple, snowflake_integer,
timestamp_normalized_name,
};
use crate::type_system::_helpers::python_text::{python_int, split_type_and_params, upper};
use crate::type_system::_helpers::shallow_args::shallow_args;
use crate::type_system::_helpers::spelling::{SpellingDialect, spell};
use crate::type_system::_helpers::type_reader::read_type;
use crate::type_system::constants::{
BIGNUMERIC_TYPE_NAME, BIGQUERY_STRING_TYPE_NAME, BOOLEAN_TYPE_NAMES,
CUSTOM_NORMALIZATION_TYPE_NAMES, DATE_TYPE_NAME, DATETIME_TYPE_NAME, DECIMAL_TYPE_NAMES,
FLOAT_TYPE_NAMES, FLOAT_WIRE_TYPE_NAME, INTEGER_PARSE_TYPE_NAMES, INTEGER_PRECISION,
INTEGER_SCALE, INTEGER_TYPE_NAMES, MAX_FUNCTION_CALL_DEPTH, POLYGLOT_CUSTOM_TYPE_NAME,
POLYGLOT_TYPE_NAME_ALIASES, SIZED_TEXT_TYPE_NAMES, STRING_TYPE_NAMES, TIMESTAMP_TYPE_NAMES,
UNBOUNDED_TEXT_TYPE_NAMES, VARCHAR_TYPE_NAME,
CUSTOM_NORMALIZATION_TYPE_NAMES, CUSTOM_TYPE_TAG, DATE_TYPE_NAME, DATETIME_TYPE_NAME,
DECIMAL_TYPE_NAMES, FLOAT_TYPE_NAMES, FLOAT_WIRE_TYPE_NAME, INTEGER_PARSE_TYPE_NAMES,
INTEGER_PRECISION, INTEGER_SCALE, INTEGER_TYPE_NAMES, MAX_TYPE_NESTING, SIZED_TEXT_TYPE_NAMES,
STRING_TYPE_NAMES, TIMESTAMP_TYPE_NAMES, TYPE_TAG_ALIASES, UNBOUNDED_TEXT_TYPE_NAMES,
VARCHAR_TYPE_NAME,
};
use crate::type_system::errors::TypeNormalizationError;
use crate::type_system::models::{NormalizedType, PythonInteger, TypeDialect, TypeFamily};
use crate::warehouse_types::main::type_tree::type_tree;
use crate::warehouse_types::models::TypeParser;

/// Parse one type with SQLBuild's Polyglot guard; the error text is what Python logs.
pub(crate) fn parse_type(type_sql: &str, dialect: &Dialect) -> Result<DataType, String> {
dialect
.parse_data_type_with_options(
type_sql,
&ParseOptions {
complexity_guard: Some(ComplexityGuardOptions {
max_function_call_depth: Some(MAX_FUNCTION_CALL_DEPTH),
..Default::default()
}),
},
)
.map_err(|error| error.to_string())
/// The type tree of `type_sql` from SQLQuack's parser, else SQLBuild's reader, or the error.
pub(crate) fn parse_type(type_sql: &str, dialect: SpellingDialect) -> Result<Value, String> {
if nesting_bound(type_sql) > MAX_TYPE_NESTING {
return Err("the type nests too deeply".to_owned());
}
let parser: TypeParser = match dialect {
SpellingDialect::Snowflake => TypeParser::Core(Dialect::Snowflake),
SpellingDialect::DuckDb => TypeParser::Core(Dialect::DuckDB),
SpellingDialect::Sqlite => TypeParser::Core(Dialect::SQLite),
SpellingDialect::Postgres => TypeParser::Postgres,
SpellingDialect::BigQuery => TypeParser::BigQuery,
SpellingDialect::Generic
| SpellingDialect::Databricks
| SpellingDialect::Tsql
| SpellingDialect::ClickHouse => return read_type(type_sql, dialect),
};
let native: Result<Value, String> = type_tree(type_sql, parser);
match native {
Ok(tree) if !has_custom(&tree) => Ok(tree),
Ok(tree) => Ok(read_type(type_sql, dialect).unwrap_or(tree)),
Err(error) => read_type(type_sql, dialect).map_err(|_| error),
}
}

/// The parsed type's SQL, in generic spelling when the dialect refuses a type it cannot hold.
fn generated_type(
expression: &Expression,
polyglot: &Dialect,
) -> Result<String, TypeNormalizationError> {
match polyglot.generate(expression) {
Ok(generated) => Ok(generated),
Err(polyglot_sql::error::Error::Unsupported { .. }) => Dialect::get(DialectType::Generic)
.generate(expression)
.map_err(|error| TypeNormalizationError::Generation(error.to_string())),
Err(error) => Err(TypeNormalizationError::Generation(error.to_string())),
/// A bound on the type tree's depth: the deepest brackets plus the array suffixes.
pub(crate) fn nesting_bound(type_sql: &str) -> usize {
let (mut depth, mut deepest, mut suffixes): (usize, usize, usize) = (0, 0, 0);
for character in type_sql.chars() {
match character {
'(' | '<' => {
depth += 1;
deepest = deepest.max(depth);
}
')' | '>' => depth = depth.saturating_sub(1),
'[' => suffixes += 1,
_ => {}
}
}
deepest + suffixes
}

/// Whether `tree` or any type nested in it is an unknown type name.
fn has_custom(tree: &Value) -> bool {
match tree {
Value::Object(fields) => {
fields
.get("data_type")
.and_then(Value::as_str)
.is_some_and(|tag| tag.eq_ignore_ascii_case(CUSTOM_TYPE_TAG))
|| fields.values().any(has_custom)
}
Value::Array(items) => items.iter().any(has_custom),
_ => false,
}
}

/// Python's `_normalized_from_parsed_type`.
/// The normalization of a parsed type.
pub(crate) fn normalize_parsed(
parsed: DataType,
polyglot: &Dialect,
tree: &Value,
spelling: SpellingDialect,
dialect: Option<TypeDialect>,
) -> Result<NormalizedType, TypeNormalizationError> {
let args: Map<String, Value> = shallow_args(&parsed);
let expression: Expression = Expression::DataType(parsed);
let generated: String = generated_type(&expression, polyglot)?;
let normalized_name: String = upper(&generated).replace(' ', "");
let dtype_name: String = polyglot_type_name(&args);
let params: Vec<PythonInteger> = polyglot_type_params(&args);
) -> NormalizedType {
let normalized_name: String = upper(&spell(tree, spelling)).replace(' ', "");
let dtype_name: String = type_name(tree);
let params: Vec<PythonInteger> = type_params(tree);
let snowflake: bool = dialect == Some(TypeDialect::Snowflake);
let bigquery: bool = dialect == Some(TypeDialect::BigQuery);
let dtype: &str = dtype_name.as_str();

if bigquery && INTEGER_PARSE_TYPE_NAMES.contains(&dtype) {
return Ok(simple("INT64", TypeFamily::Integer));
return simple("INT64", TypeFamily::Integer);
}
if bigquery && dtype == BIGNUMERIC_TYPE_NAME {
return Ok(simple("BIGNUMERIC", TypeFamily::Decimal));
return simple("BIGNUMERIC", TypeFamily::Decimal);
}
if bigquery && dtype == FLOAT_WIRE_TYPE_NAME {
return Ok(simple("FLOAT64", TypeFamily::Float));
return simple("FLOAT64", TypeFamily::Float);
}
if dtype == POLYGLOT_CUSTOM_TYPE_NAME {
let raw_name: String = upper(&match args.get("name") {
Some(value) => python_str(value),
if dtype == CUSTOM_TYPE_TAG {
let raw_name: String = upper(&match tree.get("name").and_then(Value::as_str) {
Some(name) => name.to_owned(),
None => normalized_name.clone(),
})
.replace(' ', "");
if snowflake && raw_name.starts_with("NUMBER") {
return Ok(snowflake_number(&raw_name));
return snowflake_number(&raw_name);
}
return Ok(normalize_with_fallback(&raw_name, dialect));
return normalize_with_fallback(&raw_name, dialect);
}
if INTEGER_TYPE_NAMES.contains(&dtype) {
if snowflake {
return Ok(snowflake_integer());
return snowflake_integer();
}
return Ok(simple(&normalized_name, TypeFamily::Integer));
return simple(&normalized_name, TypeFamily::Integer);
}
if DECIMAL_TYPE_NAMES.contains(&dtype) {
let mut precision: Option<PythonInteger> = params.first().cloned();
Expand All @@ -102,16 +126,16 @@ pub(crate) fn normalize_parsed(
scale = Some(INTEGER_SCALE.into());
name = format!("DECIMAL({INTEGER_PRECISION},{INTEGER_SCALE})");
}
return Ok(NormalizedType {
return NormalizedType {
normalized_name: name,
family: TypeFamily::Decimal,
precision,
scale,
length: None,
});
};
}
if FLOAT_TYPE_NAMES.contains(&dtype) {
return Ok(simple(&normalized_name, TypeFamily::Float));
return simple(&normalized_name, TypeFamily::Float);
}
if STRING_TYPE_NAMES.contains(&dtype) {
let mut length: Option<PythonInteger> = params.first().cloned();
Expand All @@ -126,30 +150,44 @@ pub(crate) fn normalize_parsed(
} else if length.is_some() && SIZED_TEXT_TYPE_NAMES.contains(&base_name) {
name = name.replacen(base_name, VARCHAR_TYPE_NAME, 1);
}
return Ok(NormalizedType {
return NormalizedType {
normalized_name: name,
family: TypeFamily::String,
precision: None,
scale: None,
length,
});
};
}
if BOOLEAN_TYPE_NAMES.contains(&dtype) {
return Ok(simple(&normalized_name, TypeFamily::Boolean));
return simple(&normalized_name, TypeFamily::Boolean);
}
if TIMESTAMP_TYPE_NAMES.contains(&dtype) {
return Ok(simple(
if bigquery && normalized_name == DATETIME_TYPE_NAME {
return simple(&normalized_name, TypeFamily::Datetime);
}
return simple(
&timestamp_normalized_name(&normalized_name, dialect),
TypeFamily::Timestamp,
));
);
}
if dtype == DATE_TYPE_NAME {
return Ok(simple(&normalized_name, TypeFamily::Date));
return simple(&normalized_name, TypeFamily::Date);
}
if dtype == DATETIME_TYPE_NAME {
return Ok(simple(&normalized_name, TypeFamily::Datetime));
return simple(&normalized_name, TypeFamily::Datetime);
}
Ok(simple(&normalized_name, TypeFamily::Other))
simple(&normalized_name, TypeFamily::Other)
}

/// The scalar parameters of a type tree, keyed as the tree names them.
pub(crate) fn type_args(tree: &Value) -> serde_json::Map<String, Value> {
let mut args: serde_json::Map<String, Value> = serde_json::Map::new();
for (key, value) in tree.as_object().into_iter().flatten() {
if !value.is_array() && !value.is_object() {
args.insert(key.clone(), value.clone());
}
}
args
}

/// Snowflake's `NUMBER(p,s)` spelled as a custom type: Python's `DECIMAL` rewrite.
Expand All @@ -172,57 +210,30 @@ fn snowflake_number(raw_name: &str) -> NormalizedType {
}
}

/// Python's `_polyglot_type_name`.
fn polyglot_type_name(args: &Map<String, Value>) -> String {
let data_type: String = upper(&match args.get("data_type") {
Some(value) => python_str(value),
None => String::new(),
});
if let Some((_, alias)) = POLYGLOT_TYPE_NAME_ALIASES
.iter()
.find(|(name, _)| *name == data_type)
{
/// The type's tag, upper-cased with its word separators as the type names spell them.
fn type_name(tree: &Value) -> String {
let tag: String = upper(tree.get("data_type").and_then(Value::as_str).unwrap_or(""));
if let Some((_, alias)) = TYPE_TAG_ALIASES.iter().find(|(name, _)| *name == tag) {
return (*alias).to_owned();
}
if data_type == POLYGLOT_CUSTOM_TYPE_NAME {
let name: String = upper(&match args.get("name") {
Some(value) => python_str(value),
None => String::new(),
})
.replace(' ', "");
if tag == CUSTOM_TYPE_TAG {
let name: String =
upper(tree.get("name").and_then(Value::as_str).unwrap_or("")).replace(' ', "");
if CUSTOM_NORMALIZATION_TYPE_NAMES.contains(&name.as_str()) {
return name;
}
}
data_type
tag
}

/// Python's `_polyglot_type_params`: `int(value)` of each parameter, skipping what it rejects.
fn polyglot_type_params(args: &Map<String, Value>) -> Vec<PythonInteger> {
let mut params: Vec<PythonInteger> = Vec::new();
for key in ["precision", "scale", "length"] {
let Some(value) = args.get(key) else {
continue;
};
let param: Option<PythonInteger> = match value {
Value::Null | Value::Array(_) | Value::Object(_) => None,
Value::Bool(flag) => Some(i64::from(*flag).into()),
/// The type's precision, scale and length, in that order, where it has them.
fn type_params(tree: &Value) -> Vec<PythonInteger> {
["precision", "scale", "length"]
.iter()
.filter_map(|key| tree.get(*key))
.filter_map(|value| match value {
Value::Number(number) => python_int(&number.to_string()),
Value::String(text) => python_int(text),
};
params.extend(param);
}
params
}

/// Python's `str(value)` for the scalar values Polyglot gives a parsed type's tag and name.
fn python_str(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
Value::Null => "None".to_owned(),
Value::Bool(true) => "True".to_owned(),
Value::Bool(false) => "False".to_owned(),
Value::Number(number) => number.to_string(),
Value::Array(_) | Value::Object(_) => value.to_string(),
}
_ => None,
})
.collect()
}
Loading
Loading