Skip to content
Merged
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
12 changes: 6 additions & 6 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exclude = [
]

[workspace.package]
version = "0.13.3"
version = "0.13.4"
edition = "2021"
license = "MIT"
authors = ["polyglot contributors"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ If you want to disable `stacker` for a native Rust build, turn off default featu

```toml
[dependencies]
polyglot-sql = { version = "0.13.3", default-features = false, features = ["all-dialects", "transpile"] }
polyglot-sql = { version = "0.13.4", default-features = false, features = ["all-dialects", "transpile"] }
```

That can reduce overhead slightly on trusted inputs, but you lose the default stack-growth protection for deeply nested SQL.
Expand Down
2 changes: 1 addition & 1 deletion crates/polyglot-sql-function-catalogs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ With `polyglot-sql` compile-time wiring (recommended):

```toml
[dependencies]
polyglot-sql = { version = "0.13.3", features = ["function-catalog-clickhouse"] }
polyglot-sql = { version = "0.13.4", features = ["function-catalog-clickhouse"] }
```

Then run schema validation with type checks:
Expand Down
11 changes: 11 additions & 0 deletions crates/polyglot-sql-function-catalogs/src/duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1648,4 +1648,15 @@ pub(crate) fn register<S: CatalogSink>(catalog: &mut S) {
catalog.register(d, "~~", vec![FunctionSignature::exact(2)]);
catalog.register(d, "~~*", vec![FunctionSignature::exact(2)]);
catalog.register(d, "~~~", vec![FunctionSignature::exact(2)]);

// SQL keyword forms rewritten by DuckDB's parser rather than listed by
// duckdb_functions(); they are valid without brackets.
for name in [
"current_time",
"current_timestamp",
"localtime",
"localtimestamp",
] {
catalog.register(d, name, vec![FunctionSignature::exact(0)]);
}
}
4 changes: 2 additions & 2 deletions crates/polyglot-sql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@ function-catalog-all-dialects = [

[dependencies]
regex = { version = "1", optional = true }
polyglot-sql-ast-derive = { path = "../polyglot-sql-ast-derive", version = "0.13.3" }
polyglot-sql-ast-derive = { path = "../polyglot-sql-ast-derive", version = "0.13.4" }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
unicode-segmentation = { workspace = true }
stacker = { version = "0.1", optional = true }
ts-rs = { version = "12.0", features = ["serde-compat"], optional = true }
polyglot-sql-function-catalogs = { path = "../polyglot-sql-function-catalogs", version = "0.13.3", optional = true, default-features = false }
polyglot-sql-function-catalogs = { path = "../polyglot-sql-function-catalogs", version = "0.13.4", optional = true, default-features = false }

[dev-dependencies]
pretty_assertions = "1.4"
Expand Down
8 changes: 4 additions & 4 deletions crates/polyglot-sql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ By default, `polyglot-sql` enables the full public API. Parser-only consumers ca
disable default features and opt into only the dialect parsers they need:

```toml
polyglot-sql = { version = "0.13.3", default-features = false }
polyglot-sql = { version = "0.13.4", default-features = false }
```

```toml
polyglot-sql = {
version = "0.13.3",
version = "0.13.4",
default-features = false,
features = ["dialect-clickhouse"],
}
Expand All @@ -44,14 +44,14 @@ Examples:
```toml
# Parse and generate SQL for one dialect.
polyglot-sql = {
version = "0.13.3",
version = "0.13.4",
default-features = false,
features = ["generate", "dialect-clickhouse"],
}

# Cross-dialect transpilation.
polyglot-sql = {
version = "0.13.3",
version = "0.13.4",
default-features = false,
features = ["transpile", "dialect-clickhouse", "dialect-postgresql"],
}
Expand Down
17 changes: 16 additions & 1 deletion crates/polyglot-sql/src/function_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub(crate) const NO_PAREN_FUNCTION_NAME_LIST: &[&str] = &[
"UTC_TIMESTAMP",
"SESSION_USER",
"SYSTEM_USER",
// Note: USER by itself is NOT a no-paren function in standard SQL - only CURRENT_USER is
// Bare USER is dialect-specific; see `bare_user_is_function`.
"PI",
// MySQL/Databricks CURDATE
"CURDATE",
Expand Down Expand Up @@ -2224,6 +2224,21 @@ pub(crate) fn is_no_paren_function_name_upper(upper_name: &str) -> bool {
NO_PAREN_FUNCTION_NAME_SET.contains(upper_name)
}

/// Whether an unquoted, unqualified `USER` is the niladic session-user function.
/// Other dialects either require `USER()` or treat `user` as an ordinary name.
pub(crate) fn bare_user_is_function(dialect: Option<DialectType>) -> bool {
matches!(
dialect,
Some(
DialectType::PostgreSQL
| DialectType::Redshift
| DialectType::DuckDB
| DialectType::TSQL
| DialectType::Oracle
)
)
}

/// Returns true if the given uppercased name should use aggregate parsing behavior.
pub(crate) fn is_aggregate_function_name_upper(upper_name: &str) -> bool {
AGGREGATE_FUNCTION_NAME_SET.contains(upper_name)
Expand Down
80 changes: 65 additions & 15 deletions crates/polyglot-sql/src/optimizer/annotate_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use crate::expressions::{
use crate::schema::{normalize_name, Schema, SchemaError, SchemaResult, TABLE_PARTS};
use crate::traversal::ExpressionWalk;

mod engine;

/// Type coercion class for determining result types in binary operations.
/// Higher-priority classes win during coercion.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
Expand Down Expand Up @@ -403,6 +405,15 @@ impl<'a> TypeAnnotator<'a> {

/// Annotate types for an expression tree
pub fn annotate(&mut self, expr: &Expression) -> Option<DataType> {
if let Some(engine) = engine::Engine::of(self._dialect) {
if let Some(result) = self.annotate_engine(engine, expr) {
return result;
}
}
self.annotate_default(expr)
}

fn annotate_default(&mut self, expr: &Expression) -> Option<DataType> {
if self._dialect == Some(DialectType::Snowflake) {
match expr {
Expression::CurrentTimestamp(_) | Expression::CurrentTimestampLTZ(_) => {
Expand Down Expand Up @@ -1345,6 +1356,17 @@ impl<'a> TypeAnnotator<'a> {
self.annotate(&lv.this)
}

/// Annotate a literal as bound by `dialect`.
pub(super) fn annotate_literal_for(
lit: &Literal,
dialect: Option<DialectType>,
) -> Option<DataType> {
match engine::Engine::of(dialect) {
Some(engine) => engine::literal_type(engine, lit),
None => Self::annotate_literal(lit),
}
}

/// Annotate a literal value
pub(super) fn annotate_literal(lit: &Literal) -> Option<DataType> {
match lit {
Expand Down Expand Up @@ -1446,6 +1468,11 @@ impl<'a> TypeAnnotator<'a> {
{
return func.args.first().and_then(|arg| self.annotate(arg));
}
if let Some(engine) = engine::Engine::of(self._dialect) {
if let Some(result) = self.annotate_engine_function(engine, func) {
return result;
}
}
let func_name = func.name.to_uppercase();

if self._dialect == Some(DialectType::Snowflake) && !func.quoted {
Expand Down Expand Up @@ -1589,7 +1616,10 @@ impl<'a> TypeAnnotator<'a> {
};
}
self.used_unknown_function_fallback = true;
if self._dialect == Some(DialectType::Snowflake) {
if engine::Engine::of(self._dialect).is_some() {
// An unmodelled function's result is not its first argument's type.
None
} else if self._dialect == Some(DialectType::Snowflake) {
Some(DataType::Unknown)
} else {
func.args.first().and_then(|arg| self.annotate(arg))
Expand Down Expand Up @@ -2410,9 +2440,9 @@ fn projection_name(expression: &Expression) -> Option<String> {
}
}

fn projection_type(expression: &Expression) -> DataType {
fn projection_type(expression: &Expression, dialect: Option<DialectType>) -> DataType {
if let Expression::Literal(literal) = expression {
return TypeAnnotator::annotate_literal(literal).unwrap_or(DataType::Unknown);
return TypeAnnotator::annotate_literal_for(literal, dialect).unwrap_or(DataType::Unknown);
}
if matches!(expression, Expression::Boolean(_)) {
return DataType::Boolean;
Expand Down Expand Up @@ -2449,7 +2479,7 @@ fn query_outputs(expressions: &[Expression], dialect: Option<DialectType>) -> Ou
if unknown_function {
DataType::Unknown
} else {
projection_type(expression)
projection_type(expression, dialect)
},
)
})
Expand Down Expand Up @@ -3596,10 +3626,18 @@ mod tests {
TypeAnnotator::new(None, Some(DialectType::DuckDB)).annotate(&expr),
None
);
// PostgreSQL widens SUM(BIGINT) to NUMERIC.
let expr = parse_one("SUM(CAST(1 AS BIGINT))", DialectType::PostgreSQL).unwrap();
assert_eq!(
TypeAnnotator::new(None, Some(DialectType::PostgreSQL)).annotate(&expr),
Some(DataType::Decimal {
precision: None,
scale: None
})
);
// The dialect-specific override must not change the generic/default rule.
for dialect in [
DialectType::Generic,
DialectType::PostgreSQL,
DialectType::MySQL,
DialectType::BigQuery,
] {
Expand Down Expand Up @@ -3687,14 +3725,20 @@ mod tests {
let mut restored: Expression = serde_json::from_value(unannotated).unwrap();
assert_eq!(restored, expression);
annotate_types(&mut restored, None, Some(dialect));
assert_eq!(restored.inferred_type(), Some(&varchar), "{dialect:?}");
// PostgreSQL string functions return TEXT.
let (expected, tag) = if dialect == DialectType::PostgreSQL {
(DataType::Text, "text")
} else {
(varchar.clone(), "var_char")
};
assert_eq!(restored.inferred_type(), Some(&expected), "{dialect:?}");
assert_eq!(restored.sql_for(dialect), expression.sql_for(dialect));

let annotated = serde_json::to_value(&restored).unwrap();
assert_eq!(annotated["trim"]["inferred_type"]["data_type"], "var_char");
assert_eq!(annotated["trim"]["inferred_type"]["data_type"], tag);
let roundtripped: Expression = serde_json::from_value(annotated).unwrap();
assert_eq!(roundtripped, restored);
assert_eq!(roundtripped.inferred_type(), Some(&varchar));
assert_eq!(roundtripped.inferred_type(), Some(&expected));
}
}

Expand Down Expand Up @@ -4062,10 +4106,15 @@ mod tests {
Some(DialectType::Snowflake),
Some(DialectType::DuckDB),
] {
let expected = if dialect == Some(DialectType::DuckDB) {
duckdb_type
} else {
&int
let numeric = DataType::Decimal {
precision: None,
scale: None,
};
let expected = match dialect {
Some(DialectType::DuckDB) => duckdb_type,
// PostgreSQL 14+ EXTRACT returns NUMERIC for every field.
Some(DialectType::PostgreSQL) => &numeric,
_ => &int,
};
assert_eq!(
TypeAnnotator::new(None, dialect)
Expand Down Expand Up @@ -4585,12 +4634,13 @@ mod tests {
let Expression::Alias(alias) = &select.expressions[0] else {
panic!("expected alias");
};
// DuckDB binds 2.5 as DECIMAL(2,1); INTEGER widens to DECIMAL(11,1).
assert_eq!(
alias.this.inferred_type(),
Some(&DataType::Array {
element_type: Box::new(DataType::Double {
precision: None,
scale: None,
element_type: Box::new(DataType::Decimal {
precision: Some(11),
scale: Some(1),
}),
dimension: None,
})
Expand Down
Loading
Loading