Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ mod snowflake;
mod spark;
mod sqlite;
mod teradata;
mod trino;

use core::any::{Any, TypeId};
use core::fmt::Debug;
Expand All @@ -56,6 +57,7 @@ pub use self::snowflake::SnowflakeDialect;
pub use self::spark::SparkSqlDialect;
pub use self::sqlite::SQLiteDialect;
pub use self::teradata::TeradataDialect;
pub use self::trino::TrinoDialect;

/// Macro for streamlining the creation of derived `Dialect` objects.
/// The generated struct includes `new()` and `default()` constructors.
Expand Down Expand Up @@ -1927,6 +1929,7 @@ pub fn dialect_from_str(dialect_name: impl AsRef<str>) -> Option<Box<dyn Dialect
"spark" | "sparksql" => Some(Box::new(SparkSqlDialect {})),
"oracle" => Some(Box::new(OracleDialect {})),
"teradata" => Some(Box::new(TeradataDialect {})),
"trino" => Some(Box::new(TrinoDialect {})),
_ => None,
}
}
Expand Down Expand Up @@ -1981,6 +1984,7 @@ mod tests {
assert!(parse_dialect("DataBricks").is::<DatabricksDialect>());
assert!(parse_dialect("databricks").is::<DatabricksDialect>());
assert!(parse_dialect("teradata").is::<TeradataDialect>());
assert!(parse_dialect("trino").is::<TrinoDialect>());
assert!(parse_dialect("Teradata").is::<TeradataDialect>());

// error cases
Expand Down
110 changes: 110 additions & 0 deletions src/dialect/trino.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

use crate::dialect::Dialect;

/// A [`Dialect`] for [Trino](https://trino.io/docs/current/language.html),
/// the distributed SQL engine that descends from Presto.
///
/// Trino follows the SQL standard closely: identifiers are delimited with
/// double quotes only, aggregates take `FILTER (WHERE ...)`, higher-order
/// functions take `x -> ...` lambdas, and row pattern recognition, table
/// sampling, table versioning (time travel) and parenthesized `EXPLAIN`
/// options are all part of the grammar.
///
/// See <https://trino.io/docs/current/sql.html>.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TrinoDialect;

impl Dialect for TrinoDialect {
/// Trino delimits identifiers with double quotes only; backquotes are a
/// syntax error, the most common mistake of users arriving from
/// MySQL, ClickHouse or Spark.
///
/// See <https://trino.io/docs/current/language/reserved.html>
fn is_delimited_identifier_start(&self, ch: char) -> bool {
ch == '"'
}

fn is_identifier_start(&self, ch: char) -> bool {
ch.is_ascii_alphabetic() || ch == '_'
}

fn is_identifier_part(&self, ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_'
}

/// See <https://trino.io/docs/current/sql/select.html#filter>
fn supports_filter_during_aggregation(&self) -> bool {
true
}

/// `GROUP BY` accepts arbitrary expressions, `GROUPING SETS`, `CUBE`
/// and `ROLLUP`.
///
/// See <https://trino.io/docs/current/sql/select.html#group-by-clause>
fn supports_group_by_expr(&self) -> bool {
true
}

/// See <https://trino.io/docs/current/functions/lambda.html>
fn supports_lambda_functions(&self) -> bool {
true
}

/// See <https://trino.io/docs/current/sql/match-recognize.html>
fn supports_match_recognize(&self) -> bool {
true
}

/// Trino reads Iceberg and Delta tables at a point in time with
/// `FOR TIMESTAMP AS OF` and `FOR VERSION AS OF`.
///
/// See <https://trino.io/docs/current/connector/iceberg.html#time-travel-queries>
fn supports_table_versioning(&self) -> bool {
true
}

/// `EXPLAIN (TYPE IO, FORMAT JSON) ...` and friends.
///
/// See <https://trino.io/docs/current/sql/explain.html>
fn supports_explain_with_utility_options(&self) -> bool {
true
}

/// Named arguments to table functions use `=>`, e.g.
/// `TABLE(sequence(start => 1, stop => 10))`.
///
/// See <https://trino.io/docs/current/functions/table.html>
fn supports_named_fn_args_with_rarrow_operator(&self) -> bool {
true
}

/// See <https://trino.io/docs/current/sql/comment.html>
fn supports_comment_on(&self) -> bool {
true
}

/// `SHOW TABLES FROM schema LIKE '%x%'`: the source comes before the
/// pattern.
///
/// See <https://trino.io/docs/current/sql/show-tables.html>
fn supports_show_like_before_in(&self) -> bool {
false
}
}
228 changes: 228 additions & 0 deletions tests/sqlparser_trino.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

#![warn(clippy::all)]
//! Test SQL syntax specific to Trino.

use sqlparser::ast::*;
use sqlparser::dialect::TrinoDialect;
use sqlparser::parser::ParserError;
use test_utils::*;

#[macro_use]
mod test_utils;

fn trino() -> TestedDialects {
TestedDialects::new(vec![Box::new(TrinoDialect {})])
}

// --------------------------------
// Identifiers
// --------------------------------

#[test]
fn double_quoted_identifiers() {
let select = trino().verified_only_select(r#"SELECT "order_id" FROM iceberg."demo"."orders""#);
match &select.projection[0] {
SelectItem::UnnamedExpr(Expr::Identifier(ident)) => {
assert_eq!(ident.value, "order_id");
assert_eq!(ident.quote_style, Some('"'));
}
other => panic!("expected a quoted identifier, got {other:?}"),
}
}

#[test]
fn backquoted_identifiers_are_rejected() {
// Trino has no backquote quoting; the parser must not accept it either,
// or a MySQL/ClickHouse habit would be reported far from its cause.
let err = trino()
.parse_sql_statements("SELECT * FROM `demo`.`orders`")
.unwrap_err();
assert!(matches!(
err,
ParserError::ParserError(_) | ParserError::TokenizerError(_)
));
}

#[test]
fn hidden_columns_are_quoted_identifiers() {
trino().verified_stmt(r#"SELECT "$path" FROM iceberg.demo.orders"#);
}

// --------------------------------
// Aggregates, grouping and lambdas
// --------------------------------

#[test]
fn filter_during_aggregation() {
trino().verified_stmt("SELECT count(*) FILTER (WHERE status = 'DELIVERED') FROM orders");
}

#[test]
fn approx_percentile_and_array_agg_with_order() {
trino().verified_stmt("SELECT approx_percentile(total, 0.5) FROM orders");
trino().verified_stmt("SELECT array_agg(status ORDER BY status) FROM orders");
}

#[test]
fn grouping_sets_cube_rollup() {
trino().verified_stmt(
"SELECT status, region, count(*) FROM orders GROUP BY GROUPING SETS ((status), (region), ())",
);
trino().verified_stmt("SELECT status, count(*) FROM orders GROUP BY ROLLUP (status)");
trino().verified_stmt("SELECT status, count(*) FROM orders GROUP BY CUBE (status, region)");
}

#[test]
fn lambda_functions() {
trino().verified_stmt("SELECT filter(ARRAY[1, 2, 3], x -> x > 1)");
trino().verified_stmt("SELECT transform(ARRAY[1, 2], x -> x * 2)");
trino().verified_stmt("SELECT reduce(ARRAY[1, 2, 3], 0, (s, x) -> s + x, s -> s)");
}

// --------------------------------
// Relations
// --------------------------------

#[test]
fn unnest_with_ordinality() {
trino().verified_stmt("SELECT t.x, t.i FROM UNNEST(ARRAY[10, 20]) WITH ORDINALITY AS t (x, i)");
}

#[test]
fn cross_join_unnest() {
trino().verified_stmt("SELECT o.id, e FROM orders AS o CROSS JOIN UNNEST(o.items) AS t (e)");
}

#[test]
fn values_with_column_aliases() {
trino().verified_stmt("SELECT * FROM (VALUES (1, 'a'), (2, 'b')) AS t (id, name)");
}

#[test]
fn tablesample() {
trino().verified_stmt("SELECT count(*) FROM orders TABLESAMPLE BERNOULLI (50)");
trino().verified_stmt("SELECT count(*) FROM orders AS o TABLESAMPLE SYSTEM (10)");
}

#[test]
fn table_functions_with_named_arguments() {
trino().verified_stmt("SELECT * FROM TABLE(sequence(start => 1, stop => 10))");
}

// --------------------------------
// Expressions and literals
// --------------------------------

#[test]
fn typed_literals() {
trino().verified_stmt("SELECT DATE '2026-01-01'");
trino().verified_stmt("SELECT TIMESTAMP '2026-01-01 00:00:00 UTC'");
trino().verified_stmt("SELECT DECIMAL '1.5'");
}

#[test]
fn interval_literal() {
trino().verified_stmt(
"SELECT count(*) FROM orders WHERE order_date > current_date - INTERVAL '365' DAY",
);
}

#[test]
fn cast_and_try_cast() {
trino().verified_stmt("SELECT CAST(order_id AS VARCHAR) FROM orders");
trino().verified_stmt("SELECT TRY_CAST(order_id AS VARCHAR) FROM orders");
}

#[test]
fn at_time_zone() {
trino().verified_stmt("SELECT created_at AT TIME ZONE 'UTC' FROM orders");
}

#[test]
fn is_distinct_from() {
trino().verified_stmt("SELECT count(*) FROM orders WHERE status IS DISTINCT FROM 'x'");
}

#[test]
fn json_path_functions() {
trino().verified_stmt(r#"SELECT JSON_VALUE('{"a":1}', 'lax $.a')"#);
trino().verified_stmt(r#"SELECT JSON_QUERY('{"a":[1,2]}', 'lax $.a')"#);
}

#[test]
fn listagg_within_group() {
trino().verified_stmt("SELECT LISTAGG(status, ',') WITHIN GROUP (ORDER BY status) FROM orders");
}

#[test]
fn map_subscript() {
trino().verified_stmt("SELECT MAP(ARRAY['k'], ARRAY['v'])['k']");
}

#[test]
fn like_with_escape() {
trino().verified_stmt(r"SELECT count(*) FROM orders WHERE status LIKE 'a\_%' ESCAPE '\'");
}

// --------------------------------
// Query shape
// --------------------------------

#[test]
fn fetch_first_rows_only() {
trino().verified_stmt("SELECT order_id FROM orders ORDER BY order_id FETCH FIRST 2 ROWS ONLY");
}

#[test]
fn with_cte() {
trino().verified_stmt(
"WITH x AS (SELECT status FROM orders) SELECT status, count(*) FROM x GROUP BY status",
);
}

#[test]
fn match_recognize() {
trino().verified_stmt(
"SELECT * FROM orders MATCH_RECOGNIZE(PARTITION BY customer_id ORDER BY order_date MEASURES A.order_date AS start_date ONE ROW PER MATCH PATTERN (A B+) DEFINE B AS B.total > PREV(B.total))",
);
}

// --------------------------------
// Statements around queries
// --------------------------------

#[test]
fn explain_with_options() {
trino().verified_stmt("EXPLAIN (TYPE IO, FORMAT JSON) SELECT * FROM orders");
trino().verified_stmt("EXPLAIN (TYPE VALIDATE) SELECT * FROM orders");
}

#[test]
fn show_and_describe() {
trino().verified_stmt("SHOW SCHEMAS FROM iceberg");
trino().verified_stmt("SHOW TABLES FROM demo");
trino().verified_stmt("SHOW COLUMNS FROM demo.orders");
trino().verified_stmt("DESCRIBE demo.orders");
}

#[test]
fn comment_on() {
trino().verified_stmt("COMMENT ON TABLE demo.orders IS 'orders'");
trino().verified_stmt("COMMENT ON COLUMN demo.orders.status IS 'lifecycle'");
}