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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ test-rust-feature-gates:
cargo check -p polyglot-sql --no-default-features --features transpile,dialect-clickhouse,dialect-postgresql
cargo check -p polyglot-sql --no-default-features --features transpile,dialect-tsql
cargo check -p polyglot-sql --no-default-features --features transpile,dialect-fabric
cargo check -p polyglot-sql --no-default-features --features transpile,dialect-vertica
cargo check -p polyglot-sql --no-default-features --features dialect-snowflake
cargo check -p polyglot-sql --no-default-features --features generate,dialect-snowflake
cargo check -p polyglot-sql --no-default-features --features transpile,dialect-snowflake
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Release notes are tracked in [`CHANGELOG.md`](CHANGELOG.md).
| MySQL | Oracle | PostgreSQL | Presto | Redshift |
| RisingWave | SingleStore | Snowflake | Solr | Spark |
| SQLite | StarRocks | Tableau | Teradata | TiDB |
| Trino | TSQL | DataFusion | SAP HANA | Generic SQL |
| Trino | TSQL | Vertica | DataFusion | SAP HANA |
| Generic SQL | | | | |

## Quick Start

Expand Down
3 changes: 2 additions & 1 deletion crates/polyglot-sql-ffi/src/dialects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use polyglot_sql::dialects::DialectType;
use std::os::raw::c_char;
use std::ptr;

