diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 836d55614..000000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Rust - -on: [push, pull_request] - -jobs: - - codestyle: - runs-on: ubuntu-latest - steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - with: - components: rustfmt - # Note that `nightly` is required for `license_template_path`, as - # it's an unstable feature. - rust-version: nightly - - uses: actions/checkout@v2 - - run: cargo +nightly fmt -- --check --config-path <(echo 'license_template_path = "HEADER"') - - lint: - runs-on: ubuntu-latest - steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - with: - components: clippy - - uses: actions/checkout@v2 - - run: cargo clippy --all-targets --all-features -- -D warnings - - compile: - runs-on: ubuntu-latest - steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - - uses: actions/checkout@master - - run: cargo check --all-targets --all-features - - docs: - runs-on: ubuntu-latest - env: - RUSTDOCFLAGS: "-Dwarnings" - steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - - uses: actions/checkout@master - - run: cargo doc --document-private-items --no-deps --workspace --all-features - - compile-no-std: - runs-on: ubuntu-latest - steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - with: - targets: 'thumbv6m-none-eabi' - - uses: actions/checkout@master - - run: cargo check --no-default-features --target thumbv6m-none-eabi - - test: - strategy: - matrix: - rust: [stable, beta, nightly] - runs-on: ubuntu-latest - steps: - - name: Setup Rust - uses: hecrj/setup-rust-action@v1 - with: - rust-version: ${{ matrix.rust }} - - name: Install Tarpaulin - uses: actions-rs/install@v0.1 - with: - crate: cargo-tarpaulin - version: 0.14.2 - use-tool-cache: true - - name: Checkout - uses: actions/checkout@v2 - - name: Test - run: cargo test --all-features - - name: Coverage - if: matrix.rust == 'stable' - run: cargo tarpaulin -o Lcov --output-dir ./coverage - - name: Coveralls - if: matrix.rust == 'stable' - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - publish-crate: - if: startsWith(github.ref, 'refs/tags/v0') - runs-on: ubuntu-latest - needs: [test] - steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - - uses: actions/checkout@v2 - - name: Publish - shell: bash - run: | - cargo publish --token ${{ secrets.CRATES_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..ff44d8f3d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Test Suite +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize] + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + + test: + name: Test Suite + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: test diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 35210526a..ca9d51a81 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -584,12 +584,14 @@ pub enum Expr { /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) /// TRIM() + /// TRIM(, [, characters]) -- only Snowflake /// ``` Trim { expr: Box, // ([BOTH | LEADING | TRAILING] trim_where: Option, trim_what: Option>, + trim_characters: Option>, }, /// ```sql /// OVERLAY( PLACING FROM [ FOR ] @@ -984,6 +986,7 @@ impl fmt::Display for Expr { expr, trim_where, trim_what, + trim_characters } => { write!(f, "TRIM(")?; if let Some(ident) = trim_where { @@ -994,6 +997,9 @@ impl fmt::Display for Expr { } else { write!(f, "{expr}")?; } + if let Some(characters) = trim_characters { + write!(f, ", {}", display_comma_separated(characters))?; + } write!(f, ")") } diff --git a/src/parser.rs b/src/parser.rs index 17c197801..a632fd3d4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1254,6 +1254,7 @@ impl<'a> Parser<'a> { /// ```sql /// TRIM ([WHERE] ['text' FROM] 'text') /// TRIM ('text') + /// TRIM(, [, characters]) -- only Snowflake /// ``` pub fn parse_trim_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; @@ -1275,6 +1276,16 @@ impl<'a> Parser<'a> { expr: Box::new(expr), trim_where, trim_what: Some(trim_what), + trim_characters: None, + }) + } else if self.consume_token(&Token::Comma) && dialect_of!(self is SnowflakeDialect) { + let characters = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + Ok(Expr::Trim { + expr: Box::new(expr), + trim_where: None, + trim_what: None, + trim_characters: Some(characters), }) } else { self.expect_token(&Token::RParen)?; @@ -1282,6 +1293,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), trim_where, trim_what: None, + trim_characters: None, }) } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4dc758e74..6d0d3ff22 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4967,6 +4967,30 @@ fn parse_trim() { ParserError::ParserError("Expected ), found: 'xyz'\nNear `SELECT TRIM(FOO`".to_owned()), parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')").unwrap_err() ); + + //keep Snowflake TRIM syntax failing + let all_expected_snowflake = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + //Box::new(SnowflakeDialect {}), + Box::new(HiveDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), + ], + options: None, + }; + assert_eq!( + ParserError::ParserError("Expected ), found: 'a'\nNear `SELECT TRIM('xyz',`".to_owned()), + all_expected_snowflake + .parse_sql_statements("SELECT TRIM('xyz', 'a')") + .unwrap_err() + ); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 7be43cb59..3d90c9090 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1053,3 +1053,28 @@ fn test_snowflake_stage_object_names() { } } } + +#[test] +fn test_snowflake_trim() { + let real_sql = r#"SELECT customer_id, TRIM(sub_items.value:item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; + assert_eq!(snowflake().verified_stmt(real_sql).to_string(), real_sql); + + let sql_only_select = "SELECT TRIM('xyz', 'a')"; + let select = snowflake().verified_only_select(sql_only_select); + assert_eq!( + &Expr::Trim { + expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value(Value::SingleQuotedString("a".to_owned()))]), + }, + expr_from_projection(only(&select.projection)) + ); + + // missing comma separation + let error_sql = "SELECT TRIM('xyz' 'a')"; + assert_eq!( + ParserError::ParserError("Expected ), found: 'a'\nNear `SELECT TRIM('xyz'`".to_owned()), + snowflake().parse_sql_statements(error_sql).unwrap_err() + ); +}