diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 0e2a6158d..93ca5cf6a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -30,6 +30,7 @@ mod redshift; mod snowflake; mod spark; mod sqlite; +mod teradata; use core::any::{Any, TypeId}; use core::fmt::Debug; @@ -54,6 +55,7 @@ pub use self::snowflake::parse_snowflake_stage_name; pub use self::snowflake::SnowflakeDialect; pub use self::spark::SparkSqlDialect; pub use self::sqlite::SQLiteDialect; +pub use self::teradata::TeradataDialect; /// Macro for streamlining the creation of derived `Dialect` objects. /// The generated struct includes `new()` and `default()` constructors. @@ -1841,6 +1843,7 @@ pub fn dialect_from_str(dialect_name: impl AsRef) -> Option Some(Box::new(DatabricksDialect {})), "spark" | "sparksql" => Some(Box::new(SparkSqlDialect {})), "oracle" => Some(Box::new(OracleDialect {})), + "teradata" => Some(Box::new(TeradataDialect {})), _ => None, } } @@ -1894,6 +1897,8 @@ mod tests { assert!(parse_dialect("DuckDb").is::()); assert!(parse_dialect("DataBricks").is::()); assert!(parse_dialect("databricks").is::()); + assert!(parse_dialect("teradata").is::()); + assert!(parse_dialect("Teradata").is::()); // error cases assert!(dialect_from_str("Unknown").is_none()); diff --git a/src/dialect/teradata.rs b/src/dialect/teradata.rs new file mode 100644 index 000000000..e88a40075 --- /dev/null +++ b/src/dialect/teradata.rs @@ -0,0 +1,92 @@ +// 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 [Teradata](https://docs.teradata.com/). +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct TeradataDialect; + +impl Dialect for TeradataDialect { + /// See + fn identifier_quote_style(&self, _identifier: &str) -> Option { + Some('"') + } + + /// See + fn is_delimited_identifier_start(&self, ch: char) -> bool { + ch == '"' + } + + /// See + fn is_identifier_start(&self, ch: char) -> bool { + ch.is_alphabetic() || ch == '_' || ch == '#' || ch == '$' + } + + // See + fn is_identifier_part(&self, ch: char) -> bool { + ch.is_alphanumeric() || self.is_identifier_start(ch) + } + + /// See + fn supports_group_by_expr(&self) -> bool { + true + } + + /// Teradata has no native `BOOLEAN` data type. + /// + /// See + fn supports_boolean_literals(&self) -> bool { + false + } + + /// See + fn require_interval_qualifier(&self) -> bool { + true + } + + /// See + fn supports_comment_on(&self) -> bool { + true + } + + /// See + fn supports_create_table_select(&self) -> bool { + true + } + + /// See + fn supports_execute_immediate(&self) -> bool { + true + } + + /// See + fn supports_top_before_distinct(&self) -> bool { + true + } + + /// See + fn supports_window_function_null_treatment_arg(&self) -> bool { + true + } + + /// See + fn supports_string_literal_concatenation(&self) -> bool { + true + } +} diff --git a/src/test_utils.rs b/src/test_utils.rs index 9ba5960e8..58882e7c5 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -292,6 +292,7 @@ pub fn all_dialects() -> TestedDialects { Box::new(DatabricksDialect {}), Box::new(ClickHouseDialect {}), Box::new(OracleDialect {}), + Box::new(TeradataDialect {}), ]) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c76899f04..5df573185 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18180,7 +18180,7 @@ fn test_parse_semantic_view_table_factor() { #[test] fn parse_adjacent_string_literal_concatenation() { - let sql = r#"SELECT 'M' "y" 'S' "q" 'l'"#; + let sql = r#"SELECT 'M' 'y' 'S' 'q' 'l'"#; let dialects = all_dialects_where(|d| d.supports_string_literal_concatenation()); dialects.one_statement_parses_to(sql, r"SELECT 'MySql'"); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f0de7d817..eeefb7c6c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4891,3 +4891,9 @@ fn parse_create_database_with_charset_option_ordering() { "CREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci", ); } + +#[test] +fn parse_adjacent_string_literal_concatenation() { + let sql = r#"SELECT 'M' "y" 'S' "q" 'l'"#; + mysql().one_statement_parses_to(sql, r"SELECT 'MySql'"); +} diff --git a/tests/sqlparser_teradata.rs b/tests/sqlparser_teradata.rs new file mode 100644 index 000000000..b8e6a53ce --- /dev/null +++ b/tests/sqlparser_teradata.rs @@ -0,0 +1,63 @@ +// 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. + +//! Test SQL syntax, specific to [sqlparser::dialect::TeradataDialect]. + +use sqlparser::dialect::{Dialect, TeradataDialect}; +use test_utils::TestedDialects; + +mod test_utils; + +fn teradata() -> TestedDialects { + TestedDialects::new(vec![Box::new(TeradataDialect)]) +} + +#[test] +fn dialect_methods() { + let d: &dyn Dialect = &TeradataDialect; + assert_eq!(d.identifier_quote_style("x"), Some('"')); + assert!(d.is_delimited_identifier_start('"')); + assert!(!d.is_delimited_identifier_start('`')); + assert!(d.is_identifier_start('$')); + assert!(d.is_identifier_start('#')); + assert!(d.is_identifier_part('$')); + assert!(d.is_identifier_part('#')); + assert!(d.supports_group_by_expr()); + assert!(!d.supports_boolean_literals()); + assert!(d.require_interval_qualifier()); + assert!(d.supports_comment_on()); + assert!(d.supports_create_table_select()); + assert!(d.supports_execute_immediate()); + assert!(d.supports_top_before_distinct()); + assert!(d.supports_window_function_null_treatment_arg()); + assert!(d.supports_string_literal_concatenation()); +} + +#[test] +fn parse_identifier() { + teradata().verified_stmt(concat!( + "SELECT ", + "NULL AS foo, ", + "NULL AS _bar, ", + "NULL AS #baz, ", + "NULL AS $qux, ", + "NULL AS a$1, ", + "NULL AS a#1, ", + "NULL AS a_1, ", + r#"NULL AS "quoted id""# + )); +}