const DIALECTS: [DialectType; 35] = [
const DIALECTS: &[DialectType] = &[
DialectType::Generic,
DialectType::PostgreSQL,
DialectType::MySQL,
Expand Down Expand Up @@ -39,6 +39,7 @@ const DIALECTS: [DialectType; 35] = [
DialectType::Exasol,
DialectType::DataFusion,
DialectType::HANA,
DialectType::Vertica,
];

/// Return supported dialect names as JSON.
Expand Down
2 changes: 1 addition & 1 deletion crates/polyglot-sql-ffi/tests/ffi_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2050,7 +2050,7 @@ fn test_dialect_list_and_count() {
let list: Vec<String> = serde_json::from_str(&json).expect("invalid dialect list json");
let count = polyglot_dialect_count();
assert_eq!(list.len() as i32, count);
assert_eq!(count, 35);
assert_eq!(count, 36);
let unique: BTreeSet<&str> = list.iter().map(String::as_str).collect();
assert_eq!(unique.len(), list.len());
assert!(list.iter().any(|d| d == "generic"));
Expand Down
2 changes: 1 addition & 1 deletion crates/polyglot-sql-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ All functions are exported from `polyglot_sql`.

Current dialect names returned by `polyglot_sql.dialects()`:

`athena`, `bigquery`, `clickhouse`, `cockroachdb`, `datafusion`, `databricks`, `doris`, `dremio`, `drill`, `druid`, `duckdb`, `dune`, `exasol`, `fabric`, `generic`, `hana`, `hive`, `materialize`, `mysql`, `oracle`, `postgres`, `presto`, `redshift`, `risingwave`, `singlestore`, `snowflake`, `solr`, `spark`, `sqlite`, `starrocks`, `tableau`, `teradata`, `tidb`, `trino`, `tsql`.
`athena`, `bigquery`, `clickhouse`, `cockroachdb`, `datafusion`, `databricks`, `doris`, `dremio`, `drill`, `druid`, `duckdb`, `dune`, `exasol`, `fabric`, `generic`, `hana`, `hive`, `materialize`, `mysql`, `oracle`, `postgres`, `presto`, `redshift`, `risingwave`, `singlestore`, `snowflake`, `solr`, `spark`, `sqlite`, `starrocks`, `tableau`, `teradata`, `tidb`, `trino`, `tsql`, `vertica`.

## Error Handling

Expand Down
2 changes: 2 additions & 0 deletions crates/polyglot-sql-python/python/polyglot_sql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,7 @@ def dense_rank():
Variance,
VectorSearch,
Version,
Vertica,
ViewAttributeProperty,
VolatileProperty,
WatermarkColumnConstraint,
Expand Down Expand Up @@ -1301,6 +1302,7 @@ def dense_rank():
"Select",
"Column",
"Table",
"Vertica",
"Literal",
"Identifier",
"Star",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,7 @@ class Variadic(Expression): ...
class Variance(Expression): ...
class VectorSearch(Expression): ...
class Version(Expression): ...
class Vertica(Expression): ...
class ViewAttributeProperty(Expression): ...
class VolatileProperty(Expression): ...
class WatermarkColumnConstraint(Expression): ...
Expand Down
1 change: 1 addition & 0 deletions crates/polyglot-sql-python/src/dialects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const DIALECT_NAMES: &[&str] = &[
"tidb",
"trino",
"tsql",
"vertica",
];

#[pyfunction]
Expand Down
1 change: 1 addition & 0 deletions crates/polyglot-sql-python/src/expr_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ macro_rules! define_expression_subclasses {
}

define_expression_subclasses!(
Vertica,
Literal,
Boolean,
Null,
Expand Down
1 change: 1 addition & 0 deletions crates/polyglot-sql-python/tests/test_dialects.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"tidb",
"trino",
"tsql",
"vertica",
}


Expand Down
12 changes: 12 additions & 0 deletions crates/polyglot-sql-python/tests/test_transpile.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,15 @@ def test_hana_native_clauses_and_source_semantics():
) == ["SELECT STRPOS('abcabc', 'bc')"]
with pytest.raises(polyglot_sql.TranspileError):
polyglot_sql.transpile(sql, read="hana", write="duckdb")


def test_vertica_structured_syntax_and_semantic_errors():
sql = "COPY t FROM LOCAL '/tmp/data.json' PARSER FJSONPARSER(flatten_maps=TRUE)"
ast = polyglot_sql.parse_one(sql, dialect="vertica")
assert ast.to_dict()["vertica"]["kind"] == "copy"
assert "FJSONPARSER(flatten_maps = TRUE)" in ast.sql(dialect="vertica")
assert polyglot_sql.transpile("SELECT B'101100'", read="vertica", write="duckdb") == ["SELECT UNHEX('2c')"]
for level in ("ignore", "warn", "raise", "immediate"):
for sql in ("SELECT x::!INT FROM t", "SELECT LISTAGG(x) FROM t", "SELECT id FROM t FOR UPDATE"):
with pytest.raises(polyglot_sql.TranspileError):
polyglot_sql.transpile(sql, read="vertica", write="postgres", unsupported_level=level)
2 changes: 2 additions & 0 deletions crates/polyglot-sql-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ all-dialects = [
"dialect-druid", "dialect-solr", "dialect-tableau",
"dialect-dune", "dialect-fabric", "dialect-drill",
"dialect-dremio", "dialect-exasol", "dialect-datafusion", "dialect-hana",
"dialect-vertica",
]
dialect-postgresql = ["polyglot-sql/dialect-postgresql"]
dialect-mysql = ["polyglot-sql/dialect-mysql"]
Expand Down Expand Up @@ -109,6 +110,7 @@ dialect-dremio = ["polyglot-sql/dialect-dremio"]
dialect-exasol = ["polyglot-sql/dialect-exasol"]
dialect-datafusion = ["polyglot-sql/dialect-datafusion"]
dialect-hana = ["polyglot-sql/dialect-hana"]
dialect-vertica = ["polyglot-sql/dialect-vertica"]
function-catalog-clickhouse = [
"dialect-clickhouse",
"semantic",
Expand Down
12 changes: 8 additions & 4 deletions crates/polyglot-sql-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,8 @@ fn get_dialects_internal() -> Vec<&'static str> {
dialects.push("datafusion");
#[cfg(feature = "dialect-hana")]
dialects.push("hana");
#[cfg(feature = "dialect-vertica")]
dialects.push("vertica");
dialects
}

Expand Down Expand Up @@ -2929,12 +2931,13 @@ mod tests {
let dialects: Vec<String> = serde_json::from_str(&result).unwrap();
let unique: std::collections::BTreeSet<&str> =
dialects.iter().map(String::as_str).collect();
assert_eq!(dialects.len(), 35);
assert_eq!(dialects.len(), 36);
assert_eq!(unique.len(), dialects.len());
assert!(unique.contains("generic"));
assert!(unique.contains("postgresql"));
assert!(unique.contains("datafusion"));
assert!(unique.contains("hana"));
assert!(unique.contains("vertica"));
}

#[test]
Expand Down Expand Up @@ -3585,6 +3588,7 @@ mod tests {
"dremio",
"exasol",
"datafusion",
"vertica",
];

for dialect in dialects {
Expand Down Expand Up @@ -4527,7 +4531,7 @@ mod tests {
// ============================================================================

/// When all-dialects is disabled, get_dialects() must always include "generic"
/// and must NOT include all 34 dialects.
/// and must NOT include all 36 dialects.
#[test]
#[cfg(not(feature = "all-dialects"))]
fn test_per_dialect_get_dialects_subset() {
Expand All @@ -4539,8 +4543,8 @@ mod tests {
dialects
);
assert!(
dialects.len() < 34,
"Per-dialect build should have fewer than 34 dialects, got {}",
dialects.len() < 36,
"Per-dialect build should have fewer than 36 dialects, got {}",
dialects.len()
);
}
Expand Down
2 changes: 2 additions & 0 deletions crates/polyglot-sql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ all-dialects = [
"dialect-druid", "dialect-solr", "dialect-tableau",
"dialect-dune", "dialect-fabric", "dialect-drill",
"dialect-dremio", "dialect-exasol", "dialect-datafusion", "dialect-hana",
"dialect-vertica",
]
dialect-postgresql = []
dialect-mysql = []
Expand Down Expand Up @@ -83,6 +84,7 @@ dialect-dremio = []
dialect-exasol = []
dialect-datafusion = []
dialect-hana = []
dialect-vertica = []
function-catalog-clickhouse = [
"semantic",
"dep:polyglot-sql-function-catalogs",
Expand Down
2 changes: 1 addition & 1 deletion crates/polyglot-sql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ assert_eq!(err.line(), None);

## Supported Dialects

Athena, BigQuery, ClickHouse, CockroachDB, DataFusion, Databricks, Doris, Dremio, Drill, Druid, DuckDB, Dune, Exasol, Fabric, Generic SQL, SAP HANA, Hive, Materialize, MySQL, Oracle, PostgreSQL, Presto, Redshift, RisingWave, SingleStore, Snowflake, Solr, Spark, SQLite, StarRocks, Tableau, Teradata, TiDB, Trino, TSQL
Athena, BigQuery, ClickHouse, CockroachDB, DataFusion, Databricks, Doris, Dremio, Drill, Druid, DuckDB, Dune, Exasol, Fabric, Generic SQL, SAP HANA, Hive, Materialize, MySQL, Oracle, PostgreSQL, Presto, Redshift, RisingWave, SingleStore, Snowflake, Solr, Spark, SQLite, StarRocks, Tableau, Teradata, TiDB, Trino, TSQL, Vertica

## Feature Flags

Expand Down
45 changes: 32 additions & 13 deletions crates/polyglot-sql/src/ast_mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,19 +520,13 @@ pub(crate) fn set_case_else(expression: &mut Expression, result: Expression) ->
Ok(())
}

pub(crate) fn subquery(
query: Expression,
alias: Option<Identifier>,
modifiers_inside: bool,
) -> Result<Expression> {
if !is_query(&query) {
return Err(invalid_method("subquery", &query));
}
Ok(Expression::Subquery(Box::new(Subquery {
this: query,
/// Wrap an expression in a derived table with an explicit AS alias.
pub(crate) fn derived_table(this: Expression, alias: Option<Identifier>) -> Expression {
Expression::Subquery(Box::new(Subquery {
this,
alias,
column_aliases: Vec::new(),
alias_explicit_as: false,
alias_explicit_as: true,
alias_keyword: None,
order_by: None,
limit: None,
Expand All @@ -541,10 +535,35 @@ pub(crate) fn subquery(
sort_by: None,
cluster_by: None,
lateral: false,
modifiers_inside,
modifiers_inside: true,
trailing_comments: Vec::new(),
inferred_type: None,
})))
}))
}

/// Qualify a column without losing the output identifier's quoting.
pub(crate) fn qualified_column(table: &str, name: &Identifier) -> Expression {
let Expression::Column(mut c) = Expression::qualified_column(table, &name.name) else {
unreachable!()
};
c.name = name.clone();
Expression::Column(c)
}

pub(crate) fn subquery(
query: Expression,
alias: Option<Identifier>,
modifiers_inside: bool,
) -> Result<Expression> {
if !is_query(&query) {
return Err(invalid_method("subquery", &query));
}
let Expression::Subquery(mut subquery) = derived_table(query, alias) else {
unreachable!()
};
subquery.alias_explicit_as = false;
subquery.modifiers_inside = modifiers_inside;
Ok(Expression::Subquery(subquery))
}

pub(crate) fn merge(target: Expression) -> Expression {
Expand Down
83 changes: 83 additions & 0 deletions crates/polyglot-sql/src/ast_transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,63 @@ use crate::ast_mutation as engine;
use crate::expressions::*;
use crate::traversal::{is_aggregate, ExpressionWalk};

/// Allocate case-insensitively unique names for synthesized AST scopes.
#[cfg(feature = "generate")]
#[derive(Default)]
pub(crate) struct AstNames(HashSet<String>);

#[cfg(feature = "generate")]
impl AstNames {
pub(crate) fn collect(&mut self, expression: &Expression) {
for node in expression.dfs() {
match node {
Expression::Column(c) => {
self.reserve(&c.name);
if let Some(table) = &c.table {
self.reserve(table);
}
}
Expression::Identifier(id) => self.reserve(id),
Expression::Alias(a) => {
self.reserve(&a.alias);
for id in &a.column_aliases {
self.reserve(id);
}
}
Expression::Table(t) => {
self.reserve(&t.name);
if let Some(alias) = &t.alias {
self.reserve(alias);
}
}
Expression::Subquery(s) => {
if let Some(alias) = &s.alias {
self.reserve(alias);
}
for id in &s.column_aliases {
self.reserve(id);
}
}
_ => {}
}
}
}

fn reserve(&mut self, id: &Identifier) {
self.0.insert(id.name.to_ascii_lowercase());
}

pub(crate) fn fresh(&mut self, stem: &str) -> String {
let mut name = stem.to_string();
let mut suffix = 0;
while !self.0.insert(name.to_ascii_lowercase()) {
suffix += 1;
name = format!("{stem}_{suffix}");
}
name
}
}

/// Apply a bottom-up transformation to every node in the tree.
/// Wraps `crate::traversal::transform` with a simpler signature for this module.
fn xform<F: Fn(Expression) -> Expression>(expr: Expression, fun: F) -> Expression {
Expand Down Expand Up @@ -309,6 +366,32 @@ pub fn remove_nodes<F: Fn(&Expression) -> bool>(expr: Expression, predicate: F)
// Convenience getters
// ---------------------------------------------------------------------------

/// Return a single named projection while preserving identifier quoting.
#[cfg(feature = "generate")]
pub(crate) fn output_identifier(expression: &Expression) -> Option<&Identifier> {
match expression {
Expression::Alias(a) if a.column_aliases.is_empty() => Some(&a.alias),
Expression::Column(c) => Some(&c.name),
_ => None,
}
}

/// Borrow the leftmost projection list through query wrappers and set operations.
#[cfg(feature = "generate")]
pub(crate) fn query_projections(mut query: &Expression) -> Option<&[Expression]> {
loop {
query = match query {
Expression::Select(s) => return Some(&s.expressions),
Expression::Union(s) => &s.left,
Expression::Intersect(s) => &s.left,
Expression::Except(s) => &s.left,
Expression::Subquery(s) => &s.this,
Expression::Paren(p) => &p.this,
_ => return None,
};
}
}

/// Collect all column names (as `String`) referenced in the expression tree.
pub fn get_column_names(expr: &Expression) -> Vec<String> {
expr.find_all(|e| matches!(e, Expression::Column(_)))
Expand Down
3 changes: 3 additions & 0 deletions crates/polyglot-sql/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,7 @@ impl Expr {
/// call default to ascending order when passed to [`SelectBuilder::order_by()`].
pub fn asc(self) -> Expr {
Expr(Expression::Ordered(Box::new(Ordered {
nulls_auto: false,
this: self.0,
desc: false,
nulls_first: None,
Expand All @@ -1172,6 +1173,7 @@ impl Expr {
/// Used in ORDER BY clauses.
pub fn desc(self) -> Expr {
Expr(Expression::Ordered(Box::new(Ordered {
nulls_auto: false,
this: self.0,
desc: true,
nulls_first: None,
Expand Down Expand Up @@ -2275,6 +2277,7 @@ impl WindowDefBuilder {
match expr {
Expression::Ordered(o) => *o,
other => Ordered {
nulls_auto: false,
this: other,
desc: false,
nulls_first: None,
Expand Down
Loading
Loading