├── rust-toolchain ├── fuzz ├── .gitignore ├── fuzz_targets │ └── fuzz_parse_sql.rs └── Cargo.toml ├── rustfmt.toml ├── docs ├── benchmarking.md ├── custom_sql_parser.md ├── fuzzing.md └── releasing.md ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── sqlparser_bench ├── Cargo.toml └── benches │ └── sqlparser_bench.rs ├── tests ├── queries │ └── tpch │ │ ├── 6.sql │ │ ├── 17.sql │ │ ├── 14.sql │ │ ├── 13.sql │ │ ├── 4.sql │ │ ├── 3.sql │ │ ├── 1.sql │ │ ├── 5.sql │ │ ├── 18.sql │ │ ├── 16.sql │ │ ├── 11.sql │ │ ├── 10.sql │ │ ├── 15.sql │ │ ├── 12.sql │ │ ├── 9.sql │ │ ├── 20.sql │ │ ├── 22.sql │ │ ├── 2.sql │ │ ├── 21.sql │ │ ├── 8.sql │ │ ├── 7.sql │ │ └── 19.sql ├── test_utils │ └── mod.rs ├── sqlparser_regression.rs ├── sqlparser_mssql.rs ├── sqlparser_sqlite.rs ├── sqlparser_hive.rs ├── sqlparser_snowflake.rs └── sqlparser_mysql.rs ├── .gitignore ├── HEADER ├── examples ├── parse_select.rs └── cli.rs ├── src ├── dialect │ ├── ansi.rs │ ├── snowflake.rs │ ├── generic.rs │ ├── postgresql.rs │ ├── hive.rs │ ├── mysql.rs │ ├── sqlite.rs │ ├── mssql.rs │ └── mod.rs ├── lib.rs ├── ast │ ├── operator.rs │ ├── data_type.rs │ ├── value.rs │ ├── ddl.rs │ └── query.rs ├── test_utils.rs └── keywords.rs ├── Cargo.toml ├── .travis.yml ├── README.md ├── LICENSE.TXT └── CHANGELOG.md /rust-toolchain: -------------------------------------------------------------------------------- 1 | stable -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | corpus 2 | hfuzz_target 3 | hfuzz_workspace 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # We use rustfmt's default settings to format the source code -------------------------------------------------------------------------------- /docs/benchmarking.md: -------------------------------------------------------------------------------- 1 | # Benchmarking 2 | 3 | Run `cargo bench` in the project `sqlparser_bench` execute the queries. 4 | It will report results using the `criterion` library to perform the benchmarking. 5 | 6 | The bench project lives in another crate, to avoid the negative impact on building the `sqlparser` crate. 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: cargo 9 | directory: "/sqlparser_bench" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /sqlparser_bench/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlparser_bench" 3 | version = "0.1.0" 4 | authors = ["Dandandan "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | sqlparser = { path = "../" } 9 | 10 | [dev-dependencies] 11 | criterion = "0.3" 12 | 13 | [[bench]] 14 | name = "sqlparser_bench" 15 | harness = false 16 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_parse_sql.rs: -------------------------------------------------------------------------------- 1 | use honggfuzz::fuzz; 2 | use sqlparser::dialect::GenericDialect; 3 | use sqlparser::parser::Parser; 4 | 5 | fn main() { 6 | loop { 7 | fuzz!(|data: String| { 8 | let dialect = GenericDialect {}; 9 | let _ = Parser::parse_sql(&dialect, &data); 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/queries/tpch/6.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | sum(l_extendedprice * l_discount) as revenue 6 | from 7 | lineitem 8 | where 9 | l_shipdate >= date '1994-01-01' 10 | and l_shipdate < date '1994-01-01' + interval '1' year 11 | and l_discount between .06 - 0.01 and .06 + 0.01 12 | and l_quantity < 24; 13 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuzz" 3 | version = "0.1.0" 4 | edition = "2018" 5 | publish = false 6 | 7 | [dependencies] 8 | honggfuzz = "0.5.54" 9 | sqlparser = { path = ".." } 10 | 11 | # Prevent this from interfering with workspaces 12 | [workspace] 13 | members = ["."] 14 | 15 | [[bin]] 16 | name = "fuzz_parse_sql" 17 | path = "fuzz_targets/fuzz_parse_sql.rs" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /sqlparser_bench/target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # IDEs 14 | .idea 15 | -------------------------------------------------------------------------------- /tests/queries/tpch/17.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | sum(l_extendedprice) / 7.0 as avg_yearly 6 | from 7 | lineitem, 8 | part 9 | where 10 | p_partkey = l_partkey 11 | and p_brand = 'Brand#23' 12 | and p_container = 'MED BOX' 13 | and l_quantity < ( 14 | select 15 | 0.2 * avg(l_quantity) 16 | from 17 | lineitem 18 | where 19 | l_partkey = p_partkey 20 | ); 21 | -------------------------------------------------------------------------------- /tests/queries/tpch/14.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | 100.00 * sum(case 6 | when p_type like 'PROMO%' 7 | then l_extendedprice * (1 - l_discount) 8 | else 0 9 | end) / sum(l_extendedprice * (1 - l_discount)) as promo_revenue 10 | from 11 | lineitem, 12 | part 13 | where 14 | l_partkey = p_partkey 15 | and l_shipdate >= date '1995-09-01' 16 | and l_shipdate < date '1995-09-01' + interval '1' month; 17 | -------------------------------------------------------------------------------- /tests/queries/tpch/13.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | c_count, 6 | count(*) as custdist 7 | from 8 | ( 9 | select 10 | c_custkey, 11 | count(o_orderkey) 12 | from 13 | customer left outer join orders on 14 | c_custkey = o_custkey 15 | and o_comment not like '%special%requests%' 16 | group by 17 | c_custkey 18 | ) as c_orders (c_custkey, c_count) 19 | group by 20 | c_count 21 | order by 22 | custdist desc, 23 | c_count desc; 24 | -------------------------------------------------------------------------------- /tests/queries/tpch/4.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | o_orderpriority, 6 | count(*) as order_count 7 | from 8 | orders 9 | where 10 | o_orderdate >= date '1993-07-01' 11 | and o_orderdate < date '1993-07-01' + interval '3' month 12 | and exists ( 13 | select 14 | * 15 | from 16 | lineitem 17 | where 18 | l_orderkey = o_orderkey 19 | and l_commitdate < l_receiptdate 20 | ) 21 | group by 22 | o_orderpriority 23 | order by 24 | o_orderpriority; 25 | -------------------------------------------------------------------------------- /tests/queries/tpch/3.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | l_orderkey, 6 | sum(l_extendedprice * (1 - l_discount)) as revenue, 7 | o_orderdate, 8 | o_shippriority 9 | from 10 | customer, 11 | orders, 12 | lineitem 13 | where 14 | c_mktsegment = 'BUILDING' 15 | and c_custkey = o_custkey 16 | and l_orderkey = o_orderkey 17 | and o_orderdate < date '1995-03-15' 18 | and l_shipdate > date '1995-03-15' 19 | group by 20 | l_orderkey, 21 | o_orderdate, 22 | o_shippriority 23 | order by 24 | revenue desc, 25 | o_orderdate; 26 | -------------------------------------------------------------------------------- /HEADER: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. -------------------------------------------------------------------------------- /tests/queries/tpch/1.sql: -------------------------------------------------------------------------------- 1 | select 2 | l_returnflag, 3 | l_linestatus, 4 | sum(l_quantity) as sum_qty, 5 | sum(l_extendedprice) as sum_base_price, 6 | sum(l_extendedprice * (1 - l_discount)) as sum_disc_price, 7 | sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge, 8 | avg(l_quantity) as avg_qty, 9 | avg(l_extendedprice) as avg_price, 10 | avg(l_discount) as avg_disc, 11 | count(*) as count_order 12 | from 13 | lineitem 14 | where 15 | l_shipdate <= date '1998-12-01' - interval '90' day (3) 16 | group by 17 | l_returnflag, 18 | l_linestatus 19 | order by 20 | l_returnflag, 21 | l_linestatus; 22 | -------------------------------------------------------------------------------- /tests/queries/tpch/5.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | n_name, 6 | sum(l_extendedprice * (1 - l_discount)) as revenue 7 | from 8 | customer, 9 | orders, 10 | lineitem, 11 | supplier, 12 | nation, 13 | region 14 | where 15 | c_custkey = o_custkey 16 | and l_orderkey = o_orderkey 17 | and l_suppkey = s_suppkey 18 | and c_nationkey = s_nationkey 19 | and s_nationkey = n_nationkey 20 | and n_regionkey = r_regionkey 21 | and r_name = 'ASIA' 22 | and o_orderdate >= date '1994-01-01' 23 | and o_orderdate < date '1994-01-01' + interval '1' year 24 | group by 25 | n_name 26 | order by 27 | revenue desc; 28 | -------------------------------------------------------------------------------- /tests/queries/tpch/18.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | c_name, 6 | c_custkey, 7 | o_orderkey, 8 | o_orderdate, 9 | o_totalprice, 10 | sum(l_quantity) 11 | from 12 | customer, 13 | orders, 14 | lineitem 15 | where 16 | o_orderkey in ( 17 | select 18 | l_orderkey 19 | from 20 | lineitem 21 | group by 22 | l_orderkey having 23 | sum(l_quantity) > 300 24 | ) 25 | and c_custkey = o_custkey 26 | and o_orderkey = l_orderkey 27 | group by 28 | c_name, 29 | c_custkey, 30 | o_orderkey, 31 | o_orderdate, 32 | o_totalprice 33 | order by 34 | o_totalprice desc, 35 | o_orderdate; 36 | -------------------------------------------------------------------------------- /tests/queries/tpch/16.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | p_brand, 6 | p_type, 7 | p_size, 8 | count(distinct ps_suppkey) as supplier_cnt 9 | from 10 | partsupp, 11 | part 12 | where 13 | p_partkey = ps_partkey 14 | and p_brand <> 'Brand#45' 15 | and p_type not like 'MEDIUM POLISHED%' 16 | and p_size in (49, 14, 23, 45, 19, 3, 36, 9) 17 | and ps_suppkey not in ( 18 | select 19 | s_suppkey 20 | from 21 | supplier 22 | where 23 | s_comment like '%Customer%Complaints%' 24 | ) 25 | group by 26 | p_brand, 27 | p_type, 28 | p_size 29 | order by 30 | supplier_cnt desc, 31 | p_brand, 32 | p_type, 33 | p_size; 34 | -------------------------------------------------------------------------------- /tests/queries/tpch/11.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | ps_partkey, 6 | sum(ps_supplycost * ps_availqty) as value 7 | from 8 | partsupp, 9 | supplier, 10 | nation 11 | where 12 | ps_suppkey = s_suppkey 13 | and s_nationkey = n_nationkey 14 | and n_name = 'GERMANY' 15 | group by 16 | ps_partkey having 17 | sum(ps_supplycost * ps_availqty) > ( 18 | select 19 | sum(ps_supplycost * ps_availqty) * 0.0001000000 20 | from 21 | partsupp, 22 | supplier, 23 | nation 24 | where 25 | ps_suppkey = s_suppkey 26 | and s_nationkey = n_nationkey 27 | and n_name = 'GERMANY' 28 | ) 29 | order by 30 | value desc; 31 | -------------------------------------------------------------------------------- /tests/queries/tpch/10.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | c_custkey, 6 | c_name, 7 | sum(l_extendedprice * (1 - l_discount)) as revenue, 8 | c_acctbal, 9 | n_name, 10 | c_address, 11 | c_phone, 12 | c_comment 13 | from 14 | customer, 15 | orders, 16 | lineitem, 17 | nation 18 | where 19 | c_custkey = o_custkey 20 | and l_orderkey = o_orderkey 21 | and o_orderdate >= date '1993-10-01' 22 | and o_orderdate < date '1993-10-01' + interval '3' month 23 | and l_returnflag = 'R' 24 | and c_nationkey = n_nationkey 25 | group by 26 | c_custkey, 27 | c_name, 28 | c_acctbal, 29 | c_phone, 30 | n_name, 31 | c_address, 32 | c_comment 33 | order by 34 | revenue desc; 35 | -------------------------------------------------------------------------------- /docs/custom_sql_parser.md: -------------------------------------------------------------------------------- 1 | # Writing a Custom SQL Parser 2 | 3 | I have explored many different ways of building this library to make it easy to extend it for custom SQL dialects. Most of my attempts ended in failure but I have now found a workable solution. It is not without downsides but this seems to be the most pragmatic solution. 4 | 5 | The concept is simply to write a new parser that delegates to the ANSI parser so that as much as possible of the core functionality can be re-used. 6 | 7 | I also plan on building in specific support for custom data types, where a lambda function can be parsed to the parser to parse data types. 8 | 9 | For an example of this, see the [DataFusion](https://github.com/datafusion-rs/datafusion) project. 10 | 11 | -------------------------------------------------------------------------------- /tests/queries/tpch/15.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | create view revenue0 (supplier_no, total_revenue) as 4 | select 5 | l_suppkey, 6 | sum(l_extendedprice * (1 - l_discount)) 7 | from 8 | lineitem 9 | where 10 | l_shipdate >= date '1996-01-01' 11 | and l_shipdate < date '1996-01-01' + interval '3' month 12 | group by 13 | l_suppkey; 14 | 15 | 16 | select 17 | s_suppkey, 18 | s_name, 19 | s_address, 20 | s_phone, 21 | total_revenue 22 | from 23 | supplier, 24 | revenue0 25 | where 26 | s_suppkey = supplier_no 27 | and total_revenue = ( 28 | select 29 | max(total_revenue) 30 | from 31 | revenue0 32 | ) 33 | order by 34 | s_suppkey; 35 | 36 | drop view revenue0; 37 | -------------------------------------------------------------------------------- /tests/queries/tpch/12.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | l_shipmode, 6 | sum(case 7 | when o_orderpriority = '1-URGENT' 8 | or o_orderpriority = '2-HIGH' 9 | then 1 10 | else 0 11 | end) as high_line_count, 12 | sum(case 13 | when o_orderpriority <> '1-URGENT' 14 | and o_orderpriority <> '2-HIGH' 15 | then 1 16 | else 0 17 | end) as low_line_count 18 | from 19 | orders, 20 | lineitem 21 | where 22 | o_orderkey = l_orderkey 23 | and l_shipmode in ('MAIL', 'SHIP') 24 | and l_commitdate < l_receiptdate 25 | and l_shipdate < l_commitdate 26 | and l_receiptdate >= date '1994-01-01' 27 | and l_receiptdate < date '1994-01-01' + interval '1' year 28 | group by 29 | l_shipmode 30 | order by 31 | l_shipmode; 32 | -------------------------------------------------------------------------------- /tests/queries/tpch/9.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | nation, 6 | o_year, 7 | sum(amount) as sum_profit 8 | from 9 | ( 10 | select 11 | n_name as nation, 12 | extract(year from o_orderdate) as o_year, 13 | l_extendedprice * (1 - l_discount) - ps_supplycost * l_quantity as amount 14 | from 15 | part, 16 | supplier, 17 | lineitem, 18 | partsupp, 19 | orders, 20 | nation 21 | where 22 | s_suppkey = l_suppkey 23 | and ps_suppkey = l_suppkey 24 | and ps_partkey = l_partkey 25 | and p_partkey = l_partkey 26 | and o_orderkey = l_orderkey 27 | and s_nationkey = n_nationkey 28 | and p_name like '%green%' 29 | ) as profit 30 | group by 31 | nation, 32 | o_year 33 | order by 34 | nation, 35 | o_year desc; 36 | -------------------------------------------------------------------------------- /tests/queries/tpch/20.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | s_name, 6 | s_address 7 | from 8 | supplier, 9 | nation 10 | where 11 | s_suppkey in ( 12 | select 13 | ps_suppkey 14 | from 15 | partsupp 16 | where 17 | ps_partkey in ( 18 | select 19 | p_partkey 20 | from 21 | part 22 | where 23 | p_name like 'forest%' 24 | ) 25 | and ps_availqty > ( 26 | select 27 | 0.5 * sum(l_quantity) 28 | from 29 | lineitem 30 | where 31 | l_partkey = ps_partkey 32 | and l_suppkey = ps_suppkey 33 | and l_shipdate >= date '1994-01-01' 34 | and l_shipdate < date '1994-01-01' + interval '1' year 35 | ) 36 | ) 37 | and s_nationkey = n_nationkey 38 | and n_name = 'CANADA' 39 | order by 40 | s_name; 41 | -------------------------------------------------------------------------------- /tests/queries/tpch/22.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | cntrycode, 6 | count(*) as numcust, 7 | sum(c_acctbal) as totacctbal 8 | from 9 | ( 10 | select 11 | substring(c_phone from 1 for 2) as cntrycode, 12 | c_acctbal 13 | from 14 | customer 15 | where 16 | substring(c_phone from 1 for 2) in 17 | ('13', '31', '23', '29', '30', '18', '17') 18 | and c_acctbal > ( 19 | select 20 | avg(c_acctbal) 21 | from 22 | customer 23 | where 24 | c_acctbal > 0.00 25 | and substring(c_phone from 1 for 2) in 26 | ('13', '31', '23', '29', '30', '18', '17') 27 | ) 28 | and not exists ( 29 | select 30 | * 31 | from 32 | orders 33 | where 34 | o_custkey = c_custkey 35 | ) 36 | ) as custsale 37 | group by 38 | cntrycode 39 | order by 40 | cntrycode; 41 | -------------------------------------------------------------------------------- /tests/queries/tpch/2.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | s_acctbal, 6 | s_name, 7 | n_name, 8 | p_partkey, 9 | p_mfgr, 10 | s_address, 11 | s_phone, 12 | s_comment 13 | from 14 | part, 15 | supplier, 16 | partsupp, 17 | nation, 18 | region 19 | where 20 | p_partkey = ps_partkey 21 | and s_suppkey = ps_suppkey 22 | and p_size = 15 23 | and p_type like '%BRASS' 24 | and s_nationkey = n_nationkey 25 | and n_regionkey = r_regionkey 26 | and r_name = 'EUROPE' 27 | and ps_supplycost = ( 28 | select 29 | min(ps_supplycost) 30 | from 31 | partsupp, 32 | supplier, 33 | nation, 34 | region 35 | where 36 | p_partkey = ps_partkey 37 | and s_suppkey = ps_suppkey 38 | and s_nationkey = n_nationkey 39 | and n_regionkey = r_regionkey 40 | and r_name = 'EUROPE' 41 | ) 42 | order by 43 | s_acctbal desc, 44 | n_name, 45 | s_name, 46 | p_partkey; 47 | -------------------------------------------------------------------------------- /tests/queries/tpch/21.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | s_name, 6 | count(*) as numwait 7 | from 8 | supplier, 9 | lineitem l1, 10 | orders, 11 | nation 12 | where 13 | s_suppkey = l1.l_suppkey 14 | and o_orderkey = l1.l_orderkey 15 | and o_orderstatus = 'F' 16 | and l1.l_receiptdate > l1.l_commitdate 17 | and exists ( 18 | select 19 | * 20 | from 21 | lineitem l2 22 | where 23 | l2.l_orderkey = l1.l_orderkey 24 | and l2.l_suppkey <> l1.l_suppkey 25 | ) 26 | and not exists ( 27 | select 28 | * 29 | from 30 | lineitem l3 31 | where 32 | l3.l_orderkey = l1.l_orderkey 33 | and l3.l_suppkey <> l1.l_suppkey 34 | and l3.l_receiptdate > l3.l_commitdate 35 | ) 36 | and s_nationkey = n_nationkey 37 | and n_name = 'SAUDI ARABIA' 38 | group by 39 | s_name 40 | order by 41 | numwait desc, 42 | s_name; 43 | -------------------------------------------------------------------------------- /docs/fuzzing.md: -------------------------------------------------------------------------------- 1 | # Fuzzing 2 | 3 | ## Installing `honggfuzz` 4 | 5 | ``` 6 | cargo install honggfuzz 7 | ``` 8 | 9 | Install [dependencies](https://github.com/rust-fuzz/honggfuzz-rs#dependencies) for your system. 10 | 11 | ## Running the fuzzer 12 | 13 | Running the fuzzer is as easy as running in the `fuzz` directory. 14 | 15 | Choose a target: 16 | 17 | These are `[[bin]]` entries in `Cargo.toml`. 18 | List them with `cargo read-manifest | jq '.targets[].name'` from the `fuzz` directory. 19 | 20 | Run the fuzzer: 21 | 22 | ```shell 23 | cd fuzz 24 | cargo hfuzz run 25 | ``` 26 | 27 | After a panic is found, get a stack trace with: 28 | 29 | ```shell 30 | cargo hfuzz run-debug hfuzz_workspace//*.fuzz 31 | ``` 32 | 33 | For example, with the `fuzz_parse_sql` target: 34 | 35 | ```shell 36 | cargo hfuzz run fuzz_parse_sql 37 | cargo hfuzz run-debug fuzz_parse_sql hfuzz_workspace/fuzz_parse_sql/*.fuzz 38 | ``` 39 | -------------------------------------------------------------------------------- /tests/queries/tpch/8.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | o_year, 6 | sum(case 7 | when nation = 'BRAZIL' then volume 8 | else 0 9 | end) / sum(volume) as mkt_share 10 | from 11 | ( 12 | select 13 | extract(year from o_orderdate) as o_year, 14 | l_extendedprice * (1 - l_discount) as volume, 15 | n2.n_name as nation 16 | from 17 | part, 18 | supplier, 19 | lineitem, 20 | orders, 21 | customer, 22 | nation n1, 23 | nation n2, 24 | region 25 | where 26 | p_partkey = l_partkey 27 | and s_suppkey = l_suppkey 28 | and l_orderkey = o_orderkey 29 | and o_custkey = c_custkey 30 | and c_nationkey = n1.n_nationkey 31 | and n1.n_regionkey = r_regionkey 32 | and r_name = 'AMERICA' 33 | and s_nationkey = n2.n_nationkey 34 | and o_orderdate between date '1995-01-01' and date '1996-12-31' 35 | and p_type = 'ECONOMY ANODIZED STEEL' 36 | ) as all_nations 37 | group by 38 | o_year 39 | order by 40 | o_year; 41 | -------------------------------------------------------------------------------- /tests/queries/tpch/7.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | supp_nation, 6 | cust_nation, 7 | l_year, 8 | sum(volume) as revenue 9 | from 10 | ( 11 | select 12 | n1.n_name as supp_nation, 13 | n2.n_name as cust_nation, 14 | extract(year from l_shipdate) as l_year, 15 | l_extendedprice * (1 - l_discount) as volume 16 | from 17 | supplier, 18 | lineitem, 19 | orders, 20 | customer, 21 | nation n1, 22 | nation n2 23 | where 24 | s_suppkey = l_suppkey 25 | and o_orderkey = l_orderkey 26 | and c_custkey = o_custkey 27 | and s_nationkey = n1.n_nationkey 28 | and c_nationkey = n2.n_nationkey 29 | and ( 30 | (n1.n_name = 'FRANCE' and n2.n_name = 'GERMANY') 31 | or (n1.n_name = 'GERMANY' and n2.n_name = 'FRANCE') 32 | ) 33 | and l_shipdate between date '1995-01-01' and date '1996-12-31' 34 | ) as shipping 35 | group by 36 | supp_nation, 37 | cust_nation, 38 | l_year 39 | order by 40 | supp_nation, 41 | cust_nation, 42 | l_year; 43 | -------------------------------------------------------------------------------- /examples/parse_select.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #![warn(clippy::all)] 14 | 15 | use sqlparser::dialect::GenericDialect; 16 | use sqlparser::parser::*; 17 | 18 | fn main() { 19 | let sql = "SELECT a, b, 123, myfunc(b) \ 20 | FROM table_1 \ 21 | WHERE a > b AND b < 100 \ 22 | ORDER BY a DESC, b"; 23 | 24 | let dialect = GenericDialect {}; 25 | 26 | let ast = Parser::parse_sql(&dialect, sql).unwrap(); 27 | 28 | println!("AST: {:?}", ast); 29 | } 30 | -------------------------------------------------------------------------------- /src/dialect/ansi.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use crate::dialect::Dialect; 14 | 15 | #[derive(Debug)] 16 | pub struct AnsiDialect {} 17 | 18 | impl Dialect for AnsiDialect { 19 | fn is_identifier_start(&self, ch: char) -> bool { 20 | ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) 21 | } 22 | 23 | fn is_identifier_part(&self, ch: char) -> bool { 24 | ('a'..='z').contains(&ch) 25 | || ('A'..='Z').contains(&ch) 26 | || ('0'..='9').contains(&ch) 27 | || ch == '_' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/queries/tpch/19.sql: -------------------------------------------------------------------------------- 1 | -- using default substitutions 2 | 3 | 4 | select 5 | sum(l_extendedprice* (1 - l_discount)) as revenue 6 | from 7 | lineitem, 8 | part 9 | where 10 | ( 11 | p_partkey = l_partkey 12 | and p_brand = 'Brand#12' 13 | and p_container in ('SM CASE', 'SM BOX', 'SM PACK', 'SM PKG') 14 | and l_quantity >= 1 and l_quantity <= 1 + 10 15 | and p_size between 1 and 5 16 | and l_shipmode in ('AIR', 'AIR REG') 17 | and l_shipinstruct = 'DELIVER IN PERSON' 18 | ) 19 | or 20 | ( 21 | p_partkey = l_partkey 22 | and p_brand = 'Brand#23' 23 | and p_container in ('MED BAG', 'MED BOX', 'MED PKG', 'MED PACK') 24 | and l_quantity >= 10 and l_quantity <= 10 + 10 25 | and p_size between 1 and 10 26 | and l_shipmode in ('AIR', 'AIR REG') 27 | and l_shipinstruct = 'DELIVER IN PERSON' 28 | ) 29 | or 30 | ( 31 | p_partkey = l_partkey 32 | and p_brand = 'Brand#34' 33 | and p_container in ('LG CASE', 'LG BOX', 'LG PACK', 'LG PKG') 34 | and l_quantity >= 20 and l_quantity <= 20 + 10 35 | and p_size between 1 and 15 36 | and l_shipmode in ('AIR', 'AIR REG') 37 | and l_shipinstruct = 'DELIVER IN PERSON' 38 | ); 39 | -------------------------------------------------------------------------------- /src/dialect/snowflake.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use crate::dialect::Dialect; 14 | 15 | #[derive(Debug, Default)] 16 | pub struct SnowflakeDialect; 17 | 18 | impl Dialect for SnowflakeDialect { 19 | // see https://docs.snowflake.com/en/sql-reference/identifiers-syntax.html 20 | fn is_identifier_start(&self, ch: char) -> bool { 21 | ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' 22 | } 23 | 24 | fn is_identifier_part(&self, ch: char) -> bool { 25 | ('a'..='z').contains(&ch) 26 | || ('A'..='Z').contains(&ch) 27 | || ('0'..='9').contains(&ch) 28 | || ch == '$' 29 | || ch == '_' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/dialect/generic.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use crate::dialect::Dialect; 14 | 15 | #[derive(Debug, Default)] 16 | pub struct GenericDialect; 17 | 18 | impl Dialect for GenericDialect { 19 | fn is_identifier_start(&self, ch: char) -> bool { 20 | ('a'..='z').contains(&ch) 21 | || ('A'..='Z').contains(&ch) 22 | || ch == '_' 23 | || ch == '#' 24 | || ch == '@' 25 | } 26 | 27 | fn is_identifier_part(&self, ch: char) -> bool { 28 | ('a'..='z').contains(&ch) 29 | || ('A'..='Z').contains(&ch) 30 | || ('0'..='9').contains(&ch) 31 | || ch == '@' 32 | || ch == '$' 33 | || ch == '#' 34 | || ch == '_' 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/dialect/postgresql.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use crate::dialect::Dialect; 14 | 15 | #[derive(Debug)] 16 | pub struct PostgreSqlDialect {} 17 | 18 | impl Dialect for PostgreSqlDialect { 19 | fn is_identifier_start(&self, ch: char) -> bool { 20 | // See https://www.postgresql.org/docs/11/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS 21 | // We don't yet support identifiers beginning with "letters with 22 | // diacritical marks and non-Latin letters" 23 | ('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_' 24 | } 25 | 26 | fn is_identifier_part(&self, ch: char) -> bool { 27 | ('a'..='z').contains(&ch) 28 | || ('A'..='Z').contains(&ch) 29 | || ('0'..='9').contains(&ch) 30 | || ch == '$' 31 | || ch == '_' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/dialect/hive.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use crate::dialect::Dialect; 14 | 15 | #[derive(Debug)] 16 | pub struct HiveDialect {} 17 | 18 | impl Dialect for HiveDialect { 19 | fn is_delimited_identifier_start(&self, ch: char) -> bool { 20 | (ch == '"') || (ch == '`') 21 | } 22 | 23 | fn is_identifier_start(&self, ch: char) -> bool { 24 | ('a'..='z').contains(&ch) 25 | || ('A'..='Z').contains(&ch) 26 | || ('0'..='9').contains(&ch) 27 | || ch == '$' 28 | } 29 | 30 | fn is_identifier_part(&self, ch: char) -> bool { 31 | ('a'..='z').contains(&ch) 32 | || ('A'..='Z').contains(&ch) 33 | || ('0'..='9').contains(&ch) 34 | || ch == '_' 35 | || ch == '$' 36 | || ch == '{' 37 | || ch == '}' 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/dialect/mysql.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use crate::dialect::Dialect; 14 | 15 | #[derive(Debug)] 16 | pub struct MySqlDialect {} 17 | 18 | impl Dialect for MySqlDialect { 19 | fn is_identifier_start(&self, ch: char) -> bool { 20 | // See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html. 21 | // We don't yet support identifiers beginning with numbers, as that 22 | // makes it hard to distinguish numeric literals. 23 | ('a'..='z').contains(&ch) 24 | || ('A'..='Z').contains(&ch) 25 | || ch == '_' 26 | || ch == '$' 27 | || ('\u{0080}'..='\u{ffff}').contains(&ch) 28 | } 29 | 30 | fn is_identifier_part(&self, ch: char) -> bool { 31 | self.is_identifier_start(ch) || ('0'..='9').contains(&ch) 32 | } 33 | 34 | fn is_delimited_identifier_start(&self, ch: char) -> bool { 35 | ch == '`' 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlparser" 3 | description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" 4 | version = "0.13.1-alpha.0" 5 | authors = ["Andy Grove "] 6 | homepage = "https://github.com/sqlparser-rs/sqlparser-rs" 7 | documentation = "https://docs.rs/sqlparser/" 8 | keywords = [ "ansi", "sql", "lexer", "parser" ] 9 | repository = "https://github.com/sqlparser-rs/sqlparser-rs" 10 | license = "Apache-2.0" 11 | include = [ 12 | "src/**/*.rs", 13 | "Cargo.toml", 14 | ] 15 | edition = "2021" 16 | 17 | [lib] 18 | name = "sqlparser" 19 | path = "src/lib.rs" 20 | 21 | [features] 22 | default = ["std"] 23 | std = [] 24 | # Enable JSON output in the `cli` example: 25 | json_example = ["serde_json", "serde"] 26 | 27 | [dependencies] 28 | bigdecimal = { version = "0.3", features = ["serde"], optional = true } 29 | log = "0.4" 30 | hashbrown = "0.12.0" 31 | serde = { version = "1.0", features = ["derive"], optional = true } 32 | # serde_json is only used in examples/cli, but we have to put it outside 33 | # of dev-dependencies because of 34 | # https://github.com/rust-lang/cargo/issues/1596 35 | serde_json = { version = "1.0", optional = true } 36 | 37 | [dev-dependencies] 38 | simple_logger = "1.9" 39 | matches = "0.1" 40 | 41 | [package.metadata.release] 42 | # Instruct `cargo release` to not run `cargo publish` locally: 43 | # https://github.com/sunng87/cargo-release/blob/master/docs/reference.md#config-fields 44 | # See docs/releasing.md for details. 45 | disable-publish = true 46 | -------------------------------------------------------------------------------- /src/dialect/sqlite.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use crate::dialect::Dialect; 14 | 15 | #[derive(Debug)] 16 | pub struct SQLiteDialect {} 17 | 18 | impl Dialect for SQLiteDialect { 19 | // see https://www.sqlite.org/lang_keywords.html 20 | // parse `...`, [...] and "..." as identifier 21 | // TODO: support depending on the context tread '...' as identifier too. 22 | fn is_delimited_identifier_start(&self, ch: char) -> bool { 23 | ch == '`' || ch == '"' || ch == '[' 24 | } 25 | 26 | fn is_identifier_start(&self, ch: char) -> bool { 27 | // See https://www.sqlite.org/draft/tokenreq.html 28 | ('a'..='z').contains(&ch) 29 | || ('A'..='Z').contains(&ch) 30 | || ch == '_' 31 | || ch == '$' 32 | || ('\u{007f}'..='\u{ffff}').contains(&ch) 33 | } 34 | 35 | fn is_identifier_part(&self, ch: char) -> bool { 36 | self.is_identifier_start(ch) || ('0'..='9').contains(&ch) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/test_utils/mod.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | // Re-export everything from `src/test_utils.rs`. 14 | pub use sqlparser::test_utils::*; 15 | 16 | // For the test-only macros we take a different approach of keeping them here 17 | // rather than in the library crate. 18 | // 19 | // This is because we don't need any of them to be shared between the 20 | // integration tests (i.e. `tests/*`) and the unit tests (i.e. `src/*`), 21 | // but also because Rust doesn't scope macros to a particular module 22 | // (and while we export internal helpers as sqlparser::test_utils::<...>, 23 | // expecting our users to abstain from relying on them, exporting internal 24 | // macros at the top level, like `sqlparser::nest` was deemed too confusing). 25 | 26 | #[macro_export] 27 | macro_rules! nest { 28 | ($base:expr $(, $join:expr)*) => { 29 | TableFactor::NestedJoin(Box::new(TableWithJoins { 30 | relation: $base, 31 | joins: vec![$(join($join)),*] 32 | })) 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/dialect/mssql.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use crate::dialect::Dialect; 14 | 15 | #[derive(Debug)] 16 | pub struct MsSqlDialect {} 17 | 18 | impl Dialect for MsSqlDialect { 19 | fn is_delimited_identifier_start(&self, ch: char) -> bool { 20 | ch == '"' || ch == '[' 21 | } 22 | 23 | fn is_identifier_start(&self, ch: char) -> bool { 24 | // See https://docs.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-2017#rules-for-regular-identifiers 25 | // We don't support non-latin "letters" currently. 26 | ('a'..='z').contains(&ch) 27 | || ('A'..='Z').contains(&ch) 28 | || ch == '_' 29 | || ch == '#' 30 | || ch == '@' 31 | } 32 | 33 | fn is_identifier_part(&self, ch: char) -> bool { 34 | ('a'..='z').contains(&ch) 35 | || ('A'..='Z').contains(&ch) 36 | || ('0'..='9').contains(&ch) 37 | || ch == '@' 38 | || ch == '$' 39 | || ch == '#' 40 | || ch == '_' 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sqlparser_bench/benches/sqlparser_bench.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use criterion::{criterion_group, criterion_main, Criterion}; 14 | use sqlparser::dialect::GenericDialect; 15 | use sqlparser::parser::Parser; 16 | 17 | fn basic_queries(c: &mut Criterion) { 18 | let mut group = c.benchmark_group("sqlparser-rs parsing benchmark"); 19 | let dialect = GenericDialect {}; 20 | 21 | let string = "SELECT * FROM table WHERE 1 = 1"; 22 | group.bench_function("sqlparser::select", |b| { 23 | b.iter(|| Parser::parse_sql(&dialect, string)); 24 | }); 25 | 26 | let with_query = " 27 | WITH derived AS ( 28 | SELECT MAX(a) AS max_a, 29 | COUNT(b) AS b_num, 30 | user_id 31 | FROM TABLE 32 | GROUP BY user_id 33 | ) 34 | SELECT * FROM table 35 | LEFT JOIN derived USING (user_id) 36 | "; 37 | group.bench_function("sqlparser::with_select", |b| { 38 | b.iter(|| Parser::parse_sql(&dialect, with_query)); 39 | }); 40 | } 41 | 42 | criterion_group!(benches, basic_queries); 43 | criterion_main!(benches); 44 | -------------------------------------------------------------------------------- /tests/sqlparser_regression.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #![warn(clippy::all)] 14 | 15 | use sqlparser::dialect::GenericDialect; 16 | use sqlparser::parser::Parser; 17 | 18 | macro_rules! tpch_tests { 19 | ($($name:ident: $value:expr,)*) => { 20 | const QUERIES: &[&str] = &[ 21 | $(include_str!(concat!("queries/tpch/", $value, ".sql"))),* 22 | ]; 23 | $( 24 | 25 | #[test] 26 | fn $name() { 27 | let dialect = GenericDialect {}; 28 | let res = Parser::parse_sql(&dialect, QUERIES[$value -1]); 29 | assert!(res.is_ok()); 30 | } 31 | )* 32 | } 33 | } 34 | 35 | tpch_tests! { 36 | tpch_1: 1, 37 | tpch_2: 2, 38 | tpch_3: 3, 39 | tpch_4: 4, 40 | tpch_5: 5, 41 | tpch_6: 6, 42 | tpch_7: 7, 43 | tpch_8: 8, 44 | tpch_9: 9, 45 | tpch_10: 10, 46 | tpch_11: 11, 47 | tpch_12: 12, 48 | tpch_13: 13, 49 | tpch_14: 14, 50 | tpch_15: 15, 51 | tpch_16: 16, 52 | tpch_17: 17, 53 | tpch_18: 18, 54 | tpch_19: 19, 55 | tpch_20: 20, 56 | tpch_21: 21, 57 | tpch_22: 22, 58 | } 59 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | //! SQL Parser for Rust 14 | //! 15 | //! Example code: 16 | //! 17 | //! This crate provides an ANSI:SQL 2011 lexer and parser that can parse SQL 18 | //! into an Abstract Syntax Tree (AST). 19 | //! 20 | //! ``` 21 | //! use sqlparser::dialect::GenericDialect; 22 | //! use sqlparser::parser::Parser; 23 | //! 24 | //! let dialect = GenericDialect {}; // or AnsiDialect 25 | //! 26 | //! let sql = "SELECT a, b, 123, myfunc(b) \ 27 | //! FROM table_1 \ 28 | //! WHERE a > b AND b < 100 \ 29 | //! ORDER BY a DESC, b"; 30 | //! 31 | //! let ast = Parser::parse_sql(&dialect, sql).unwrap(); 32 | //! 33 | //! println!("AST: {:?}", ast); 34 | //! ``` 35 | 36 | #![cfg_attr(not(feature = "std"), no_std)] 37 | #![allow(clippy::upper_case_acronyms)] 38 | 39 | #[cfg(not(feature = "std"))] 40 | extern crate alloc; 41 | 42 | pub mod ast; 43 | #[macro_use] 44 | pub mod dialect; 45 | pub mod keywords; 46 | pub mod parser; 47 | pub mod tokenizer; 48 | 49 | #[doc(hidden)] 50 | // This is required to make utilities accessible by both the crate-internal 51 | // unit-tests and by the integration tests 52 | // External users are not supposed to rely on this module. 53 | pub mod test_utils; 54 | -------------------------------------------------------------------------------- /docs/releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | ## Prerequisites 4 | Publishing to crates.io has been automated via GitHub Actions, so you will only 5 | need push access to the [sqlparser-rs GitHub repository](https://github.com/sqlparser-rs/sqlparser-rs) 6 | in order to publish a release. 7 | 8 | We use the [`cargo release`](https://github.com/sunng87/cargo-release) 9 | subcommand to ensure correct versioning. Install via: 10 | 11 | ``` 12 | $ cargo install cargo-release 13 | ``` 14 | 15 | ## Process 16 | 17 | 1. **Before releasing** ensure `CHANGELOG.md` is updated appropriately and that 18 | you have a clean checkout of the `main` branch of the sqlparser repository: 19 | ``` 20 | $ git fetch && git status 21 | On branch main 22 | Your branch is up to date with 'upstream/main'. 23 | 24 | nothing to commit, working tree clean 25 | ``` 26 | * If you have the time, check that the examples in the README are up to date. 27 | 28 | 2. Using `cargo-release` we can publish a new release like so: 29 | 30 | ``` 31 | $ cargo release minor --push-remote upstream 32 | ``` 33 | 34 | After verifying, you can rerun with `--execute` if all looks good. 35 | You can add `--no-push` to stop before actually publishing the release. 36 | 37 | `cargo release` will then: 38 | 39 | * Bump the minor part of the version in `Cargo.toml` (e.g. `0.7.1-alpha.0` 40 | -> `0.8.0`. You can use `patch` instead of `minor`, as appropriate). 41 | * Create a new tag (e.g. `v0.8.0`) locally 42 | * Push the new tag to the specified remote (`upstream` in the above 43 | example), which will trigger a publishing process to crates.io as part of 44 | the [corresponding GitHub Action](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/.github/workflows/rust.yml). 45 | 46 | Note that credentials for authoring in this way are securely stored in 47 | the (GitHub) repo secrets as `CRATE_TOKEN`. 48 | * Bump the crate version again (to something like `0.8.1-alpha.0`) to 49 | indicate the start of new development cycle. 50 | 51 | 3. Push the updates to the `main` branch upstream: 52 | ``` 53 | $ git push upstream 54 | ``` 55 | 56 | 4. Check that the new version of the crate is available on crates.io: 57 | https://crates.io/crates/sqlparser 58 | 59 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | codestyle: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Set up Rust 11 | uses: hecrj/setup-rust-action@v1 12 | with: 13 | components: rustfmt 14 | # Note that `nightly` is required for `license_template_path`, as 15 | # it's an unstable feature. 16 | rust-version: nightly 17 | - uses: actions/checkout@v2 18 | - run: cargo +nightly fmt -- --check --config-path <(echo 'license_template_path = "HEADER"') 19 | 20 | lint: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Set up Rust 24 | uses: hecrj/setup-rust-action@v1 25 | with: 26 | components: clippy 27 | - uses: actions/checkout@v2 28 | - run: cargo clippy --all-targets --all-features -- -D warnings 29 | 30 | compile: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Set up Rust 34 | uses: hecrj/setup-rust-action@v1 35 | - uses: actions/checkout@master 36 | - run: cargo check --all-targets --all-features 37 | 38 | compile-no-std: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Set up Rust 42 | uses: hecrj/setup-rust-action@v1 43 | with: 44 | targets: 'thumbv6m-none-eabi' 45 | - uses: actions/checkout@master 46 | - run: cargo check --no-default-features --target thumbv6m-none-eabi 47 | 48 | test: 49 | strategy: 50 | matrix: 51 | rust: [stable, beta, nightly] 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Setup Rust 55 | uses: hecrj/setup-rust-action@v1 56 | with: 57 | rust-version: ${{ matrix.rust }} 58 | - name: Install Tarpaulin 59 | uses: actions-rs/install@v0.1 60 | with: 61 | crate: cargo-tarpaulin 62 | version: 0.14.2 63 | use-tool-cache: true 64 | - name: Checkout 65 | uses: actions/checkout@v2 66 | - name: Test 67 | run: cargo test --all-features 68 | - name: Coverage 69 | if: matrix.rust == 'stable' 70 | run: cargo tarpaulin -o Lcov --output-dir ./coverage 71 | - name: Coveralls 72 | if: matrix.rust == 'stable' 73 | uses: coverallsapp/github-action@master 74 | with: 75 | github-token: ${{ secrets.GITHUB_TOKEN }} 76 | 77 | publish-crate: 78 | if: startsWith(github.ref, 'refs/tags/v0') 79 | runs-on: ubuntu-latest 80 | needs: [test] 81 | steps: 82 | - name: Set up Rust 83 | uses: hecrj/setup-rust-action@v1 84 | - uses: actions/checkout@v2 85 | - name: Publish 86 | shell: bash 87 | run: | 88 | cargo publish --token ${{ secrets.CRATES_TOKEN }} 89 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | sudo: required 3 | cache: cargo 4 | language: rust 5 | addons: 6 | apt: 7 | packages: 8 | - kcov 9 | - libcurl4-openssl-dev 10 | - libelf-dev 11 | - libdw-dev 12 | - binutils-dev 13 | - cmake 14 | sources: 15 | - kalakris-cmake 16 | # The version of kcov shipped with Xenial (v25) doesn't support the 17 | # --verify option that `cargo coveralls` passes. This PPA has a more 18 | # up-to-date version. It can be removed if Ubuntu ever ships a newer 19 | # version, or replaced with another PPA if this one falls out of date. 20 | - sourceline: ppa:sivakov512/kcov 21 | 22 | rust: 23 | - stable 24 | 25 | before_script: 26 | # Travis installs rust with a non-default "minimal" profile, and advises us 27 | # to add clippy manually: 28 | - rustup component add clippy 29 | # Unfortunately, this method often breaks on the nightly channel, where the 30 | # most recent build might not have all the optional components. 31 | # We explicitly specify `--profile default` to obtain the most recent nightly 32 | # that has rustfmt (we don't care if it's a week old, as we need it only for 33 | # an experimental flag): 34 | - rustup toolchain install nightly --profile default 35 | 36 | - pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH 37 | - export PATH=$HOME/.cargo/bin:$PATH 38 | # `cargo install` fails if the specified binary is already installed, and 39 | # doesn't yet support a `--if-not-installed` option [0], so for now assume 40 | # failures mean the package is already installed. If installation truly 41 | # failed, the build will fail later anyway, when we try to use the installed 42 | # binary. Note that `cargo install --force` is not a solution, as it always 43 | # rebuilds from scratch, ignoring the cache entirely. 44 | # 45 | # [0]: https://github.com/rust-lang/cargo/issues/2082 46 | - cargo install cargo-update || echo "cargo-update already installed" # for `cargo install-update` 47 | - cargo install cargo-travis || echo "cargo-travis already installed" # for `cargo coveralls` 48 | - cargo install-update -a # updates cargo-travis, if the cached version is outdated 49 | 50 | script: 51 | # Clippy must be run first, as its lints are only triggered during 52 | # compilation. Put another way: after a successful `cargo build`, `cargo 53 | # clippy` is guaranteed to produce no results. This bug is known upstream: 54 | # https://github.com/rust-lang/rust-clippy/issues/2604. 55 | - travis-cargo clippy -- --all-targets --all-features -- -D warnings 56 | - travis-cargo build 57 | - travis-cargo test 58 | - travis-cargo test -- all-features 59 | # The license_template_path setting we use to verify copyright headers is 60 | # only available on the nightly rustfmt. 61 | - cargo +nightly fmt -- --check --config-path <(echo 'license_template_path = "HEADER"') 62 | 63 | after_success: 64 | - cargo coveralls --verbose 65 | 66 | env: 67 | global: 68 | - TRAVIS_CARGO_NIGHTLY_FEATURE="" 69 | -------------------------------------------------------------------------------- /examples/cli.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #![warn(clippy::all)] 14 | 15 | /// A small command-line app to run the parser. 16 | /// Run with `cargo run --example cli` 17 | use std::fs; 18 | 19 | use simple_logger::SimpleLogger; 20 | use sqlparser::dialect::*; 21 | use sqlparser::parser::Parser; 22 | 23 | fn main() { 24 | SimpleLogger::new().init().unwrap(); 25 | 26 | let filename = std::env::args().nth(1).expect( 27 | r#" 28 | No arguments provided! 29 | 30 | Usage: 31 | $ cargo run --example cli FILENAME.sql [--dialectname] 32 | 33 | To print the parse results as JSON: 34 | $ cargo run --feature json_example --example cli FILENAME.sql [--dialectname] 35 | 36 | "#, 37 | ); 38 | 39 | let dialect: Box = match std::env::args().nth(2).unwrap_or_default().as_ref() { 40 | "--ansi" => Box::new(AnsiDialect {}), 41 | "--postgres" => Box::new(PostgreSqlDialect {}), 42 | "--ms" => Box::new(MsSqlDialect {}), 43 | "--mysql" => Box::new(MySqlDialect {}), 44 | "--snowflake" => Box::new(SnowflakeDialect {}), 45 | "--hive" => Box::new(HiveDialect {}), 46 | "--generic" | "" => Box::new(GenericDialect {}), 47 | s => panic!("Unexpected parameter: {}", s), 48 | }; 49 | 50 | println!("Parsing from file '{}' using {:?}", &filename, dialect); 51 | let contents = fs::read_to_string(&filename) 52 | .unwrap_or_else(|_| panic!("Unable to read the file {}", &filename)); 53 | let without_bom = if contents.chars().next().unwrap() as u64 != 0xfeff { 54 | contents.as_str() 55 | } else { 56 | let mut chars = contents.chars(); 57 | chars.next(); 58 | chars.as_str() 59 | }; 60 | let parse_result = Parser::parse_sql(&*dialect, without_bom); 61 | match parse_result { 62 | Ok(statements) => { 63 | println!( 64 | "Round-trip:\n'{}'", 65 | statements 66 | .iter() 67 | .map(std::string::ToString::to_string) 68 | .collect::>() 69 | .join("\n") 70 | ); 71 | 72 | if cfg!(feature = "json_example") { 73 | #[cfg(feature = "json_example")] 74 | { 75 | let serialized = serde_json::to_string_pretty(&statements).unwrap(); 76 | println!("Serialized as JSON:\n{}", serialized); 77 | } 78 | } else { 79 | println!("Parse results:\n{:#?}", statements); 80 | } 81 | 82 | std::process::exit(0); 83 | } 84 | Err(e) => { 85 | println!("Error during parsing: {:?}", e); 86 | std::process::exit(1); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/dialect/mod.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | mod ansi; 14 | mod generic; 15 | mod hive; 16 | mod mssql; 17 | mod mysql; 18 | mod postgresql; 19 | mod snowflake; 20 | mod sqlite; 21 | 22 | use core::any::{Any, TypeId}; 23 | use core::fmt::Debug; 24 | 25 | pub use self::ansi::AnsiDialect; 26 | pub use self::generic::GenericDialect; 27 | pub use self::hive::HiveDialect; 28 | pub use self::mssql::MsSqlDialect; 29 | pub use self::mysql::MySqlDialect; 30 | pub use self::postgresql::PostgreSqlDialect; 31 | pub use self::snowflake::SnowflakeDialect; 32 | pub use self::sqlite::SQLiteDialect; 33 | pub use crate::keywords; 34 | 35 | /// `dialect_of!(parser is SQLiteDialect | GenericDialect)` evaluates 36 | /// to `true` iff `parser.dialect` is one of the `Dialect`s specified. 37 | macro_rules! dialect_of { 38 | ( $parsed_dialect: ident is $($dialect_type: ty)|+ ) => { 39 | ($($parsed_dialect.dialect.is::<$dialect_type>())||+) 40 | }; 41 | } 42 | 43 | pub trait Dialect: Debug + Any { 44 | /// Determine if a character starts a quoted identifier. The default 45 | /// implementation, accepting "double quoted" ids is both ANSI-compliant 46 | /// and appropriate for most dialects (with the notable exception of 47 | /// MySQL, MS SQL, and sqlite). You can accept one of characters listed 48 | /// in `Word::matching_end_quote` here 49 | fn is_delimited_identifier_start(&self, ch: char) -> bool { 50 | ch == '"' 51 | } 52 | /// Determine if a character is a valid start character for an unquoted identifier 53 | fn is_identifier_start(&self, ch: char) -> bool; 54 | /// Determine if a character is a valid unquoted identifier character 55 | fn is_identifier_part(&self, ch: char) -> bool; 56 | } 57 | 58 | impl dyn Dialect { 59 | #[inline] 60 | pub fn is(&self) -> bool { 61 | // borrowed from `Any` implementation 62 | TypeId::of::() == self.type_id() 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::ansi::AnsiDialect; 69 | use super::generic::GenericDialect; 70 | use super::*; 71 | 72 | struct DialectHolder<'a> { 73 | dialect: &'a dyn Dialect, 74 | } 75 | 76 | #[test] 77 | fn test_is_dialect() { 78 | let generic_dialect: &dyn Dialect = &GenericDialect {}; 79 | let ansi_dialect: &dyn Dialect = &AnsiDialect {}; 80 | 81 | let generic_holder = DialectHolder { 82 | dialect: generic_dialect, 83 | }; 84 | let ansi_holder = DialectHolder { 85 | dialect: ansi_dialect, 86 | }; 87 | 88 | assert!(dialect_of!(generic_holder is GenericDialect | AnsiDialect),); 89 | assert!(!dialect_of!(generic_holder is AnsiDialect)); 90 | assert!(dialect_of!(ansi_holder is AnsiDialect)); 91 | assert!(dialect_of!(ansi_holder is GenericDialect | AnsiDialect)); 92 | assert!(!dialect_of!(ansi_holder is GenericDialect | MsSqlDialect)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/sqlparser_mssql.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #![warn(clippy::all)] 14 | //! Test SQL syntax specific to Microsoft's T-SQL. The parser based on the 15 | //! generic dialect is also tested (on the inputs it can handle). 16 | 17 | #[macro_use] 18 | mod test_utils; 19 | use test_utils::*; 20 | 21 | use sqlparser::ast::*; 22 | use sqlparser::dialect::{GenericDialect, MsSqlDialect}; 23 | 24 | #[test] 25 | fn parse_mssql_identifiers() { 26 | let sql = "SELECT @@version, _foo$123 FROM ##temp"; 27 | let select = ms_and_generic().verified_only_select(sql); 28 | assert_eq!( 29 | &Expr::Identifier(Ident::new("@@version")), 30 | expr_from_projection(&select.projection[0]), 31 | ); 32 | assert_eq!( 33 | &Expr::Identifier(Ident::new("_foo$123")), 34 | expr_from_projection(&select.projection[1]), 35 | ); 36 | assert_eq!(2, select.projection.len()); 37 | match &only(&select.from).relation { 38 | TableFactor::Table { name, .. } => { 39 | assert_eq!("##temp".to_string(), name.to_string()); 40 | } 41 | _ => unreachable!(), 42 | }; 43 | } 44 | 45 | #[test] 46 | fn parse_mssql_single_quoted_aliases() { 47 | let _ = ms_and_generic().one_statement_parses_to("SELECT foo 'alias'", "SELECT foo AS 'alias'"); 48 | } 49 | 50 | #[test] 51 | fn parse_mssql_delimited_identifiers() { 52 | let _ = ms().one_statement_parses_to( 53 | "SELECT [a.b!] [FROM] FROM foo [WHERE]", 54 | "SELECT [a.b!] AS [FROM] FROM foo AS [WHERE]", 55 | ); 56 | } 57 | 58 | #[test] 59 | fn parse_mssql_apply_join() { 60 | let _ = ms_and_generic().verified_only_select( 61 | "SELECT * FROM sys.dm_exec_query_stats AS deqs \ 62 | CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle)", 63 | ); 64 | let _ = ms_and_generic().verified_only_select( 65 | "SELECT * FROM sys.dm_exec_query_stats AS deqs \ 66 | OUTER APPLY sys.dm_exec_query_plan(deqs.plan_handle)", 67 | ); 68 | let _ = ms_and_generic().verified_only_select( 69 | "SELECT * FROM foo \ 70 | OUTER APPLY (SELECT foo.x + 1) AS bar", 71 | ); 72 | } 73 | 74 | #[test] 75 | fn parse_mssql_top_paren() { 76 | let sql = "SELECT TOP (5) * FROM foo"; 77 | let select = ms_and_generic().verified_only_select(sql); 78 | let top = select.top.unwrap(); 79 | assert_eq!(Some(Expr::Value(number("5"))), top.quantity); 80 | assert!(!top.percent); 81 | } 82 | 83 | #[test] 84 | fn parse_mssql_top_percent() { 85 | let sql = "SELECT TOP (5) PERCENT * FROM foo"; 86 | let select = ms_and_generic().verified_only_select(sql); 87 | let top = select.top.unwrap(); 88 | assert_eq!(Some(Expr::Value(number("5"))), top.quantity); 89 | assert!(top.percent); 90 | } 91 | 92 | #[test] 93 | fn parse_mssql_top_with_ties() { 94 | let sql = "SELECT TOP (5) WITH TIES * FROM foo"; 95 | let select = ms_and_generic().verified_only_select(sql); 96 | let top = select.top.unwrap(); 97 | assert_eq!(Some(Expr::Value(number("5"))), top.quantity); 98 | assert!(top.with_ties); 99 | } 100 | 101 | #[test] 102 | fn parse_mssql_top_percent_with_ties() { 103 | let sql = "SELECT TOP (10) PERCENT WITH TIES * FROM foo"; 104 | let select = ms_and_generic().verified_only_select(sql); 105 | let top = select.top.unwrap(); 106 | assert_eq!(Some(Expr::Value(number("10"))), top.quantity); 107 | assert!(top.percent); 108 | } 109 | 110 | #[test] 111 | fn parse_mssql_top() { 112 | let sql = "SELECT TOP 5 bar, baz FROM foo"; 113 | let _ = ms_and_generic().one_statement_parses_to(sql, "SELECT TOP (5) bar, baz FROM foo"); 114 | } 115 | 116 | #[test] 117 | fn parse_mssql_bin_literal() { 118 | let _ = ms_and_generic().one_statement_parses_to("SELECT 0xdeadBEEF", "SELECT X'deadBEEF'"); 119 | } 120 | 121 | fn ms() -> TestedDialects { 122 | TestedDialects { 123 | dialects: vec![Box::new(MsSqlDialect {})], 124 | } 125 | } 126 | fn ms_and_generic() -> TestedDialects { 127 | TestedDialects { 128 | dialects: vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})], 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/ast/operator.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | use core::fmt; 14 | 15 | #[cfg(feature = "serde")] 16 | use serde::{Deserialize, Serialize}; 17 | 18 | /// Unary operators 19 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 20 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 21 | pub enum UnaryOperator { 22 | Plus, 23 | Minus, 24 | Not, 25 | /// Bitwise Not, e.g. `~9` (PostgreSQL-specific) 26 | PGBitwiseNot, 27 | /// Square root, e.g. `|/9` (PostgreSQL-specific) 28 | PGSquareRoot, 29 | /// Cube root, e.g. `||/27` (PostgreSQL-specific) 30 | PGCubeRoot, 31 | /// Factorial, e.g. `9!` (PostgreSQL-specific) 32 | PGPostfixFactorial, 33 | /// Factorial, e.g. `!!9` (PostgreSQL-specific) 34 | PGPrefixFactorial, 35 | /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) 36 | PGAbs, 37 | } 38 | 39 | impl fmt::Display for UnaryOperator { 40 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 41 | f.write_str(match self { 42 | UnaryOperator::Plus => "+", 43 | UnaryOperator::Minus => "-", 44 | UnaryOperator::Not => "NOT", 45 | UnaryOperator::PGBitwiseNot => "~", 46 | UnaryOperator::PGSquareRoot => "|/", 47 | UnaryOperator::PGCubeRoot => "||/", 48 | UnaryOperator::PGPostfixFactorial => "!", 49 | UnaryOperator::PGPrefixFactorial => "!!", 50 | UnaryOperator::PGAbs => "@", 51 | }) 52 | } 53 | } 54 | 55 | /// Binary operators 56 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 57 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 58 | pub enum BinaryOperator { 59 | Plus, 60 | Minus, 61 | Multiply, 62 | Divide, 63 | Div, 64 | Modulo, 65 | StringConcat, 66 | Gt, 67 | Lt, 68 | GtEq, 69 | LtEq, 70 | Spaceship, 71 | Eq, 72 | NotEq, 73 | And, 74 | Or, 75 | Xor, 76 | Like, 77 | NotLike, 78 | ILike, 79 | NotILike, 80 | Regexp, 81 | NotRegexp, 82 | RLike, 83 | NotRLike, 84 | BitwiseOr, 85 | BitwiseAnd, 86 | BitwiseXor, 87 | PGBitwiseXor, 88 | PGBitwiseShiftLeft, 89 | PGBitwiseShiftRight, 90 | PGRegexMatch, 91 | PGRegexIMatch, 92 | PGRegexNotMatch, 93 | PGRegexNotIMatch, 94 | } 95 | 96 | impl fmt::Display for BinaryOperator { 97 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 98 | f.write_str(match self { 99 | BinaryOperator::Plus => "+", 100 | BinaryOperator::Minus => "-", 101 | BinaryOperator::Multiply => "*", 102 | BinaryOperator::Divide => "/", 103 | BinaryOperator::Div => "DIV", 104 | BinaryOperator::Modulo => "%", 105 | BinaryOperator::StringConcat => "||", 106 | BinaryOperator::Gt => ">", 107 | BinaryOperator::Lt => "<", 108 | BinaryOperator::GtEq => ">=", 109 | BinaryOperator::LtEq => "<=", 110 | BinaryOperator::Spaceship => "<=>", 111 | BinaryOperator::Eq => "=", 112 | BinaryOperator::NotEq => "<>", 113 | BinaryOperator::And => "AND", 114 | BinaryOperator::Or => "OR", 115 | BinaryOperator::Xor => "XOR", 116 | BinaryOperator::Like => "LIKE", 117 | BinaryOperator::NotLike => "NOT LIKE", 118 | BinaryOperator::ILike => "ILIKE", 119 | BinaryOperator::NotILike => "NOT ILIKE", 120 | BinaryOperator::Regexp => "REGEXP", 121 | BinaryOperator::NotRegexp => "NOT REGEXP", 122 | BinaryOperator::RLike => "RLIKE", 123 | BinaryOperator::NotRLike => "NOT RLIKE", 124 | BinaryOperator::BitwiseOr => "|", 125 | BinaryOperator::BitwiseAnd => "&", 126 | BinaryOperator::BitwiseXor => "^", 127 | BinaryOperator::PGBitwiseXor => "#", 128 | BinaryOperator::PGBitwiseShiftLeft => "<<", 129 | BinaryOperator::PGBitwiseShiftRight => ">>", 130 | BinaryOperator::PGRegexMatch => "~", 131 | BinaryOperator::PGRegexIMatch => "~*", 132 | BinaryOperator::PGRegexNotMatch => "!~", 133 | BinaryOperator::PGRegexNotIMatch => "!~*", 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/sqlparser_sqlite.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #![warn(clippy::all)] 14 | //! Test SQL syntax specific to SQLite. The parser based on the 15 | //! generic dialect is also tested (on the inputs it can handle). 16 | 17 | #[macro_use] 18 | mod test_utils; 19 | use test_utils::*; 20 | 21 | use sqlparser::ast::*; 22 | use sqlparser::dialect::{GenericDialect, SQLiteDialect}; 23 | use sqlparser::tokenizer::Token; 24 | 25 | #[test] 26 | fn parse_create_table_without_rowid() { 27 | let sql = "CREATE TABLE t (a INT) WITHOUT ROWID"; 28 | match sqlite_and_generic().verified_stmt(sql) { 29 | Statement::CreateTable { 30 | name, 31 | without_rowid: true, 32 | .. 33 | } => { 34 | assert_eq!("t", name.to_string()); 35 | } 36 | _ => unreachable!(), 37 | } 38 | } 39 | 40 | #[test] 41 | fn parse_create_virtual_table() { 42 | let sql = "CREATE VIRTUAL TABLE IF NOT EXISTS t USING module_name (arg1, arg2)"; 43 | match sqlite_and_generic().verified_stmt(sql) { 44 | Statement::CreateVirtualTable { 45 | name, 46 | if_not_exists: true, 47 | module_name, 48 | module_args, 49 | } => { 50 | let args = vec![Ident::new("arg1"), Ident::new("arg2")]; 51 | assert_eq!("t", name.to_string()); 52 | assert_eq!("module_name", module_name.to_string()); 53 | assert_eq!(args, module_args); 54 | } 55 | _ => unreachable!(), 56 | } 57 | 58 | let sql = "CREATE VIRTUAL TABLE t USING module_name"; 59 | sqlite_and_generic().verified_stmt(sql); 60 | } 61 | 62 | #[test] 63 | fn parse_create_table_auto_increment() { 64 | let sql = "CREATE TABLE foo (bar INT PRIMARY KEY AUTOINCREMENT)"; 65 | match sqlite_and_generic().verified_stmt(sql) { 66 | Statement::CreateTable { name, columns, .. } => { 67 | assert_eq!(name.to_string(), "foo"); 68 | assert_eq!( 69 | vec![ColumnDef { 70 | name: "bar".into(), 71 | data_type: DataType::Int(None), 72 | collation: None, 73 | options: vec![ 74 | ColumnOptionDef { 75 | name: None, 76 | option: ColumnOption::Unique { is_primary: true } 77 | }, 78 | ColumnOptionDef { 79 | name: None, 80 | option: ColumnOption::DialectSpecific(vec![Token::make_keyword( 81 | "AUTOINCREMENT" 82 | )]) 83 | } 84 | ], 85 | }], 86 | columns 87 | ); 88 | } 89 | _ => unreachable!(), 90 | } 91 | } 92 | 93 | #[test] 94 | fn parse_create_sqlite_quote() { 95 | let sql = "CREATE TABLE `PRIMARY` (\"KEY\" INT, [INDEX] INT)"; 96 | match sqlite().verified_stmt(sql) { 97 | Statement::CreateTable { name, columns, .. } => { 98 | assert_eq!(name.to_string(), "`PRIMARY`"); 99 | assert_eq!( 100 | vec![ 101 | ColumnDef { 102 | name: Ident::with_quote('"', "KEY"), 103 | data_type: DataType::Int(None), 104 | collation: None, 105 | options: vec![], 106 | }, 107 | ColumnDef { 108 | name: Ident::with_quote('[', "INDEX"), 109 | data_type: DataType::Int(None), 110 | collation: None, 111 | options: vec![], 112 | }, 113 | ], 114 | columns 115 | ); 116 | } 117 | _ => unreachable!(), 118 | } 119 | } 120 | 121 | fn sqlite() -> TestedDialects { 122 | TestedDialects { 123 | dialects: vec![Box::new(SQLiteDialect {})], 124 | } 125 | } 126 | 127 | fn sqlite_and_generic() -> TestedDialects { 128 | TestedDialects { 129 | // we don't have a separate SQLite dialect, so test only the generic dialect for now 130 | dialects: vec![Box::new(SQLiteDialect {}), Box::new(GenericDialect {})], 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/sqlparser_hive.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #![warn(clippy::all)] 14 | 15 | //! Test SQL syntax specific to Hive. The parser based on the generic dialect 16 | //! is also tested (on the inputs it can handle). 17 | 18 | use sqlparser::dialect::HiveDialect; 19 | use sqlparser::test_utils::*; 20 | 21 | #[test] 22 | fn parse_table_create() { 23 | let sql = r#"CREATE TABLE IF NOT EXISTS db.table (a BIGINT, b STRING, c TIMESTAMP) PARTITIONED BY (d STRING, e TIMESTAMP) STORED AS ORC LOCATION 's3://...' TBLPROPERTIES ("prop" = "2", "asdf" = '1234', 'asdf' = "1234", "asdf" = 2)"#; 24 | let iof = r#"CREATE TABLE IF NOT EXISTS db.table (a BIGINT, b STRING, c TIMESTAMP) PARTITIONED BY (d STRING, e TIMESTAMP) STORED AS INPUTFORMAT 'org.apache.hadoop.hive.ql.io.orc.OrcInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat' LOCATION 's3://...'"#; 25 | 26 | hive().verified_stmt(sql); 27 | hive().verified_stmt(iof); 28 | } 29 | 30 | #[test] 31 | fn parse_insert_overwrite() { 32 | let insert_partitions = r#"INSERT OVERWRITE TABLE db.new_table PARTITION (a = '1', b) SELECT a, b, c FROM db.table"#; 33 | hive().verified_stmt(insert_partitions); 34 | } 35 | 36 | #[test] 37 | fn test_truncate() { 38 | let truncate = r#"TRUNCATE TABLE db.table"#; 39 | hive().verified_stmt(truncate); 40 | } 41 | 42 | #[test] 43 | fn parse_analyze() { 44 | let analyze = r#"ANALYZE TABLE db.table_name PARTITION (a = '1234', b) COMPUTE STATISTICS NOSCAN CACHE METADATA"#; 45 | hive().verified_stmt(analyze); 46 | } 47 | 48 | #[test] 49 | fn parse_analyze_for_columns() { 50 | let analyze = 51 | r#"ANALYZE TABLE db.table_name PARTITION (a = '1234', b) COMPUTE STATISTICS FOR COLUMNS"#; 52 | hive().verified_stmt(analyze); 53 | } 54 | 55 | #[test] 56 | fn parse_msck() { 57 | let msck = r#"MSCK REPAIR TABLE db.table_name ADD PARTITIONS"#; 58 | let msck2 = r#"MSCK REPAIR TABLE db.table_name"#; 59 | hive().verified_stmt(msck); 60 | hive().verified_stmt(msck2); 61 | } 62 | 63 | #[test] 64 | fn parse_set() { 65 | let set = "SET HIVEVAR:name = a, b, c_d"; 66 | hive().verified_stmt(set); 67 | } 68 | 69 | #[test] 70 | fn test_spaceship() { 71 | let spaceship = "SELECT * FROM db.table WHERE a <=> b"; 72 | hive().verified_stmt(spaceship); 73 | } 74 | 75 | #[test] 76 | fn parse_with_cte() { 77 | let with = "WITH a AS (SELECT * FROM b) INSERT INTO TABLE db.table_table PARTITION (a) SELECT * FROM b"; 78 | hive().verified_stmt(with); 79 | } 80 | 81 | #[test] 82 | fn drop_table_purge() { 83 | let purge = "DROP TABLE db.table_name PURGE"; 84 | hive().verified_stmt(purge); 85 | } 86 | 87 | #[test] 88 | fn create_table_like() { 89 | let like = "CREATE TABLE db.table_name LIKE db.other_table"; 90 | hive().verified_stmt(like); 91 | } 92 | 93 | // Turning off this test until we can parse identifiers starting with numbers :( 94 | #[test] 95 | fn test_identifier() { 96 | let between = "SELECT a AS 3_barrr_asdf FROM db.table_name"; 97 | hive().verified_stmt(between); 98 | } 99 | 100 | #[test] 101 | fn test_alter_partition() { 102 | let alter = "ALTER TABLE db.table PARTITION (a = 2) RENAME TO PARTITION (a = 1)"; 103 | hive().verified_stmt(alter); 104 | } 105 | 106 | #[test] 107 | fn test_add_partition() { 108 | let add = "ALTER TABLE db.table ADD IF NOT EXISTS PARTITION (a = 'asdf', b = 2)"; 109 | hive().verified_stmt(add); 110 | } 111 | 112 | #[test] 113 | fn test_drop_partition() { 114 | let drop = "ALTER TABLE db.table DROP PARTITION (a = 1)"; 115 | hive().verified_stmt(drop); 116 | } 117 | 118 | #[test] 119 | fn test_drop_if_exists() { 120 | let drop = "ALTER TABLE db.table DROP IF EXISTS PARTITION (a = 'b', c = 'd')"; 121 | hive().verified_stmt(drop); 122 | } 123 | 124 | #[test] 125 | fn test_cluster_by() { 126 | let cluster = "SELECT a FROM db.table CLUSTER BY a, b"; 127 | hive().verified_stmt(cluster); 128 | } 129 | 130 | #[test] 131 | fn test_distribute_by() { 132 | let cluster = "SELECT a FROM db.table DISTRIBUTE BY a, b"; 133 | hive().verified_stmt(cluster); 134 | } 135 | 136 | #[test] 137 | fn no_join_condition() { 138 | let join = "SELECT a, b FROM db.table_name JOIN a"; 139 | hive().verified_stmt(join); 140 | } 141 | 142 | #[test] 143 | fn long_numerics() { 144 | let query = r#"SELECT MIN(MIN(10, 5), 1L) AS a"#; 145 | hive().verified_stmt(query); 146 | } 147 | 148 | #[test] 149 | fn decimal_precision() { 150 | let query = "SELECT CAST(a AS DECIMAL(18,2)) FROM db.table"; 151 | let expected = "SELECT CAST(a AS NUMERIC(18,2)) FROM db.table"; 152 | hive().one_statement_parses_to(query, expected); 153 | } 154 | 155 | #[test] 156 | fn create_temp_table() { 157 | let query = "CREATE TEMPORARY TABLE db.table (a INT NOT NULL)"; 158 | let query2 = "CREATE TEMP TABLE db.table (a INT NOT NULL)"; 159 | 160 | hive().verified_stmt(query); 161 | hive().one_statement_parses_to(query2, query); 162 | } 163 | 164 | #[test] 165 | fn create_local_directory() { 166 | let query = 167 | "INSERT OVERWRITE LOCAL DIRECTORY '/home/blah' STORED AS TEXTFILE SELECT * FROM db.table"; 168 | hive().verified_stmt(query); 169 | } 170 | 171 | #[test] 172 | fn lateral_view() { 173 | let view = "SELECT a FROM db.table LATERAL VIEW explode(a) t AS j, P LATERAL VIEW OUTER explode(a) t AS a, b WHERE a = 1"; 174 | hive().verified_stmt(view); 175 | } 176 | 177 | #[test] 178 | fn sort_by() { 179 | let sort_by = "SELECT * FROM db.table SORT BY a"; 180 | hive().verified_stmt(sort_by); 181 | } 182 | 183 | #[test] 184 | fn rename_table() { 185 | let rename = "ALTER TABLE db.table_name RENAME TO db.table_2"; 186 | hive().verified_stmt(rename); 187 | } 188 | 189 | #[test] 190 | fn map_access() { 191 | let rename = r#"SELECT a.b["asdf"] FROM db.table WHERE a = 2"#; 192 | hive().verified_stmt(rename); 193 | } 194 | 195 | #[test] 196 | fn from_cte() { 197 | let rename = 198 | "WITH cte AS (SELECT * FROM a.b) FROM cte INSERT INTO TABLE a.b PARTITION (a) SELECT *"; 199 | println!("{}", hive().verified_stmt(rename)); 200 | } 201 | 202 | fn hive() -> TestedDialects { 203 | TestedDialects { 204 | dialects: vec![Box::new(HiveDialect {})], 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | /// This module contains internal utilities used for testing the library. 14 | /// While technically public, the library's users are not supposed to rely 15 | /// on this module, as it will change without notice. 16 | // 17 | // Integration tests (i.e. everything under `tests/`) import this 18 | // via `tests/test_utils/mod.rs`. 19 | 20 | #[cfg(not(feature = "std"))] 21 | use alloc::{ 22 | boxed::Box, 23 | string::{String, ToString}, 24 | vec, 25 | vec::Vec, 26 | }; 27 | use core::fmt::Debug; 28 | 29 | use crate::ast::*; 30 | use crate::dialect::*; 31 | use crate::parser::{Parser, ParserError}; 32 | use crate::tokenizer::Tokenizer; 33 | 34 | /// Tests use the methods on this struct to invoke the parser on one or 35 | /// multiple dialects. 36 | pub struct TestedDialects { 37 | pub dialects: Vec>, 38 | } 39 | 40 | impl TestedDialects { 41 | /// Run the given function for all of `self.dialects`, assert that they 42 | /// return the same result, and return that result. 43 | pub fn one_of_identical_results(&self, f: F) -> T 44 | where 45 | F: Fn(&dyn Dialect) -> T, 46 | { 47 | let parse_results = self.dialects.iter().map(|dialect| (dialect, f(&**dialect))); 48 | parse_results 49 | .fold(None, |s, (dialect, parsed)| { 50 | if let Some((prev_dialect, prev_parsed)) = s { 51 | assert_eq!( 52 | prev_parsed, parsed, 53 | "Parse results with {:?} are different from {:?}", 54 | prev_dialect, dialect 55 | ); 56 | } 57 | Some((dialect, parsed)) 58 | }) 59 | .unwrap() 60 | .1 61 | } 62 | 63 | pub fn run_parser_method(&self, sql: &str, f: F) -> T 64 | where 65 | F: Fn(&mut Parser) -> T, 66 | { 67 | self.one_of_identical_results(|dialect| { 68 | let mut tokenizer = Tokenizer::new(dialect, sql); 69 | let (tokens, pos_map) = tokenizer.tokenize().unwrap(); 70 | f(&mut Parser::new(tokens, pos_map, dialect)) 71 | }) 72 | } 73 | 74 | pub fn parse_sql_statements(&self, sql: &str) -> Result, ParserError> { 75 | self.one_of_identical_results(|dialect| Parser::parse_sql(dialect, sql)) 76 | // To fail the `ensure_multiple_dialects_are_tested` test: 77 | // Parser::parse_sql(&**self.dialects.first().unwrap(), sql) 78 | } 79 | 80 | /// Ensures that `sql` parses as a single statement and returns it. 81 | /// If non-empty `canonical` SQL representation is provided, 82 | /// additionally asserts that parsing `sql` results in the same parse 83 | /// tree as parsing `canonical`, and that serializing it back to string 84 | /// results in the `canonical` representation. 85 | pub fn one_statement_parses_to(&self, sql: &str, canonical: &str) -> Statement { 86 | let mut statements = self.parse_sql_statements(sql).unwrap(); 87 | assert_eq!(statements.len(), 1); 88 | 89 | if !canonical.is_empty() && sql != canonical { 90 | assert_eq!(self.parse_sql_statements(canonical).unwrap(), statements); 91 | } 92 | 93 | let only_statement = statements.pop().unwrap(); 94 | if !canonical.is_empty() { 95 | assert_eq!(canonical, only_statement.to_string()) 96 | } 97 | only_statement 98 | } 99 | 100 | /// Ensures that `sql` parses as a single [Statement], and is not modified 101 | /// after a serialization round-trip. 102 | pub fn verified_stmt(&self, query: &str) -> Statement { 103 | self.one_statement_parses_to(query, query) 104 | } 105 | 106 | /// Ensures that `sql` parses as a single [Query], and is not modified 107 | /// after a serialization round-trip. 108 | pub fn verified_query(&self, sql: &str) -> Query { 109 | match self.verified_stmt(sql) { 110 | Statement::Query(query) => *query, 111 | _ => panic!("Expected Query"), 112 | } 113 | } 114 | 115 | /// Ensures that `sql` parses as a single [Select], and is not modified 116 | /// after a serialization round-trip. 117 | pub fn verified_only_select(&self, query: &str) -> Select { 118 | match self.verified_query(query).body { 119 | SetExpr::Select(s) => *s, 120 | _ => panic!("Expected SetExpr::Select"), 121 | } 122 | } 123 | 124 | /// Ensures that `sql` parses as an expression, and is not modified 125 | /// after a serialization round-trip. 126 | pub fn verified_expr(&self, sql: &str) -> Expr { 127 | let ast = self 128 | .run_parser_method(sql, |parser| parser.parse_expr()) 129 | .unwrap(); 130 | assert_eq!(sql, &ast.to_string(), "round-tripping without changes"); 131 | ast 132 | } 133 | } 134 | 135 | pub fn all_dialects() -> TestedDialects { 136 | TestedDialects { 137 | dialects: vec![ 138 | Box::new(GenericDialect {}), 139 | Box::new(PostgreSqlDialect {}), 140 | Box::new(MsSqlDialect {}), 141 | Box::new(AnsiDialect {}), 142 | Box::new(SnowflakeDialect {}), 143 | Box::new(HiveDialect {}), 144 | ], 145 | } 146 | } 147 | 148 | pub fn only(v: impl IntoIterator) -> T { 149 | let mut iter = v.into_iter(); 150 | if let (Some(item), None) = (iter.next(), iter.next()) { 151 | item 152 | } else { 153 | panic!("only called on collection without exactly one item") 154 | } 155 | } 156 | 157 | pub fn expr_from_projection(item: &SelectItem) -> &Expr { 158 | match item { 159 | SelectItem::UnnamedExpr(expr) => expr, 160 | _ => panic!("Expected UnnamedExpr"), 161 | } 162 | } 163 | 164 | pub fn number(n: &'static str) -> Value { 165 | Value::Number(n.parse().unwrap(), false) 166 | } 167 | 168 | pub fn table_alias(name: impl Into) -> Option { 169 | Some(TableAlias { 170 | name: Ident::new(name), 171 | columns: vec![], 172 | }) 173 | } 174 | 175 | pub fn table(name: impl Into) -> TableFactor { 176 | TableFactor::Table { 177 | name: ObjectName(vec![Ident::new(name.into())]), 178 | alias: None, 179 | args: vec![], 180 | with_hints: vec![], 181 | instant: None, 182 | } 183 | } 184 | 185 | pub fn join(relation: TableFactor) -> Join { 186 | Join { 187 | relation, 188 | join_operator: JoinOperator::Inner(JoinConstraint::Natural), 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /tests/sqlparser_snowflake.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #![warn(clippy::all)] 14 | //! Test SQL syntax specific to Snowflake. The parser based on the 15 | //! generic dialect is also tested (on the inputs it can handle). 16 | 17 | #[macro_use] 18 | mod test_utils; 19 | use test_utils::*; 20 | 21 | use sqlparser::ast::*; 22 | use sqlparser::dialect::{GenericDialect, SnowflakeDialect}; 23 | use sqlparser::parser::ParserError; 24 | use sqlparser::tokenizer::*; 25 | 26 | #[test] 27 | fn test_snowflake_create_table() { 28 | let sql = "CREATE TABLE _my_$table (am00unt number)"; 29 | match snowflake_and_generic().verified_stmt(sql) { 30 | Statement::CreateTable { name, .. } => { 31 | assert_eq!("_my_$table", name.to_string()); 32 | } 33 | _ => unreachable!(), 34 | } 35 | } 36 | 37 | #[test] 38 | fn test_snowflake_single_line_tokenize() { 39 | let sql = "CREATE TABLE# this is a comment \ntable_1"; 40 | let dialect = SnowflakeDialect {}; 41 | let mut tokenizer = Tokenizer::new(&dialect, sql); 42 | let (tokens, _) = tokenizer.tokenize().unwrap(); 43 | 44 | let expected = vec![ 45 | Token::make_keyword("CREATE"), 46 | Token::Whitespace(Whitespace::Space), 47 | Token::make_keyword("TABLE"), 48 | Token::Whitespace(Whitespace::SingleLineComment { 49 | prefix: "#".to_string(), 50 | comment: " this is a comment \n".to_string(), 51 | }), 52 | Token::make_word("table_1", None), 53 | ]; 54 | 55 | assert_eq!(expected, tokens); 56 | 57 | let sql = "CREATE TABLE// this is a comment \ntable_1"; 58 | let mut tokenizer = Tokenizer::new(&dialect, sql); 59 | let (tokens, _) = tokenizer.tokenize().unwrap(); 60 | 61 | let expected = vec![ 62 | Token::make_keyword("CREATE"), 63 | Token::Whitespace(Whitespace::Space), 64 | Token::make_keyword("TABLE"), 65 | Token::Whitespace(Whitespace::SingleLineComment { 66 | prefix: "//".to_string(), 67 | comment: " this is a comment \n".to_string(), 68 | }), 69 | Token::make_word("table_1", None), 70 | ]; 71 | 72 | assert_eq!(expected, tokens); 73 | } 74 | 75 | #[test] 76 | fn parse_colon_map_access_expr() { 77 | let sql = "SELECT foo:key1:key2 FROM foos"; 78 | let select = snowflake_and_generic().verified_only_select(sql); 79 | assert_eq!( 80 | &Expr::MapAccess { 81 | column: Box::new(Expr::Identifier(Ident { 82 | value: "foo".to_string(), 83 | quote_style: None 84 | })), 85 | keys: vec![ 86 | Value::ColonString("key1".to_string()), 87 | Value::ColonString("key2".to_string()), 88 | ] 89 | }, 90 | expr_from_projection(only(&select.projection)), 91 | ); 92 | let sql = r#"SELECT foo:key1.key2["key3"] FROM foos"#; 93 | let select = snowflake_and_generic().verified_only_select(sql); 94 | assert_eq!( 95 | &Expr::MapAccess { 96 | column: Box::new(Expr::Identifier(Ident { 97 | value: "foo".to_string(), 98 | quote_style: None 99 | })), 100 | keys: vec![ 101 | Value::ColonString("key1".to_string()), 102 | Value::PeriodString("key2".to_string()), 103 | Value::SingleQuotedString("key3".to_string()), 104 | ] 105 | }, 106 | expr_from_projection(only(&select.projection)), 107 | ); 108 | 109 | let sql = "SELECT foo:key1:0 FROM foos"; 110 | let res = snowflake_and_generic().parse_sql_statements(sql); 111 | assert_eq!( 112 | ParserError::ParserError("Expected literal string, found: 0".to_string()), 113 | res.unwrap_err() 114 | ); 115 | } 116 | 117 | #[test] 118 | fn test_sf_derived_table_in_parenthesis() { 119 | // Nesting a subquery in an extra set of parentheses is non-standard, 120 | // but supported in Snowflake SQL 121 | snowflake_and_generic().one_statement_parses_to( 122 | "SELECT * FROM ((SELECT 1) AS t)", 123 | "SELECT * FROM (SELECT 1) AS t", 124 | ); 125 | snowflake_and_generic().one_statement_parses_to( 126 | "SELECT * FROM (((SELECT 1) AS t))", 127 | "SELECT * FROM (SELECT 1) AS t", 128 | ); 129 | } 130 | 131 | #[test] 132 | fn test_single_table_in_parenthesis() { 133 | // Parenthesized table names are non-standard, but supported in Snowflake SQL 134 | snowflake_and_generic().one_statement_parses_to( 135 | "SELECT * FROM (a NATURAL JOIN (b))", 136 | "SELECT * FROM (a NATURAL JOIN b)", 137 | ); 138 | snowflake_and_generic().one_statement_parses_to( 139 | "SELECT * FROM (a NATURAL JOIN ((b)))", 140 | "SELECT * FROM (a NATURAL JOIN b)", 141 | ); 142 | } 143 | 144 | #[test] 145 | fn test_single_table_in_parenthesis_with_alias() { 146 | snowflake_and_generic().one_statement_parses_to( 147 | "SELECT * FROM (a NATURAL JOIN (b) c )", 148 | "SELECT * FROM (a NATURAL JOIN b AS c)", 149 | ); 150 | 151 | snowflake_and_generic().one_statement_parses_to( 152 | "SELECT * FROM (a NATURAL JOIN ((b)) c )", 153 | "SELECT * FROM (a NATURAL JOIN b AS c)", 154 | ); 155 | 156 | snowflake_and_generic().one_statement_parses_to( 157 | "SELECT * FROM (a NATURAL JOIN ( (b) c ) )", 158 | "SELECT * FROM (a NATURAL JOIN b AS c)", 159 | ); 160 | 161 | snowflake_and_generic().one_statement_parses_to( 162 | "SELECT * FROM (a NATURAL JOIN ( (b) as c ) )", 163 | "SELECT * FROM (a NATURAL JOIN b AS c)", 164 | ); 165 | 166 | snowflake_and_generic().one_statement_parses_to( 167 | "SELECT * FROM (a alias1 NATURAL JOIN ( (b) c ) )", 168 | "SELECT * FROM (a AS alias1 NATURAL JOIN b AS c)", 169 | ); 170 | 171 | snowflake_and_generic().one_statement_parses_to( 172 | "SELECT * FROM (a as alias1 NATURAL JOIN ( (b) as c ) )", 173 | "SELECT * FROM (a AS alias1 NATURAL JOIN b AS c)", 174 | ); 175 | 176 | let res = snowflake_and_generic().parse_sql_statements("SELECT * FROM (a NATURAL JOIN b) c"); 177 | assert_eq!( 178 | ParserError::ParserError("Expected end of statement, found: c".to_string()), 179 | res.unwrap_err() 180 | ); 181 | 182 | let res = snowflake().parse_sql_statements("SELECT * FROM (a b) c"); 183 | assert_eq!( 184 | ParserError::ParserError("duplicate alias b".to_string()), 185 | res.unwrap_err() 186 | ); 187 | } 188 | 189 | fn snowflake() -> TestedDialects { 190 | TestedDialects { 191 | dialects: vec![Box::new(SnowflakeDialect {})], 192 | } 193 | } 194 | 195 | fn snowflake_and_generic() -> TestedDialects { 196 | TestedDialects { 197 | dialects: vec![Box::new(SnowflakeDialect {}), Box::new(GenericDialect {})], 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extensible SQL Lexer and Parser for Rust 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | [![Version](https://img.shields.io/crates/v/sqlparser.svg)](https://crates.io/crates/sqlparser) 5 | [![Build Status](https://github.com/sqlparser-rs/sqlparser-rs/workflows/Rust/badge.svg?branch=main)](https://github.com/sqlparser-rs/sqlparser-rs/actions?query=workflow%3ARust+branch%3Amain) 6 | [![Coverage Status](https://coveralls.io/repos/github/sqlparser-rs/sqlparser-rs/badge.svg?branch=main)](https://coveralls.io/github/sqlparser-rs/sqlparser-rs?branch=main) 7 | [![Gitter Chat](https://badges.gitter.im/sqlparser-rs/community.svg)](https://gitter.im/sqlparser-rs/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | The goal of this project is to build a SQL lexer and parser capable of parsing 10 | SQL that conforms with the [ANSI/ISO SQL standard][sql-standard] while also 11 | making it easy to support custom dialects so that this crate can be used as a 12 | foundation for vendor-specific parsers. 13 | 14 | This parser is currently being used by the [DataFusion] query engine, 15 | [LocustDB], [Ballista] and [GlueSQL]. 16 | 17 | ## Example 18 | 19 | To parse a simple `SELECT` statement: 20 | 21 | ```rust 22 | use sqlparser::dialect::GenericDialect; 23 | use sqlparser::parser::Parser; 24 | 25 | let sql = "SELECT a, b, 123, myfunc(b) \ 26 | FROM table_1 \ 27 | WHERE a > b AND b < 100 \ 28 | ORDER BY a DESC, b"; 29 | 30 | let dialect = GenericDialect {}; // or AnsiDialect, or your own dialect ... 31 | 32 | let ast = Parser::parse_sql(&dialect, sql).unwrap(); 33 | 34 | println!("AST: {:?}", ast); 35 | ``` 36 | 37 | This outputs 38 | 39 | ```rust 40 | AST: [Query(Query { ctes: [], body: Select(Select { distinct: false, projection: [UnnamedExpr(Identifier("a")), UnnamedExpr(Identifier("b")), UnnamedExpr(Value(Long(123))), UnnamedExpr(Function(Function { name: ObjectName(["myfunc"]), args: [Identifier("b")], over: None, distinct: false }))], from: [TableWithJoins { relation: Table { name: ObjectName(["table_1"]), alias: None, args: [], with_hints: [] }, joins: [] }], selection: Some(BinaryOp { left: BinaryOp { left: Identifier("a"), op: Gt, right: Identifier("b") }, op: And, right: BinaryOp { left: Identifier("b"), op: Lt, right: Value(Long(100)) } }), group_by: [], having: None }), order_by: [OrderByExpr { expr: Identifier("a"), asc: Some(false) }, OrderByExpr { expr: Identifier("b"), asc: None }], limit: None, offset: None, fetch: None })] 41 | ``` 42 | 43 | ## Command line 44 | To parse a file and dump the results as JSON: 45 | ``` 46 | $ cargo run --features json_example --example cli FILENAME.sql [--dialectname] 47 | ``` 48 | 49 | ## SQL compliance 50 | 51 | SQL was first standardized in 1987, and revisions of the standard have been 52 | published regularly since. Most revisions have added significant new features to 53 | the language, and as a result no database claims to support the full breadth of 54 | features. This parser currently supports most of the SQL-92 syntax, plus some 55 | syntax from newer versions that have been explicitly requested, plus some MSSQL, 56 | PostgreSQL, and other dialect-specific syntax. Whenever possible, the [online 57 | SQL:2016 grammar][sql-2016-grammar] is used to guide what syntax to accept. 58 | 59 | Unfortunately, stating anything more specific about compliance is difficult. 60 | There is no publicly available test suite that can assess compliance 61 | automatically, and doing so manually would strain the project's limited 62 | resources. Still, we are interested in eventually supporting the full SQL 63 | dialect, and we are slowly building out our own test suite. 64 | 65 | If you are assessing whether this project will be suitable for your needs, 66 | you'll likely need to experimentally verify whether it supports the subset of 67 | SQL that you need. Please file issues about any unsupported queries that you 68 | discover. Doing so helps us prioritize support for the portions of the standard 69 | that are actually used. Note that if you urgently need support for a feature, 70 | you will likely need to write the implementation yourself. See the 71 | [Contributing](#Contributing) section for details. 72 | 73 | ### Supporting custom SQL dialects 74 | 75 | This is a work in progress, but we have some notes on [writing a custom SQL 76 | parser](docs/custom_sql_parser.md). 77 | 78 | ## Design 79 | 80 | The core expression parser uses the [Pratt Parser] design, which is a top-down 81 | operator-precedence (TDOP) parser, while the surrounding SQL statement parser is 82 | a traditional, hand-written recursive descent parser. Eli Bendersky has a good 83 | [tutorial on TDOP parsers][tdop-tutorial], if you are interested in learning 84 | more about the technique. 85 | 86 | We are a fan of this design pattern over parser generators for the following 87 | reasons: 88 | 89 | - Code is simple to write and can be concise and elegant 90 | - Performance is generally better than code generated by parser generators 91 | - Debugging is much easier with hand-written code 92 | - It is far easier to extend and make dialect-specific extensions 93 | compared to using a parser generator 94 | 95 | ## Contributing 96 | 97 | Contributions are highly encouraged! 98 | 99 | Pull requests that add support for or fix a bug in a feature in the SQL 100 | standard, or a feature in a popular RDBMS, like Microsoft SQL Server or 101 | PostgreSQL, will almost certainly be accepted after a brief review. For 102 | particularly large or invasive changes, consider opening an issue first, 103 | especially if you are a first time contributor, so that you can coordinate with 104 | the maintainers. CI will ensure that your code passes `cargo test`, 105 | `cargo fmt`, and `cargo clippy`, so you will likely want to run all three 106 | commands locally before submitting your PR. 107 | 108 | If you are unable to submit a patch, feel free to file an issue instead. Please 109 | try to include: 110 | 111 | * some representative examples of the syntax you wish to support or fix; 112 | * the relevant bits of the [SQL grammar][sql-2016-grammar], if the syntax is 113 | part of SQL:2016; and 114 | * links to documentation for the feature for a few of the most popular 115 | databases that support it. 116 | 117 | Please be aware that, while we strive to address bugs and review PRs quickly, we 118 | make no such guarantees for feature requests. If you need support for a feature, 119 | you will likely need to implement it yourself. Our goal as maintainers is to 120 | facilitate the integration of various features from various contributors, but 121 | not to provide the implementations ourselves, as we simply don't have the 122 | resources. 123 | 124 | [tdop-tutorial]: https://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing 125 | [`cargo fmt`]: https://github.com/rust-lang/rustfmt#on-the-stable-toolchain 126 | [current issues]: https://github.com/sqlparser-rs/sqlparser-rs/issues 127 | [DataFusion]: https://github.com/apache/arrow-datafusion 128 | [LocustDB]: https://github.com/cswinter/LocustDB 129 | [Ballista]: https://github.com/apache/arrow-datafusion/tree/master/ballista 130 | [GlueSQL]: https://github.com/gluesql/gluesql 131 | [Pratt Parser]: https://tdop.github.io/ 132 | [sql-2016-grammar]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html 133 | [sql-standard]: https://en.wikipedia.org/wiki/ISO/IEC_9075 134 | -------------------------------------------------------------------------------- /src/ast/data_type.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #[cfg(not(feature = "std"))] 14 | use alloc::{boxed::Box, vec::Vec}; 15 | use core::fmt; 16 | 17 | #[cfg(feature = "serde")] 18 | use serde::{Deserialize, Serialize}; 19 | 20 | use crate::ast::Ident; 21 | use crate::ast::ObjectName; 22 | 23 | /// SQL data types 24 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 25 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 26 | pub enum DataType { 27 | /// Fixed-length character type e.g. CHAR(10) 28 | Char(Option), 29 | /// Variable-length character type e.g. VARCHAR(10) 30 | Varchar(Option), 31 | /// Uuid type 32 | Uuid, 33 | /// Large character object e.g. CLOB(1000) 34 | Clob(u64), 35 | /// Fixed-length binary type e.g. BINARY(10) 36 | Binary(u64), 37 | /// Variable-length binary type e.g. VARBINARY(10) 38 | Varbinary(u64), 39 | /// Large binary object e.g. BLOB(1000) 40 | Blob(u64), 41 | /// Decimal type with optional precision and scale e.g. DECIMAL(10,2) 42 | Decimal(Option, Option), 43 | /// Floating point with optional precision e.g. FLOAT(8) 44 | Float(Option), 45 | /// Tiny integer with optional display width e.g. TINYINT or TINYINT(3) 46 | TinyInt(Option), 47 | /// Unsigned tiny integer with optional display width e.g. TINYINT UNSIGNED or TINYINT(3) UNSIGNED 48 | UnsignedTinyInt(Option), 49 | /// Small integer with optional display width e.g. SMALLINT or SMALLINT(5) 50 | SmallInt(Option), 51 | /// Unsigned small integer with optional display width e.g. SMALLINT UNSIGNED or SMALLINT(5) UNSIGNED 52 | UnsignedSmallInt(Option), 53 | /// Integer with optional display width e.g. INT or INT(11) 54 | Int(Option), 55 | /// Unsigned integer with optional display width e.g. INT UNSIGNED or INT(11) UNSIGNED 56 | UnsignedInt(Option), 57 | /// Big integer with optional display width e.g. BIGINT or BIGINT(20) 58 | BigInt(Option), 59 | /// Unsigned big integer with optional display width e.g. BIGINT UNSIGNED or BIGINT(20) UNSIGNED 60 | UnsignedBigInt(Option), 61 | /// Floating point e.g. REAL 62 | Real, 63 | /// Double e.g. DOUBLE PRECISION 64 | Double, 65 | /// Boolean 66 | Boolean, 67 | /// Date 68 | Date, 69 | /// Time 70 | Time, 71 | /// Timestamp without tz 72 | Timestamp(Option), 73 | /// DateTime without tz 74 | DateTime(Option), 75 | /// Interval 76 | Interval, 77 | /// Regclass used in postgresql serial 78 | Regclass, 79 | /// Text 80 | Text, 81 | /// String 82 | String, 83 | /// Bytea 84 | Bytea, 85 | /// Custom type such as enums 86 | Custom(ObjectName), 87 | /// Arrays 88 | Array(Box, bool), 89 | /// Tuple 90 | Tuple(Option>, Vec>), 91 | } 92 | 93 | impl fmt::Display for DataType { 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | match self { 96 | DataType::Char(size) => format_type_with_optional_length(f, "CHAR", size, false), 97 | DataType::Varchar(size) => { 98 | format_type_with_optional_length(f, "CHARACTER VARYING", size, false) 99 | } 100 | DataType::Uuid => write!(f, "UUID"), 101 | DataType::Clob(size) => write!(f, "CLOB({})", size), 102 | DataType::Binary(size) => write!(f, "BINARY({})", size), 103 | DataType::Varbinary(size) => write!(f, "VARBINARY({})", size), 104 | DataType::Blob(size) => write!(f, "BLOB({})", size), 105 | DataType::Decimal(precision, scale) => { 106 | if let Some(scale) = scale { 107 | write!(f, "NUMERIC({},{})", precision.unwrap(), scale) 108 | } else { 109 | format_type_with_optional_length(f, "NUMERIC", precision, false) 110 | } 111 | } 112 | DataType::Float(size) => format_type_with_optional_length(f, "FLOAT", size, false), 113 | DataType::TinyInt(zerofill) => { 114 | format_type_with_optional_length(f, "TINYINT", zerofill, false) 115 | } 116 | DataType::UnsignedTinyInt(zerofill) => { 117 | format_type_with_optional_length(f, "TINYINT", zerofill, true) 118 | } 119 | DataType::SmallInt(zerofill) => { 120 | format_type_with_optional_length(f, "SMALLINT", zerofill, false) 121 | } 122 | DataType::UnsignedSmallInt(zerofill) => { 123 | format_type_with_optional_length(f, "SMALLINT", zerofill, true) 124 | } 125 | DataType::Int(zerofill) => format_type_with_optional_length(f, "INT", zerofill, false), 126 | DataType::UnsignedInt(zerofill) => { 127 | format_type_with_optional_length(f, "INT", zerofill, true) 128 | } 129 | DataType::BigInt(zerofill) => { 130 | format_type_with_optional_length(f, "BIGINT", zerofill, false) 131 | } 132 | DataType::UnsignedBigInt(zerofill) => { 133 | format_type_with_optional_length(f, "BIGINT", zerofill, true) 134 | } 135 | DataType::Real => write!(f, "REAL"), 136 | DataType::Double => write!(f, "DOUBLE"), 137 | DataType::Boolean => write!(f, "BOOLEAN"), 138 | DataType::Date => write!(f, "DATE"), 139 | DataType::Time => write!(f, "TIME"), 140 | DataType::Timestamp(n) => format_type_with_optional_length(f, "TIMESTAMP", n, false), 141 | DataType::Interval => write!(f, "INTERVAL"), 142 | DataType::Regclass => write!(f, "REGCLASS"), 143 | DataType::Text => write!(f, "TEXT"), 144 | DataType::String => write!(f, "STRING"), 145 | DataType::Bytea => write!(f, "BYTEA"), 146 | DataType::Array(ty, nullable) => { 147 | if *nullable { 148 | write!(f, "ARRAY({} NULL)", ty) 149 | } else { 150 | write!(f, "ARRAY({})", ty) 151 | } 152 | } 153 | DataType::Custom(ty) => write!(f, "{}", ty), 154 | DataType::DateTime(n) => format_type_with_optional_length(f, "DATETIME", n, false), 155 | DataType::Tuple(names, types) => match names { 156 | Some(names) => format_named_tuple(f, names, types), 157 | None => format_tuple(f, types), 158 | }, 159 | } 160 | } 161 | } 162 | 163 | fn format_type_with_optional_length( 164 | f: &mut fmt::Formatter, 165 | sql_type: &'static str, 166 | len: &Option, 167 | unsigned: bool, 168 | ) -> fmt::Result { 169 | write!(f, "{}", sql_type)?; 170 | if let Some(len) = len { 171 | write!(f, "({})", len)?; 172 | } 173 | if unsigned { 174 | write!(f, " UNSIGNED")?; 175 | } 176 | Ok(()) 177 | } 178 | 179 | fn format_tuple(f: &mut fmt::Formatter, types: &[Box]) -> fmt::Result { 180 | write!(f, "TUPLE(")?; 181 | let mut first = true; 182 | for ty in types.iter() { 183 | if !first { 184 | write!(f, ", ")?; 185 | } 186 | first = false; 187 | write!(f, "{}", ty)?; 188 | } 189 | write!(f, ")")?; 190 | Ok(()) 191 | } 192 | 193 | fn format_named_tuple( 194 | f: &mut fmt::Formatter, 195 | names: &[Ident], 196 | types: &[Box], 197 | ) -> fmt::Result { 198 | debug_assert!(names.len() == types.len()); 199 | write!(f, "TUPLE(")?; 200 | let mut first = true; 201 | for (name, ty) in names.iter().zip(types.iter()) { 202 | if !first { 203 | write!(f, ", ")?; 204 | } 205 | first = false; 206 | write!(f, "{} {}", name, ty)?; 207 | } 208 | write!(f, ")")?; 209 | Ok(()) 210 | } 211 | -------------------------------------------------------------------------------- /src/ast/value.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #[cfg(not(feature = "std"))] 14 | use alloc::{format, string::String}; 15 | use core::fmt; 16 | 17 | #[cfg(feature = "bigdecimal")] 18 | use bigdecimal::BigDecimal; 19 | #[cfg(feature = "bigdecimal")] 20 | use core::ops::Neg; 21 | #[cfg(feature = "serde")] 22 | use serde::{Deserialize, Serialize}; 23 | 24 | /// Primitive SQL values such as number and string 25 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 26 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 27 | pub enum Value { 28 | /// Numeric literal 29 | #[cfg(not(feature = "bigdecimal"))] 30 | Number(String, bool), 31 | #[cfg(feature = "bigdecimal")] 32 | Number(BigDecimal, bool), 33 | /// 'string value' 34 | SingleQuotedString(String), 35 | /// N'string value' 36 | NationalStringLiteral(String), 37 | /// X'hex value' 38 | HexStringLiteral(String), 39 | /// :string value 40 | ColonString(String), 41 | /// .string value 42 | PeriodString(String), 43 | 44 | DoubleQuotedString(String), 45 | /// Boolean value true or false 46 | Boolean(bool), 47 | /// INTERVAL literals, roughly in the following format: 48 | /// `INTERVAL '' [ [ () ] ] 49 | /// [ TO [ () ] ]`, 50 | /// e.g. `INTERVAL '123:45.67' MINUTE(3) TO SECOND(2)`. 51 | /// 52 | /// The parser does not validate the ``, nor does it ensure 53 | /// that the `` units >= the units in ``, 54 | /// so the user will have to reject intervals like `HOUR TO YEAR`. 55 | Interval { 56 | value: String, 57 | leading_field: Option, 58 | leading_precision: Option, 59 | last_field: Option, 60 | /// The seconds precision can be specified in SQL source as 61 | /// `INTERVAL '__' SECOND(_, x)` (in which case the `leading_field` 62 | /// will be `Second` and the `last_field` will be `None`), 63 | /// or as `__ TO SECOND(x)`. 64 | fractional_seconds_precision: Option, 65 | }, 66 | /// `NULL` value 67 | Null, 68 | } 69 | 70 | impl Value { 71 | pub fn to_negative(self) -> Self { 72 | match self { 73 | #[cfg(not(feature = "bigdecimal"))] 74 | Value::Number(x, v) => Value::Number(format!("-{}", x), v), 75 | #[cfg(feature = "bigdecimal")] 76 | Value::Number(x, v) => Value::Number(x.neg(), v), 77 | 78 | Value::SingleQuotedString(v) => Value::SingleQuotedString(format!("-{}", v)), 79 | Value::NationalStringLiteral(v) => Value::NationalStringLiteral(format!("-{}", v)), 80 | Value::HexStringLiteral(v) => Value::HexStringLiteral(format!("-{}", v)), 81 | Value::DoubleQuotedString(v) => Value::DoubleQuotedString(format!("-{}", v)), 82 | Value::ColonString(v) => Value::ColonString(format!("-{}", v)), 83 | Value::PeriodString(v) => Value::PeriodString(format!("-{}", v)), 84 | Value::Boolean(v) => Value::Boolean(v), 85 | Value::Interval { 86 | value, 87 | leading_field, 88 | leading_precision, 89 | last_field, 90 | fractional_seconds_precision, 91 | } => Value::Interval { 92 | value: format!("-{}", value), 93 | leading_field, 94 | leading_precision, 95 | last_field, 96 | fractional_seconds_precision, 97 | }, 98 | Value::Null => Value::Null, 99 | } 100 | } 101 | } 102 | 103 | impl fmt::Display for Value { 104 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 105 | match self { 106 | Value::Number(v, l) => write!(f, "{}{long}", v, long = if *l { "L" } else { "" }), 107 | Value::DoubleQuotedString(v) => write!(f, "\"{}\"", v), 108 | Value::SingleQuotedString(v) => write!(f, "'{}'", escape_single_quote_string(v)), 109 | Value::NationalStringLiteral(v) => write!(f, "N'{}'", v), 110 | Value::HexStringLiteral(v) => write!(f, "X'{}'", v), 111 | Value::ColonString(v) => write!(f, ":{}", v), 112 | Value::PeriodString(v) => write!(f, ".{}", v), 113 | Value::Boolean(v) => write!(f, "{}", v), 114 | Value::Interval { 115 | value, 116 | leading_field: Some(DateTimeField::Second), 117 | leading_precision: Some(leading_precision), 118 | last_field, 119 | fractional_seconds_precision: Some(fractional_seconds_precision), 120 | } => { 121 | // When the leading field is SECOND, the parser guarantees that 122 | // the last field is None. 123 | assert!(last_field.is_none()); 124 | write!( 125 | f, 126 | "INTERVAL '{}' SECOND ({}, {})", 127 | escape_single_quote_string(value), 128 | leading_precision, 129 | fractional_seconds_precision 130 | ) 131 | } 132 | Value::Interval { 133 | value, 134 | leading_field, 135 | leading_precision, 136 | last_field, 137 | fractional_seconds_precision, 138 | } => { 139 | write!(f, "INTERVAL '{}'", escape_single_quote_string(value))?; 140 | if let Some(leading_field) = leading_field { 141 | write!(f, " {}", leading_field)?; 142 | } 143 | if let Some(leading_precision) = leading_precision { 144 | write!(f, " ({})", leading_precision)?; 145 | } 146 | if let Some(last_field) = last_field { 147 | write!(f, " TO {}", last_field)?; 148 | } 149 | if let Some(fractional_seconds_precision) = fractional_seconds_precision { 150 | write!(f, " ({})", fractional_seconds_precision)?; 151 | } 152 | Ok(()) 153 | } 154 | Value::Null => write!(f, "NULL"), 155 | } 156 | } 157 | } 158 | 159 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 160 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 161 | pub enum DateTimeField { 162 | Year, 163 | Month, 164 | Day, 165 | Hour, 166 | Minute, 167 | Second, 168 | } 169 | 170 | impl fmt::Display for DateTimeField { 171 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 172 | f.write_str(match self { 173 | DateTimeField::Year => "YEAR", 174 | DateTimeField::Month => "MONTH", 175 | DateTimeField::Day => "DAY", 176 | DateTimeField::Hour => "HOUR", 177 | DateTimeField::Minute => "MINUTE", 178 | DateTimeField::Second => "SECOND", 179 | }) 180 | } 181 | } 182 | 183 | pub struct EscapeSingleQuoteString<'a>(&'a str); 184 | 185 | impl<'a> fmt::Display for EscapeSingleQuoteString<'a> { 186 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 187 | for c in self.0.chars() { 188 | if c == '\'' { 189 | write!(f, "\'\'")?; 190 | } else { 191 | write!(f, "{}", c)?; 192 | } 193 | } 194 | Ok(()) 195 | } 196 | } 197 | 198 | pub fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> { 199 | EscapeSingleQuoteString(s) 200 | } 201 | 202 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 203 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 204 | pub enum TrimWhereField { 205 | Both, 206 | Leading, 207 | Trailing, 208 | } 209 | 210 | impl fmt::Display for TrimWhereField { 211 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 212 | use TrimWhereField::*; 213 | f.write_str(match self { 214 | Both => "BOTH", 215 | Leading => "LEADING", 216 | Trailing => "TRAILING", 217 | }) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/keywords.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | //! This module defines 14 | //! 1) a list of constants for every keyword that 15 | //! can appear in [Word::keyword]: 16 | //! pub const KEYWORD = "KEYWORD" 17 | //! 2) an `ALL_KEYWORDS` array with every keyword in it 18 | //! This is not a list of *reserved* keywords: some of these can be 19 | //! parsed as identifiers if the parser decides so. This means that 20 | //! new keywords can be added here without affecting the parse result. 21 | //! 22 | //! As a matter of fact, most of these keywords are not used at all 23 | //! and could be removed. 24 | //! 3) a `RESERVED_FOR_TABLE_ALIAS` array with keywords reserved in a 25 | //! "table alias" context. 26 | 27 | #[cfg(feature = "serde")] 28 | use serde::{Deserialize, Serialize}; 29 | 30 | /// Defines a string constant for a single keyword: `kw_def!(SELECT);` 31 | /// expands to `pub const SELECT = "SELECT";` 32 | macro_rules! kw_def { 33 | ($ident:ident = $string_keyword:expr) => { 34 | pub const $ident: &'static str = $string_keyword; 35 | }; 36 | ($ident:ident) => { 37 | kw_def!($ident = stringify!($ident)); 38 | }; 39 | } 40 | 41 | /// Expands to a list of `kw_def!()` invocations for each keyword 42 | /// and defines an ALL_KEYWORDS array of the defined constants. 43 | macro_rules! define_keywords { 44 | ($( 45 | $ident:ident $(= $string_keyword:expr)? 46 | ),*) => { 47 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] 48 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 49 | #[allow(non_camel_case_types)] 50 | pub enum Keyword { 51 | NoKeyword, 52 | $($ident),* 53 | } 54 | 55 | pub const ALL_KEYWORDS_INDEX: &[Keyword] = &[ 56 | $(Keyword::$ident),* 57 | ]; 58 | 59 | $(kw_def!($ident $(= $string_keyword)?);)* 60 | pub const ALL_KEYWORDS: &[&str] = &[ 61 | $($ident),* 62 | ]; 63 | }; 64 | } 65 | 66 | // The following keywords should be sorted to be able to match using binary search 67 | define_keywords!( 68 | ABORT, 69 | ABS, 70 | ACTION, 71 | ADD, 72 | ALL, 73 | ALLOCATE, 74 | ALTER, 75 | ANALYZE, 76 | AND, 77 | ANY, 78 | APPLY, 79 | ARE, 80 | ARRAY, 81 | ARRAY_AGG, 82 | ARRAY_MAX_CARDINALITY, 83 | AS, 84 | ASC, 85 | ASENSITIVE, 86 | ASSERT, 87 | ASYMMETRIC, 88 | AT, 89 | ATOMIC, 90 | AUTHORIZATION, 91 | AUTOINCREMENT, 92 | AUTO_INCREMENT, 93 | AVG, 94 | AVRO, 95 | BEGIN, 96 | BEGIN_FRAME, 97 | BEGIN_PARTITION, 98 | BETWEEN, 99 | BIGINT, 100 | BINARY, 101 | BLOB, 102 | BOOLEAN, 103 | BOTH, 104 | BY, 105 | BYTEA, 106 | CACHE, 107 | CALL, 108 | CALLED, 109 | CARDINALITY, 110 | CASCADE, 111 | CASCADED, 112 | CASE, 113 | CAST, 114 | CEIL, 115 | CEILING, 116 | CHAIN, 117 | CHANGE, 118 | CHAR, 119 | CHARACTER, 120 | CHARACTER_LENGTH, 121 | CHAR_LENGTH, 122 | CHECK, 123 | CLOB, 124 | CLOSE, 125 | CLUSTER, 126 | COALESCE, 127 | COLLATE, 128 | COLLECT, 129 | COLUMN, 130 | COLUMNS, 131 | COMMENT, 132 | COMMIT, 133 | COMMITTED, 134 | COMPUTE, 135 | CONDITION, 136 | CONNECT, 137 | CONSTRAINT, 138 | CONTAINS, 139 | CONVERT, 140 | COPY, 141 | CORR, 142 | CORRESPONDING, 143 | COUNT, 144 | COVAR_POP, 145 | COVAR_SAMP, 146 | CREATE, 147 | CROSS, 148 | CSV, 149 | CUBE, 150 | CUME_DIST, 151 | CURRENT, 152 | CURRENT_CATALOG, 153 | CURRENT_DATE, 154 | CURRENT_DEFAULT_TRANSFORM_GROUP, 155 | CURRENT_PATH, 156 | CURRENT_ROLE, 157 | CURRENT_ROW, 158 | CURRENT_SCHEMA, 159 | CURRENT_TIME, 160 | CURRENT_TIMESTAMP, 161 | CURRENT_TRANSFORM_GROUP_FOR_TYPE, 162 | CURRENT_USER, 163 | CURSOR, 164 | CYCLE, 165 | DATABASE, 166 | DATE, 167 | DATETIME, 168 | DAY, 169 | DEALLOCATE, 170 | DEC, 171 | DECIMAL, 172 | DECLARE, 173 | DEFAULT, 174 | DELETE, 175 | DELIMITED, 176 | DENSE_RANK, 177 | DEREF, 178 | DESC, 179 | DESCRIBE, 180 | DETERMINISTIC, 181 | DIRECTORY, 182 | DISCONNECT, 183 | DISTINCT, 184 | DISTRIBUTE, 185 | DIV, 186 | DOUBLE, 187 | DROP, 188 | DUPLICATE, 189 | DYNAMIC, 190 | EACH, 191 | ELEMENT, 192 | ELSE, 193 | END, 194 | END_EXEC = "END-EXEC", 195 | END_FRAME, 196 | END_PARTITION, 197 | EQUALS, 198 | ERROR, 199 | ESCAPE, 200 | EVENT, 201 | EVERY, 202 | EXCEPT, 203 | EXEC, 204 | EXECUTE, 205 | EXISTS, 206 | EXP, 207 | EXPLAIN, 208 | EXTENDED, 209 | EXTERNAL, 210 | EXTRACT, 211 | FAIL, 212 | FALSE, 213 | FETCH, 214 | FIELDS, 215 | FILTER, 216 | FIRST, 217 | FIRST_VALUE, 218 | FLOAT, 219 | FLOOR, 220 | FOLLOWING, 221 | FOR, 222 | FOREIGN, 223 | FORMAT, 224 | FRAME_ROW, 225 | FREE, 226 | FROM, 227 | FULL, 228 | FUNCTION, 229 | FUSION, 230 | GET, 231 | GLOBAL, 232 | GRANT, 233 | GRANTED, 234 | GROUP, 235 | GROUPING, 236 | GROUPS, 237 | HAVING, 238 | HEADER, 239 | HIVEVAR, 240 | HOLD, 241 | HOUR, 242 | IDENTIFIED, 243 | IDENTITY, 244 | IF, 245 | IGNORE, 246 | ILIKE, 247 | IN, 248 | INDEX, 249 | INDICATOR, 250 | INNER, 251 | INOUT, 252 | INPUTFORMAT, 253 | INSENSITIVE, 254 | INSERT, 255 | INT, 256 | INTEGER, 257 | INTERSECT, 258 | INTERSECTION, 259 | INTERVAL, 260 | INTO, 261 | IS, 262 | ISOLATION, 263 | JOIN, 264 | JSONFILE, 265 | KEY, 266 | LAG, 267 | LANGUAGE, 268 | LARGE, 269 | LAST, 270 | LAST_VALUE, 271 | LATERAL, 272 | LEAD, 273 | LEADING, 274 | LEFT, 275 | LEVEL, 276 | LIKE, 277 | LIKE_REGEX, 278 | LIMIT, 279 | LIST, 280 | LISTAGG, 281 | LN, 282 | LOCAL, 283 | LOCALTIME, 284 | LOCALTIMESTAMP, 285 | LOCATION, 286 | LOWER, 287 | MANAGEDLOCATION, 288 | MATCH, 289 | MATERIALIZED, 290 | MAX, 291 | MEMBER, 292 | MERGE, 293 | METADATA, 294 | METHOD, 295 | MIN, 296 | MINUTE, 297 | MOD, 298 | MODIFIES, 299 | MODULE, 300 | MONTH, 301 | MSCK, 302 | MULTISET, 303 | NATIONAL, 304 | NATURAL, 305 | NCHAR, 306 | NCLOB, 307 | NEW, 308 | NEXT, 309 | NO, 310 | NONE, 311 | NORMALIZE, 312 | NOSCAN, 313 | NOT, 314 | NTH_VALUE, 315 | NTILE, 316 | NULL, 317 | NULLIF, 318 | NULLS, 319 | NUMERIC, 320 | OBJECT, 321 | OCCURRENCES_REGEX, 322 | OCTET_LENGTH, 323 | OF, 324 | OFFSET, 325 | OLD, 326 | ON, 327 | ONLY, 328 | OPEN, 329 | OPTION, 330 | OR, 331 | ORC, 332 | ORDER, 333 | OUT, 334 | OUTER, 335 | OUTPUTFORMAT, 336 | OVER, 337 | OVERFLOW, 338 | OVERLAPS, 339 | OVERLAY, 340 | OVERWRITE, 341 | PARAMETER, 342 | PARQUET, 343 | PARTITION, 344 | PARTITIONED, 345 | PARTITIONS, 346 | PERCENT, 347 | PERCENTILE_CONT, 348 | PERCENTILE_DISC, 349 | PERCENT_RANK, 350 | PERIOD, 351 | PORTION, 352 | POSITION, 353 | POSITION_REGEX, 354 | POWER, 355 | PRECEDES, 356 | PRECEDING, 357 | PRECISION, 358 | PREPARE, 359 | PRIMARY, 360 | PRIVILEGES, 361 | PROCEDURE, 362 | PURGE, 363 | RANGE, 364 | RANK, 365 | RCFILE, 366 | READ, 367 | READS, 368 | REAL, 369 | RECURSIVE, 370 | REF, 371 | REFERENCES, 372 | REFERENCING, 373 | REGCLASS, 374 | REGEXP, 375 | REGR_AVGX, 376 | REGR_AVGY, 377 | REGR_COUNT, 378 | REGR_INTERCEPT, 379 | REGR_R2, 380 | REGR_SLOPE, 381 | REGR_SXX, 382 | REGR_SXY, 383 | REGR_SYY, 384 | RELEASE, 385 | RENAME, 386 | REPAIR, 387 | REPEATABLE, 388 | REPLACE, 389 | RESTRICT, 390 | RESULT, 391 | RETURN, 392 | RETURNS, 393 | REVOKE, 394 | RIGHT, 395 | RLIKE, 396 | ROLE, 397 | ROLLBACK, 398 | ROLLUP, 399 | ROW, 400 | ROWID, 401 | ROWS, 402 | ROW_NUMBER, 403 | SAVEPOINT, 404 | SCHEMA, 405 | SCOPE, 406 | SCROLL, 407 | SEARCH, 408 | SECOND, 409 | SELECT, 410 | SENSITIVE, 411 | SEQUENCE, 412 | SEQUENCEFILE, 413 | SEQUENCES, 414 | SERDE, 415 | SERIALIZABLE, 416 | SESSION, 417 | SESSION_USER, 418 | SET, 419 | SETS, 420 | SHOW, 421 | SIMILAR, 422 | SMALLINT, 423 | SNAPSHOT, 424 | SOME, 425 | SORT, 426 | SPECIFIC, 427 | SPECIFICTYPE, 428 | SQL, 429 | SQLEXCEPTION, 430 | SQLSTATE, 431 | SQLWARNING, 432 | SQRT, 433 | STAGE, 434 | START, 435 | STATIC, 436 | STATISTICS, 437 | STDDEV_POP, 438 | STDDEV_SAMP, 439 | STDIN, 440 | STORED, 441 | STRING, 442 | SUBMULTISET, 443 | SUBSTRING, 444 | SUBSTRING_REGEX, 445 | SUCCEEDS, 446 | SUM, 447 | SUPER, 448 | SYMMETRIC, 449 | SYNC, 450 | SYSTEM, 451 | SYSTEM_TIME, 452 | SYSTEM_USER, 453 | TABLE, 454 | TABLES, 455 | TABLESAMPLE, 456 | TBLPROPERTIES, 457 | TEMP, 458 | TEMPORARY, 459 | TEXT, 460 | TEXTFILE, 461 | THEN, 462 | TIES, 463 | TIME, 464 | TIMESTAMP, 465 | TIMEZONE_HOUR, 466 | TIMEZONE_MINUTE, 467 | TINYINT, 468 | TO, 469 | TOP, 470 | TRAILING, 471 | TRANSACTION, 472 | TRANSLATE, 473 | TRANSLATE_REGEX, 474 | TRANSLATION, 475 | TREAT, 476 | TRIGGER, 477 | TRIM, 478 | TRIM_ARRAY, 479 | TRUE, 480 | TRUNCATE, 481 | TRY_CAST, 482 | TUPLE, 483 | UESCAPE, 484 | UNBOUNDED, 485 | UNCOMMITTED, 486 | UNION, 487 | UNIQUE, 488 | UNKNOWN, 489 | UNNEST, 490 | UNSIGNED, 491 | UPDATE, 492 | UPPER, 493 | USAGE, 494 | USER, 495 | USING, 496 | UUID, 497 | VALUE, 498 | VALUES, 499 | VALUE_OF, 500 | VARBINARY, 501 | VARCHAR, 502 | VARYING, 503 | VAR_POP, 504 | VAR_SAMP, 505 | VERBOSE, 506 | VERSIONING, 507 | VIEW, 508 | VIRTUAL, 509 | WHEN, 510 | WHENEVER, 511 | WHERE, 512 | WIDTH_BUCKET, 513 | WINDOW, 514 | WITH, 515 | WITHIN, 516 | WITHOUT, 517 | WORK, 518 | WRITE, 519 | XOR, 520 | YEAR, 521 | ZONE 522 | ); 523 | 524 | /// These keywords can't be used as a table alias, so that `FROM table_name alias` 525 | /// can be parsed unambiguously without looking ahead. 526 | pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ 527 | // Reserved as both a table and a column alias: 528 | Keyword::WITH, 529 | Keyword::EXPLAIN, 530 | Keyword::ANALYZE, 531 | Keyword::SELECT, 532 | Keyword::WHERE, 533 | Keyword::GROUP, 534 | Keyword::SORT, 535 | Keyword::HAVING, 536 | Keyword::ORDER, 537 | Keyword::TOP, 538 | Keyword::LATERAL, 539 | Keyword::VIEW, 540 | Keyword::LIMIT, 541 | Keyword::OFFSET, 542 | Keyword::FETCH, 543 | Keyword::UNION, 544 | Keyword::EXCEPT, 545 | Keyword::INTERSECT, 546 | // Reserved only as a table alias in the `FROM`/`JOIN` clauses: 547 | Keyword::ON, 548 | Keyword::JOIN, 549 | Keyword::INNER, 550 | Keyword::CROSS, 551 | Keyword::FULL, 552 | Keyword::LEFT, 553 | Keyword::RIGHT, 554 | Keyword::NATURAL, 555 | Keyword::USING, 556 | Keyword::CLUSTER, 557 | Keyword::DISTRIBUTE, 558 | // for MSSQL-specific OUTER APPLY (seems reserved in most dialects) 559 | Keyword::OUTER, 560 | Keyword::SET, 561 | // for FORMAT 562 | Keyword::FORMAT, 563 | Keyword::PARQUET, 564 | ]; 565 | 566 | /// Can't be used as a column alias, so that `SELECT alias` 567 | /// can be parsed unambiguously without looking ahead. 568 | pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ 569 | // Reserved as both a table and a column alias: 570 | Keyword::WITH, 571 | Keyword::EXPLAIN, 572 | Keyword::ANALYZE, 573 | Keyword::SELECT, 574 | Keyword::WHERE, 575 | Keyword::GROUP, 576 | Keyword::SORT, 577 | Keyword::HAVING, 578 | Keyword::ORDER, 579 | Keyword::TOP, 580 | Keyword::LATERAL, 581 | Keyword::VIEW, 582 | Keyword::LIMIT, 583 | Keyword::OFFSET, 584 | Keyword::FETCH, 585 | Keyword::UNION, 586 | Keyword::EXCEPT, 587 | Keyword::INTERSECT, 588 | Keyword::CLUSTER, 589 | Keyword::DISTRIBUTE, 590 | // Reserved only as a column alias in the `SELECT` clause 591 | Keyword::FROM, 592 | Keyword::FORMAT, 593 | Keyword::PARQUET, 594 | ]; 595 | -------------------------------------------------------------------------------- /src/ast/ddl.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | //! AST types specific to CREATE/ALTER variants of [Statement] 14 | //! (commonly referred to as Data Definition Language, or DDL) 15 | 16 | #[cfg(not(feature = "std"))] 17 | use alloc::{boxed::Box, vec::Vec}; 18 | use core::fmt; 19 | 20 | #[cfg(feature = "serde")] 21 | use serde::{Deserialize, Serialize}; 22 | 23 | use crate::ast::{display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName}; 24 | use crate::tokenizer::Token; 25 | 26 | /// An `ALTER TABLE` (`Statement::AlterTable`) operation 27 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 28 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 29 | pub enum AlterTableOperation { 30 | /// `ADD ` 31 | AddConstraint(TableConstraint), 32 | /// `ADD [ COLUMN ] ` 33 | AddColumn { column_def: ColumnDef }, 34 | /// TODO: implement `DROP CONSTRAINT ` 35 | DropConstraint { name: Ident }, 36 | /// `DROP [ COLUMN ] [ IF EXISTS ] [ CASCADE ]` 37 | DropColumn { 38 | column_name: Ident, 39 | if_exists: bool, 40 | cascade: bool, 41 | }, 42 | /// `RENAME TO PARTITION (partition=val)` 43 | RenamePartitions { 44 | old_partitions: Vec, 45 | new_partitions: Vec, 46 | }, 47 | /// Add Partitions 48 | AddPartitions { 49 | if_not_exists: bool, 50 | new_partitions: Vec, 51 | }, 52 | DropPartitions { 53 | partitions: Vec, 54 | if_exists: bool, 55 | }, 56 | /// `RENAME [ COLUMN ] TO ` 57 | RenameColumn { 58 | old_column_name: Ident, 59 | new_column_name: Ident, 60 | }, 61 | /// `RENAME TO ` 62 | RenameTable { table_name: ObjectName }, 63 | // CHANGE [ COLUMN ] [ ] 64 | ChangeColumn { 65 | old_name: Ident, 66 | new_name: Ident, 67 | data_type: DataType, 68 | options: Vec, 69 | }, 70 | } 71 | 72 | impl fmt::Display for AlterTableOperation { 73 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 74 | match self { 75 | AlterTableOperation::AddPartitions { 76 | if_not_exists, 77 | new_partitions, 78 | } => write!( 79 | f, 80 | "ADD{ine} PARTITION ({})", 81 | display_comma_separated(new_partitions), 82 | ine = if *if_not_exists { " IF NOT EXISTS" } else { "" } 83 | ), 84 | AlterTableOperation::AddConstraint(c) => write!(f, "ADD {}", c), 85 | AlterTableOperation::AddColumn { column_def } => { 86 | write!(f, "ADD COLUMN {}", column_def) 87 | } 88 | AlterTableOperation::DropPartitions { 89 | partitions, 90 | if_exists, 91 | } => write!( 92 | f, 93 | "DROP{ie} PARTITION ({})", 94 | display_comma_separated(partitions), 95 | ie = if *if_exists { " IF EXISTS" } else { "" } 96 | ), 97 | AlterTableOperation::DropConstraint { name } => write!(f, "DROP CONSTRAINT {}", name), 98 | AlterTableOperation::DropColumn { 99 | column_name, 100 | if_exists, 101 | cascade, 102 | } => write!( 103 | f, 104 | "DROP COLUMN {}{}{}", 105 | if *if_exists { "IF EXISTS " } else { "" }, 106 | column_name, 107 | if *cascade { " CASCADE" } else { "" } 108 | ), 109 | AlterTableOperation::RenamePartitions { 110 | old_partitions, 111 | new_partitions, 112 | } => write!( 113 | f, 114 | "PARTITION ({}) RENAME TO PARTITION ({})", 115 | display_comma_separated(old_partitions), 116 | display_comma_separated(new_partitions) 117 | ), 118 | AlterTableOperation::RenameColumn { 119 | old_column_name, 120 | new_column_name, 121 | } => write!( 122 | f, 123 | "RENAME COLUMN {} TO {}", 124 | old_column_name, new_column_name 125 | ), 126 | AlterTableOperation::RenameTable { table_name } => { 127 | write!(f, "RENAME TO {}", table_name) 128 | } 129 | AlterTableOperation::ChangeColumn { 130 | old_name, 131 | new_name, 132 | data_type, 133 | options, 134 | } => { 135 | write!(f, "CHANGE COLUMN {} {} {}", old_name, new_name, data_type)?; 136 | if options.is_empty() { 137 | Ok(()) 138 | } else { 139 | write!(f, " {}", display_separated(options, " ")) 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | /// A table-level constraint, specified in a `CREATE TABLE` or an 147 | /// `ALTER TABLE ADD ` statement. 148 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 149 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 150 | pub enum TableConstraint { 151 | /// `[ CONSTRAINT ] { PRIMARY KEY | UNIQUE } ()` 152 | Unique { 153 | name: Option, 154 | columns: Vec, 155 | /// Whether this is a `PRIMARY KEY` or just a `UNIQUE` constraint 156 | is_primary: bool, 157 | }, 158 | /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () 159 | /// REFERENCES () 160 | /// { [ON DELETE ] [ON UPDATE ] | 161 | /// [ON UPDATE ] [ON DELETE ] 162 | /// }`). 163 | ForeignKey { 164 | name: Option, 165 | columns: Vec, 166 | foreign_table: ObjectName, 167 | referred_columns: Vec, 168 | on_delete: Option, 169 | on_update: Option, 170 | }, 171 | /// `[ CONSTRAINT ] CHECK ()` 172 | Check { 173 | name: Option, 174 | expr: Box, 175 | }, 176 | } 177 | 178 | impl fmt::Display for TableConstraint { 179 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 180 | match self { 181 | TableConstraint::Unique { 182 | name, 183 | columns, 184 | is_primary, 185 | } => write!( 186 | f, 187 | "{}{} ({})", 188 | display_constraint_name(name), 189 | if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }, 190 | display_comma_separated(columns) 191 | ), 192 | TableConstraint::ForeignKey { 193 | name, 194 | columns, 195 | foreign_table, 196 | referred_columns, 197 | on_delete, 198 | on_update, 199 | } => { 200 | write!( 201 | f, 202 | "{}FOREIGN KEY ({}) REFERENCES {}({})", 203 | display_constraint_name(name), 204 | display_comma_separated(columns), 205 | foreign_table, 206 | display_comma_separated(referred_columns), 207 | )?; 208 | if let Some(action) = on_delete { 209 | write!(f, " ON DELETE {}", action)?; 210 | } 211 | if let Some(action) = on_update { 212 | write!(f, " ON UPDATE {}", action)?; 213 | } 214 | Ok(()) 215 | } 216 | TableConstraint::Check { name, expr } => { 217 | write!(f, "{}CHECK ({})", display_constraint_name(name), expr) 218 | } 219 | } 220 | } 221 | } 222 | 223 | /// SQL column definition 224 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 225 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 226 | pub struct ColumnDef { 227 | pub name: Ident, 228 | pub data_type: DataType, 229 | pub collation: Option, 230 | pub options: Vec, 231 | } 232 | 233 | impl fmt::Display for ColumnDef { 234 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 235 | write!(f, "{} {}", self.name, self.data_type)?; 236 | for option in &self.options { 237 | write!(f, " {}", option)?; 238 | } 239 | Ok(()) 240 | } 241 | } 242 | 243 | /// An optionally-named `ColumnOption`: `[ CONSTRAINT ] `. 244 | /// 245 | /// Note that implementations are substantially more permissive than the ANSI 246 | /// specification on what order column options can be presented in, and whether 247 | /// they are allowed to be named. The specification distinguishes between 248 | /// constraints (NOT NULL, UNIQUE, PRIMARY KEY, and CHECK), which can be named 249 | /// and can appear in any order, and other options (DEFAULT, GENERATED), which 250 | /// cannot be named and must appear in a fixed order. PostgreSQL, however, 251 | /// allows preceding any option with `CONSTRAINT `, even those that are 252 | /// not really constraints, like NULL and DEFAULT. MSSQL is less permissive, 253 | /// allowing DEFAULT, UNIQUE, PRIMARY KEY and CHECK to be named, but not NULL or 254 | /// NOT NULL constraints (the last of which is in violation of the spec). 255 | /// 256 | /// For maximum flexibility, we don't distinguish between constraint and 257 | /// non-constraint options, lumping them all together under the umbrella of 258 | /// "column options," and we allow any column option to be named. 259 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 260 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 261 | pub struct ColumnOptionDef { 262 | pub name: Option, 263 | pub option: ColumnOption, 264 | } 265 | 266 | impl fmt::Display for ColumnOptionDef { 267 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 268 | write!(f, "{}{}", display_constraint_name(&self.name), self.option) 269 | } 270 | } 271 | 272 | /// `ColumnOption`s are modifiers that follow a column definition in a `CREATE 273 | /// TABLE` statement. 274 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 275 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 276 | pub enum ColumnOption { 277 | /// `NULL` 278 | Null, 279 | /// `NOT NULL` 280 | NotNull, 281 | /// `DEFAULT ` 282 | Default(Expr), 283 | /// `{ PRIMARY KEY | UNIQUE }` 284 | Unique { is_primary: bool }, 285 | /// A referential integrity constraint (`[FOREIGN KEY REFERENCES 286 | /// () 287 | /// { [ON DELETE ] [ON UPDATE ] | 288 | /// [ON UPDATE ] [ON DELETE ] 289 | /// }`). 290 | ForeignKey { 291 | foreign_table: ObjectName, 292 | referred_columns: Vec, 293 | on_delete: Option, 294 | on_update: Option, 295 | }, 296 | /// `CHECK ()` 297 | Check(Expr), 298 | /// Dialect-specific options, such as: 299 | /// - MySQL's `AUTO_INCREMENT` or SQLite's `AUTOINCREMENT` 300 | /// - ... 301 | DialectSpecific(Vec), 302 | } 303 | 304 | impl fmt::Display for ColumnOption { 305 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 306 | use ColumnOption::*; 307 | match self { 308 | Null => write!(f, "NULL"), 309 | NotNull => write!(f, "NOT NULL"), 310 | Default(expr) => write!(f, "DEFAULT {}", expr), 311 | Unique { is_primary } => { 312 | write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }) 313 | } 314 | ForeignKey { 315 | foreign_table, 316 | referred_columns, 317 | on_delete, 318 | on_update, 319 | } => { 320 | write!(f, "REFERENCES {}", foreign_table)?; 321 | if !referred_columns.is_empty() { 322 | write!(f, " ({})", display_comma_separated(referred_columns))?; 323 | } 324 | if let Some(action) = on_delete { 325 | write!(f, " ON DELETE {}", action)?; 326 | } 327 | if let Some(action) = on_update { 328 | write!(f, " ON UPDATE {}", action)?; 329 | } 330 | Ok(()) 331 | } 332 | Check(expr) => write!(f, "CHECK ({})", expr), 333 | DialectSpecific(val) => write!(f, "{}", display_separated(val, " ")), 334 | } 335 | } 336 | } 337 | 338 | fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { 339 | struct ConstraintName<'a>(&'a Option); 340 | impl<'a> fmt::Display for ConstraintName<'a> { 341 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 342 | if let Some(name) = self.0 { 343 | write!(f, "CONSTRAINT {} ", name)?; 344 | } 345 | Ok(()) 346 | } 347 | } 348 | ConstraintName(name) 349 | } 350 | 351 | /// ` = 352 | /// { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` 353 | /// 354 | /// Used in foreign key constraints in `ON UPDATE` and `ON DELETE` options. 355 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 356 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 357 | pub enum ReferentialAction { 358 | Restrict, 359 | Cascade, 360 | SetNull, 361 | NoAction, 362 | SetDefault, 363 | } 364 | 365 | impl fmt::Display for ReferentialAction { 366 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 367 | f.write_str(match self { 368 | ReferentialAction::Restrict => "RESTRICT", 369 | ReferentialAction::Cascade => "CASCADE", 370 | ReferentialAction::SetNull => "SET NULL", 371 | ReferentialAction::NoAction => "NO ACTION", 372 | ReferentialAction::SetDefault => "SET DEFAULT", 373 | }) 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project aims to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | Given that the parser produces a typed AST, any changes to the AST will technically be breaking and thus will result in a `0.(N+1)` version. We document changes that break via addition as "Added". 7 | 8 | ## [Unreleased] 9 | Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. 10 | 11 | 12 | ## [0.13.0] 2020-12-10 13 | 14 | ### Added 15 | * Add ALTER TABLE CHANGE COLUMN, extend the UPDATE statement with ON clause (#375) - Thanks @0xA537FD! 16 | * Add support for GROUPIING SETS, ROLLUP and CUBE - Thanks @Jimexist! 17 | * Add basic support for GRANT and REVOKE (#365) - Thanks @blx! 18 | 19 | ### Changed 20 | * Use Rust 2021 edition (#368) - Thanks @Jimexist! 21 | 22 | ### Fixed 23 | * Fix clippy errors (#367, #374) - Thanks @Jimexist! 24 | 25 | 26 | ## [0.12.0] 2020-10-14 27 | 28 | ### Added 29 | * Add support for [NOT] IS DISTINCT FROM (#306) - @Dandandan 30 | 31 | ### Changed 32 | * Move the keywords module - Thanks @koushiro! 33 | 34 | 35 | ## [0.11.0] 2020-09-24 36 | 37 | ### Added 38 | * Support minimum display width for integer data types (#337) Thanks @vasilev-alex! 39 | * Add logical XOR operator (#357) - Thanks @xzmrdltl! 40 | * Support DESCRIBE table_name (#340) - Thanks @ovr! 41 | * Support SHOW CREATE TABLE|EVENT|FUNCTION (#338) - Thanks @ovr! 42 | * Add referential actions to TableConstraint foreign key (#306) - Thanks @joshwd36! 43 | 44 | ### Changed 45 | * Enable map access for numbers, multiple nesting levels (#356) - Thanks @Igosuki! 46 | * Rename Token::Mult to Token::Mul (#353) - Thanks @koushiro! 47 | * Use derive(Default) for HiveFormat (#348) - Thanks @koushiro! 48 | * Improve tokenizer error (#347) - Thanks @koushiro! 49 | * Eliminate redundant string copy in Tokenizer (#343) - Thanks @koushiro! 50 | * Update bigdecimal requirement from 0.2 to 0.3 dependencies (#341) 51 | * Support parsing hexadecimal literals that start with `0x` (#324) - Thanks @TheSchemm! 52 | 53 | 54 | ## [0.10.0] 2020-08-23 55 | 56 | ### Added 57 | * Support for `no_std` (#332) - Thanks @koushiro! 58 | * Postgres regular expression operators (`~`, `~*`, `!~`, `!~*`) (#328) - Thanks @b41sh! 59 | * tinyint (#320) - Thanks @sundy-li 60 | * ILIKE (#300) - Thanks @maxcountryman! 61 | * TRIM syntax (#331, #334) - Thanks ever0de 62 | 63 | 64 | ### Fixed 65 | * Return error instead of panic (#316) - Thanks @BohuTANG! 66 | 67 | ### Changed 68 | - Rename `Modulus` to `Modulo` (#335) - Thanks @RGRAVITY817! 69 | - Update links to reflect repository move to `sqlparser-rs` GitHub org (#333) - Thanks @andygrove 70 | - Add default value for `WindowFrame` (#313) - Thanks @Jimexist! 71 | 72 | ## [0.9.0] 2020-03-21 73 | 74 | ### Added 75 | * Add support for `TRY_CAST` syntax (#299) - Thanks @seddonm1! 76 | 77 | ## [0.8.0] 2020-02-20 78 | 79 | ### Added 80 | * Introduce Hive QL dialect `HiveDialect` and syntax (#235) - Thanks @hntd187! 81 | * Add `SUBSTRING(col [FROM ] [FOR ])` syntax (#293) 82 | * Support parsing floats without leading digits `.01` (#294) 83 | * Support parsing multiple show variables (#290) - Thanks @francis-du! 84 | * Support SQLite `INSERT OR [..]` syntax (#281) - Thanks @zhangli-pear! 85 | 86 | ## [0.7.0] 2020-12-28 87 | 88 | ### Changed 89 | - Change the MySQL dialect to support `` `identifiers` `` quoted with backticks instead of the standard `"double-quoted"` identifiers (#247) - thanks @mashuai! 90 | - Update bigdecimal requirement from 0.1 to 0.2 (#268) 91 | 92 | ### Added 93 | - Enable dialect-specific behaviours in the parser (`dialect_of!()`) (#254) - thanks @eyalleshem! 94 | - Support named arguments in function invocations (`ARG_NAME => val`) (#250) - thanks @eyalleshem! 95 | - Support `TABLE()` functions in `FROM` (#253) - thanks @eyalleshem! 96 | - Support Snowflake's single-line comments starting with '#' or '//' (#264) - thanks @eyalleshem! 97 | - Support PostgreSQL `PREPARE`, `EXECUTE`, and `DEALLOCATE` (#243) - thanks @silathdiir! 98 | - Support PostgreSQL math operators (#267) - thanks @alex-dukhno! 99 | - Add SQLite dialect (#248) - thanks @mashuai! 100 | - Add Snowflake dialect (#259) - thanks @eyalleshem! 101 | - Support for Recursive CTEs - thanks @rhanqtl! 102 | - Support `FROM (table_name) alias` syntax - thanks @eyalleshem! 103 | - Support for `EXPLAIN [ANALYZE] VERBOSE` - thanks @ovr! 104 | - Support `ANALYZE TABLE` 105 | - DDL: 106 | - Support `OR REPLACE` in `CREATE VIEW`/`TABLE` (#239) - thanks @Dandandan! 107 | - Support specifying `ASC`/`DESC` in index columns (#249) - thanks @mashuai! 108 | - Support SQLite `AUTOINCREMENT` and MySQL `AUTO_INCREMENT` column option in `CREATE TABLE` (#234) - thanks @mashuai! 109 | - Support PostgreSQL `IF NOT EXISTS` for `CREATE SCHEMA` (#276) - thanks @alex-dukhno! 110 | 111 | ### Fixed 112 | - Fix a typo in `JSONFILE` serialization, introduced in 0.3.1 (#237) 113 | - Change `CREATE INDEX` serialization to not end with a semicolon, introduced in 0.5.1 (#245) 114 | - Don't fail parsing `ALTER TABLE ADD COLUMN` ending with a semicolon, introduced in 0.5.1 (#246) - thanks @mashuai 115 | 116 | ## [0.6.1] - 2020-07-20 117 | 118 | ### Added 119 | - Support BigQuery `ASSERT` statement (#226) 120 | 121 | ## [0.6.0] - 2020-07-20 122 | 123 | ### Added 124 | - Support SQLite's `CREATE TABLE (...) WITHOUT ROWID` (#208) - thanks @mashuai! 125 | - Support SQLite's `CREATE VIRTUAL TABLE` (#209) - thanks @mashuai! 126 | 127 | ## [0.5.1] - 2020-06-26 128 | This release should have been called `0.6`, as it introduces multiple incompatible changes to the API. If you don't want to upgrade yet, you can revert to the previous version by changing your `Cargo.toml` to: 129 | 130 | sqlparser = "= 0.5.0" 131 | 132 | 133 | ### Changed 134 | - **`Parser::parse_sql` now accepts a `&str` instead of `String` (#182)** - thanks @Dandandan! 135 | - Change `Ident` (previously a simple `String`) to store the parsed (unquoted) `value` of the identifier and the `quote_style` separately (#143) - thanks @apparebit! 136 | - Support Snowflake's `FROM (table_name)` (#155) - thanks @eyalleshem! 137 | - Add line and column number to TokenizerError (#194) - thanks @Dandandan! 138 | - Use Token::EOF instead of Option (#195) 139 | - Make the units keyword following `INTERVAL '...'` optional (#184) - thanks @maxcountryman! 140 | - Generalize `DATE`/`TIME`/`TIMESTAMP` literals representation in the AST (`TypedString { data_type, value }`) and allow `DATE` and other keywords to be used as identifiers when not followed by a string (#187) - thanks @maxcountryman! 141 | - Output DataType capitalized (`fmt::Display`) (#202) - thanks @Dandandan! 142 | 143 | ### Added 144 | - Support MSSQL `TOP () [ PERCENT ] [ WITH TIES ]` (#150) - thanks @alexkyllo! 145 | - Support MySQL `LIMIT row_count OFFSET offset` (not followed by `ROW` or `ROWS`) and remember which variant was parsed (#158) - thanks @mjibson! 146 | - Support PostgreSQL `CREATE TABLE IF NOT EXISTS table_name` (#163) - thanks @alex-dukhno! 147 | - Support basic forms of `CREATE INDEX` and `DROP INDEX` (#167) - thanks @mashuai! 148 | - Support `ON { UPDATE | DELETE } { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` in `FOREIGN KEY` constraints (#170) - thanks @c7hm4r! 149 | - Support basic forms of `CREATE SCHEMA` and `DROP SCHEMA` (#173) - thanks @alex-dukhno! 150 | - Support `NULLS FIRST`/`LAST` in `ORDER BY` expressions (#176) - thanks @houqp! 151 | - Support `LISTAGG()` (#174) - thanks @maxcountryman! 152 | - Support the string concatentation operator `||` (#178) - thanks @Dandandan! 153 | - Support bitwise AND (`&`), OR (`|`), XOR (`^`) (#181) - thanks @Dandandan! 154 | - Add serde support to AST structs and enums (#196) - thanks @panarch! 155 | - Support `ALTER TABLE ADD COLUMN`, `RENAME COLUMN`, and `RENAME TO` (#203) - thanks @mashuai! 156 | - Support `ALTER TABLE DROP COLUMN` (#148) - thanks @ivanceras! 157 | - Support `CREATE TABLE ... AS ...` (#206) - thanks @Dandandan! 158 | 159 | ### Fixed 160 | - Report an error for unterminated string literals (#165) 161 | - Make file format (`STORED AS`) case insensitive (#200) and don't allow quoting it (#201) - thanks @Dandandan! 162 | 163 | ## [0.5.0] - 2019-10-10 164 | 165 | ### Changed 166 | - Replace the `Value::Long(u64)` and `Value::Double(f64)` variants with `Value::Number(String)` to avoid losing precision when parsing decimal literals (#130) - thanks @benesch! 167 | - `--features bigdecimal` can be enabled to work with `Value::Number(BigDecimal)` instead, at the cost of an additional dependency. 168 | 169 | ### Added 170 | - Support MySQL `SHOW COLUMNS`, `SET =`, and `SHOW ` statements (#135) - thanks @quodlibetor and @benesch! 171 | 172 | ### Fixed 173 | - Don't fail to parse `START TRANSACTION` followed by a semicolon (#139) - thanks @gaffneyk! 174 | 175 | 176 | ## [0.4.0] - 2019-07-02 177 | This release brings us closer to SQL-92 support, mainly thanks to the improvements contributed back from @MaterializeInc's fork and other work by @benesch. 178 | 179 | ### Changed 180 | - Remove "SQL" from type and enum variant names, `SQLType` -> `DataType`, remove "sql" prefix from module names (#105, #122) 181 | - Rename `ASTNode` -> `Expr` (#119) 182 | - Improve consistency of binary/unary op nodes (#112): 183 | - `ASTNode::SQLBinaryExpr` is now `Expr::BinaryOp` and `ASTNode::SQLUnary` is `Expr::UnaryOp`; 184 | - The `op: SQLOperator` field is now either a `BinaryOperator` or an `UnaryOperator`. 185 | - Change the representation of JOINs to match the standard (#109): `SQLSelect`'s `relation` and `joins` are replaced with `from: Vec`. Before this change `FROM foo NATURAL JOIN bar, baz` was represented as "foo" as the `relation` followed by two joins (`Inner(Natural)` and `Implicit`); now it's two `TableWithJoins` (`foo NATURAL JOIN bar` and `baz`). 186 | - Extract a `SQLFunction` struct (#89) 187 | - Replace `Option>` with `Vec` in the AST structs (#73) 188 | - Change `Value::Long()` to be unsigned, use u64 consistently (#65) 189 | 190 | ### Added 191 | - Infra: 192 | - Implement `fmt::Display` on AST nodes (#124) - thanks @vemoo! 193 | - Implement `Hash` (#88) and `Eq` (#123) on all AST nodes 194 | - Implement `std::error::Error` for `ParserError` (#72) 195 | - Handle Windows line-breaks (#54) 196 | - Expressions: 197 | - Support `INTERVAL` literals (#103) 198 | - Support `DATE` / `TIME` / `TIMESTAMP` literals (#99) 199 | - Support `EXTRACT` (#96) 200 | - Support `X'hex value'` literals (#95) 201 | - Support `EXISTS` subqueries (#90) 202 | - Support nested expressions in `BETWEEN` (#80) 203 | - Support `COUNT(DISTINCT x)` and similar (#77) 204 | - Support `CASE operand WHEN expected_value THEN ..` and table-valued functions (#59) 205 | - Support analytic (window) functions (`OVER` clause) (#50) 206 | - Queries / DML: 207 | - Support nested joins (#100) and derived tables with set operations (#111) 208 | - Support `UPDATE` statements (#97) 209 | - Support `INSERT INTO foo SELECT * FROM bar` and `FROM VALUES (...)` (#91) 210 | - Support `SELECT ALL` (#76) 211 | - Add `FETCH` and `OFFSET` support, and `LATERAL` (#69) - thanks @thomas-jeepe! 212 | - Support `COLLATE`, optional column list in CTEs (#64) 213 | - DDL/TCL: 214 | - Support `START/SET/COMMIT/ROLLBACK TRANSACTION` (#106) - thanks @SamuelMarks! 215 | - Parse column constraints in any order (#93) 216 | - Parse `DECIMAL` and `DEC` aliases for `NUMERIC` type (#92) 217 | - Support `DROP [TABLE|VIEW]` (#75) 218 | - Support arbitrary `WITH` options for `CREATE [TABLE|VIEW]` (#74) 219 | - Support constraints in `CREATE TABLE` (#65) 220 | - Add basic MSSQL dialect (#61) and some MSSQL-specific features: 221 | - `CROSS`/`OUTER APPLY` (#120) 222 | - MSSQL identifier and alias parsing rules (#66) 223 | - `WITH` hints (#59) 224 | 225 | ### Fixed 226 | - Report an error for `SELECT * FROM a OUTER JOIN b` instead of parsing `OUTER` as an alias (#118) 227 | - Fix the precedence of `NOT LIKE` (#82) and unary `NOT` (#107) 228 | - Do not panic when `NOT` is not followed by an expected keyword (#71) 229 | successfully instead of returning a parse error - thanks @ivanceras! (#67) - and similar fixes for queries with no `FROM` (#116) 230 | - Fix issues with `ALTER TABLE ADD CONSTRAINT` parsing (#65) 231 | - Serialize the "not equals" operator as `<>` instead of `!=` (#64) 232 | - Remove dependencies on `uuid` (#59) and `chrono` (#61) 233 | - Make `SELECT` query with `LIMIT` clause but no `WHERE` parse - Fix incorrect behavior of `ASTNode::SQLQualifiedWildcard::to_string()` (returned `foo*` instead of `foo.*`) - thanks @thomas-jeepe! (#52) 234 | 235 | ## [0.3.1] - 2019-04-20 236 | ### Added 237 | - Extended `SQLStatement::SQLCreateTable` to support Hive's EXTERNAL TABLES (`CREATE EXTERNAL TABLE .. STORED AS .. LOCATION '..'`) - thanks @zhzy0077! (#46) 238 | - Parse `SELECT DISTINCT` to `SQLSelect::distinct` (#49) 239 | 240 | ## [0.3.0] - 2019-04-03 241 | ### Changed 242 | This release includes major changes to the AST structs to add a number of features, as described in #37 and #43. In particular: 243 | - `ASTNode` variants that represent statements were extracted from `ASTNode` into a separate `SQLStatement` enum; 244 | - `Parser::parse_sql` now returns a `Vec` of parsed statements. 245 | - `ASTNode` now represents an expression (renamed to `Expr` in 0.4.0) 246 | - The query representation (formerly `ASTNode::SQLSelect`) became more complicated to support: 247 | - `WITH` and `UNION`/`EXCEPT`/`INTERSECT` (via `SQLQuery`, `Cte`, and `SQLSetExpr`), 248 | - aliases and qualified wildcards in `SELECT` (via `SQLSelectItem`), 249 | - and aliases in `FROM`/`JOIN` (via `TableFactor`). 250 | - A new `SQLObjectName` struct is used instead of `String` or `ASTNode::SQLCompoundIdentifier` - for objects like tables, custom types, etc. 251 | - Added support for "delimited identifiers" and made keywords context-specific (thus accepting them as valid identifiers in most contexts) - **this caused a regression in parsing `SELECT .. FROM .. LIMIT ..` (#67), fixed in 0.4.0** 252 | 253 | ### Added 254 | Other than the changes listed above, some less intrusive additions include: 255 | - Support `CREATE [MATERIALIZED] VIEW` statement 256 | - Support `IN`, `BETWEEN`, unary +/- in epressions 257 | - Support `CHAR` data type and `NUMERIC` not followed by `(p,s)`. 258 | - Support national string literals (`N'...'`) 259 | 260 | ## [0.2.4] - 2019-03-08 261 | Same as 0.2.2. 262 | 263 | ## [0.2.3] - 2019-03-08 [YANKED] 264 | 265 | ## [0.2.2] - 2019-03-08 266 | ### Changed 267 | - Removed `Value::String`, `Value::DoubleQuotedString`, and `Token::String`, making 268 | - `'...'` parse as a string literal (`Value::SingleQuotedString`), and 269 | - `"..."` fail to parse until version 0.3.0 (#36) 270 | 271 | ## [0.2.1] - 2019-01-13 272 | We don't have a changelog for the changes made in 2018, but thanks to @crw5996, @cswinter, @fredrikroos, @ivanceras, @nickolay, @virattara for their contributions in the early stages of the project! 273 | 274 | ## [0.1.0] - 2018-09-03 275 | Initial release 276 | -------------------------------------------------------------------------------- /tests/sqlparser_mysql.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #![warn(clippy::all)] 14 | //! Test SQL syntax specific to MySQL. The parser based on the generic dialect 15 | //! is also tested (on the inputs it can handle). 16 | 17 | #[macro_use] 18 | mod test_utils; 19 | 20 | use test_utils::*; 21 | 22 | use sqlparser::ast::*; 23 | use sqlparser::dialect::{GenericDialect, MySqlDialect}; 24 | use sqlparser::tokenizer::Token; 25 | 26 | #[test] 27 | fn parse_identifiers() { 28 | mysql().verified_stmt("SELECT $a$, àà"); 29 | } 30 | 31 | #[test] 32 | fn parse_show_columns() { 33 | let table_name = ObjectName(vec![Ident::new("mytable")]); 34 | assert_eq!( 35 | mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable"), 36 | Statement::ShowColumns { 37 | extended: false, 38 | full: false, 39 | table_name: table_name.clone(), 40 | filter: None, 41 | } 42 | ); 43 | assert_eq!( 44 | mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mydb.mytable"), 45 | Statement::ShowColumns { 46 | extended: false, 47 | full: false, 48 | table_name: ObjectName(vec![Ident::new("mydb"), Ident::new("mytable")]), 49 | filter: None, 50 | } 51 | ); 52 | assert_eq!( 53 | mysql_and_generic().verified_stmt("SHOW EXTENDED COLUMNS FROM mytable"), 54 | Statement::ShowColumns { 55 | extended: true, 56 | full: false, 57 | table_name: table_name.clone(), 58 | filter: None, 59 | } 60 | ); 61 | assert_eq!( 62 | mysql_and_generic().verified_stmt("SHOW FULL COLUMNS FROM mytable"), 63 | Statement::ShowColumns { 64 | extended: false, 65 | full: true, 66 | table_name: table_name.clone(), 67 | filter: None, 68 | } 69 | ); 70 | assert_eq!( 71 | mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable LIKE 'pattern'"), 72 | Statement::ShowColumns { 73 | extended: false, 74 | full: false, 75 | table_name: table_name.clone(), 76 | filter: Some(ShowStatementFilter::Like("pattern".into())), 77 | } 78 | ); 79 | assert_eq!( 80 | mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable WHERE 1 = 2"), 81 | Statement::ShowColumns { 82 | extended: false, 83 | full: false, 84 | table_name, 85 | filter: Some(ShowStatementFilter::Where( 86 | mysql_and_generic().verified_expr("1 = 2") 87 | )), 88 | } 89 | ); 90 | mysql_and_generic() 91 | .one_statement_parses_to("SHOW FIELDS FROM mytable", "SHOW COLUMNS FROM mytable"); 92 | mysql_and_generic() 93 | .one_statement_parses_to("SHOW COLUMNS IN mytable", "SHOW COLUMNS FROM mytable"); 94 | mysql_and_generic() 95 | .one_statement_parses_to("SHOW FIELDS IN mytable", "SHOW COLUMNS FROM mytable"); 96 | 97 | // unhandled things are truly unhandled 98 | match mysql_and_generic().parse_sql_statements("SHOW COLUMNS FROM mytable FROM mydb") { 99 | Err(_) => {} 100 | Ok(val) => panic!("unexpected successful parse: {:?}", val), 101 | } 102 | } 103 | 104 | #[test] 105 | fn parse_show_create() { 106 | let obj_name = ObjectName(vec![Ident::new("myident")]); 107 | 108 | for obj_type in &vec![ 109 | ShowCreateObject::Table, 110 | ShowCreateObject::Trigger, 111 | ShowCreateObject::Event, 112 | ShowCreateObject::Function, 113 | ShowCreateObject::Procedure, 114 | ] { 115 | assert_eq!( 116 | mysql_and_generic().verified_stmt(format!("SHOW CREATE {} myident", obj_type).as_str()), 117 | Statement::ShowCreate { 118 | obj_type: obj_type.clone(), 119 | obj_name: obj_name.clone(), 120 | } 121 | ); 122 | } 123 | } 124 | 125 | #[test] 126 | fn parse_create_table_auto_increment() { 127 | let sql = "CREATE TABLE foo (bar INT PRIMARY KEY AUTO_INCREMENT)"; 128 | match mysql().verified_stmt(sql) { 129 | Statement::CreateTable { name, columns, .. } => { 130 | assert_eq!(name.to_string(), "foo"); 131 | assert_eq!( 132 | vec![ColumnDef { 133 | name: Ident::new("bar"), 134 | data_type: DataType::Int(None), 135 | collation: None, 136 | options: vec![ 137 | ColumnOptionDef { 138 | name: None, 139 | option: ColumnOption::Unique { is_primary: true }, 140 | }, 141 | ColumnOptionDef { 142 | name: None, 143 | option: ColumnOption::DialectSpecific(vec![Token::make_keyword( 144 | "AUTO_INCREMENT" 145 | )]), 146 | }, 147 | ], 148 | }], 149 | columns 150 | ); 151 | } 152 | _ => unreachable!(), 153 | } 154 | } 155 | 156 | #[test] 157 | fn parse_create_table_with_column_comment() { 158 | let sql = "CREATE TABLE foo (bar INT COMMENT 'bar')"; 159 | match mysql().verified_stmt(sql) { 160 | Statement::CreateTable { name, columns, .. } => { 161 | assert_eq!(name.to_string(), "foo"); 162 | assert_eq!( 163 | vec![ColumnDef { 164 | name: Ident::new("bar"), 165 | data_type: DataType::Int(None), 166 | collation: None, 167 | options: vec![ColumnOptionDef { 168 | name: None, 169 | option: ColumnOption::DialectSpecific(vec![ 170 | Token::make_keyword("COMMENT"), 171 | Token::make_word("bar", Some('\'')) 172 | ]), 173 | },], 174 | }], 175 | columns 176 | ); 177 | } 178 | _ => unreachable!(), 179 | } 180 | } 181 | 182 | #[test] 183 | fn parse_quote_identifiers() { 184 | let sql = "CREATE TABLE `PRIMARY` (`BEGIN` INT PRIMARY KEY)"; 185 | match mysql().verified_stmt(sql) { 186 | Statement::CreateTable { name, columns, .. } => { 187 | assert_eq!(name.to_string(), "`PRIMARY`"); 188 | assert_eq!( 189 | vec![ColumnDef { 190 | name: Ident::with_quote('`', "BEGIN"), 191 | data_type: DataType::Int(None), 192 | collation: None, 193 | options: vec![ColumnOptionDef { 194 | name: None, 195 | option: ColumnOption::Unique { is_primary: true }, 196 | }], 197 | }], 198 | columns 199 | ); 200 | } 201 | _ => unreachable!(), 202 | } 203 | } 204 | 205 | #[test] 206 | fn parse_create_table_with_minimum_display_width() { 207 | let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3), bar_smallint SMALLINT(5), bar_int INT(11), bar_bigint BIGINT(20))"; 208 | match mysql().verified_stmt(sql) { 209 | Statement::CreateTable { name, columns, .. } => { 210 | assert_eq!(name.to_string(), "foo"); 211 | assert_eq!( 212 | vec![ 213 | ColumnDef { 214 | name: Ident::new("bar_tinyint"), 215 | data_type: DataType::TinyInt(Some(3)), 216 | collation: None, 217 | options: vec![], 218 | }, 219 | ColumnDef { 220 | name: Ident::new("bar_smallint"), 221 | data_type: DataType::SmallInt(Some(5)), 222 | collation: None, 223 | options: vec![], 224 | }, 225 | ColumnDef { 226 | name: Ident::new("bar_int"), 227 | data_type: DataType::Int(Some(11)), 228 | collation: None, 229 | options: vec![], 230 | }, 231 | ColumnDef { 232 | name: Ident::new("bar_bigint"), 233 | data_type: DataType::BigInt(Some(20)), 234 | collation: None, 235 | options: vec![], 236 | } 237 | ], 238 | columns 239 | ); 240 | } 241 | _ => unreachable!(), 242 | } 243 | } 244 | 245 | #[test] 246 | fn parse_create_table_unsigned() { 247 | let sql = "CREATE TABLE foo (bar_tinyint TINYINT(3) UNSIGNED, bar_smallint SMALLINT(5) UNSIGNED, bar_int INT(11) UNSIGNED, bar_bigint BIGINT(20) UNSIGNED)"; 248 | match mysql().verified_stmt(sql) { 249 | Statement::CreateTable { name, columns, .. } => { 250 | assert_eq!(name.to_string(), "foo"); 251 | assert_eq!( 252 | vec![ 253 | ColumnDef { 254 | name: Ident::new("bar_tinyint"), 255 | data_type: DataType::UnsignedTinyInt(Some(3)), 256 | collation: None, 257 | options: vec![], 258 | }, 259 | ColumnDef { 260 | name: Ident::new("bar_smallint"), 261 | data_type: DataType::UnsignedSmallInt(Some(5)), 262 | collation: None, 263 | options: vec![], 264 | }, 265 | ColumnDef { 266 | name: Ident::new("bar_int"), 267 | data_type: DataType::UnsignedInt(Some(11)), 268 | collation: None, 269 | options: vec![], 270 | }, 271 | ColumnDef { 272 | name: Ident::new("bar_bigint"), 273 | data_type: DataType::UnsignedBigInt(Some(20)), 274 | collation: None, 275 | options: vec![], 276 | }, 277 | ], 278 | columns 279 | ); 280 | } 281 | _ => unreachable!(), 282 | } 283 | } 284 | 285 | #[test] 286 | #[cfg(not(feature = "bigdecimal"))] 287 | fn parse_simple_insert() { 288 | let sql = r"INSERT INTO tasks (title, priority) VALUES ('Test Some Inserts', 1), ('Test Entry 2', 2), ('Test Entry 3', 3)"; 289 | 290 | match mysql().verified_stmt(sql) { 291 | Statement::Insert { 292 | table_name, 293 | columns, 294 | source, 295 | on, 296 | .. 297 | } => { 298 | assert_eq!(ObjectName(vec![Ident::new("tasks")]), table_name); 299 | assert_eq!(vec![Ident::new("title"), Ident::new("priority")], columns); 300 | assert!(on.is_none()); 301 | assert_eq!( 302 | Some(Box::new(Query { 303 | with: None, 304 | body: SetExpr::Values(Values(vec![ 305 | vec![ 306 | Expr::Value(Value::SingleQuotedString("Test Some Inserts".to_string())), 307 | Expr::Value(Value::Number("1".to_string(), false)) 308 | ], 309 | vec![ 310 | Expr::Value(Value::SingleQuotedString("Test Entry 2".to_string())), 311 | Expr::Value(Value::Number("2".to_string(), false)) 312 | ], 313 | vec![ 314 | Expr::Value(Value::SingleQuotedString("Test Entry 3".to_string())), 315 | Expr::Value(Value::Number("3".to_string(), false)) 316 | ] 317 | ])), 318 | order_by: vec![], 319 | limit: None, 320 | offset: None, 321 | fetch: None, 322 | format: None, 323 | })), 324 | source, 325 | ); 326 | } 327 | _ => unreachable!(), 328 | } 329 | 330 | let sql = r"INSERT INTO array_test (nums) VALUES ([]), ([1, 2, 3]), ([4, 5, 6])"; 331 | match mysql().verified_stmt(sql) { 332 | Statement::Insert { 333 | table_name, 334 | columns, 335 | source, 336 | on, 337 | .. 338 | } => { 339 | assert_eq!(ObjectName(vec![Ident::new("array_test")]), table_name); 340 | assert_eq!(vec![Ident::new("nums")], columns); 341 | assert!(on.is_none()); 342 | assert_eq!( 343 | Some(Box::new(Query { 344 | with: None, 345 | body: SetExpr::Values(Values(vec![ 346 | vec![Expr::Array(vec![]),], 347 | vec![Expr::Array(vec![ 348 | Expr::Value(number("1")), 349 | Expr::Value(number("2")), 350 | Expr::Value(number("3")) 351 | ]),], 352 | vec![Expr::Array(vec![ 353 | Expr::Value(number("4")), 354 | Expr::Value(number("5")), 355 | Expr::Value(number("6")) 356 | ]),], 357 | ])), 358 | order_by: vec![], 359 | limit: None, 360 | offset: None, 361 | fetch: None, 362 | format: None, 363 | })), 364 | source, 365 | ); 366 | } 367 | _ => unreachable!(), 368 | } 369 | } 370 | 371 | #[test] 372 | fn parse_double_quoted() { 373 | let sql_double_quoted = 374 | r#"INSERT INTO tasks (title, priority) VALUES ("Test S\"o\"m'e' In'se\"rt`s\"", 1)"#; 375 | let mut double_statements = mysql().parse_sql_statements(sql_double_quoted).unwrap(); 376 | assert_eq!(double_statements.len(), 1); 377 | let double_statement = double_statements.pop().unwrap(); 378 | match double_statement { 379 | Statement::Insert { source, .. } => { 380 | assert_eq!( 381 | source.unwrap().body.to_string(), 382 | "VALUES (\"Test S\"o\"m'e' In'se\"rt`s\"\", 1)" 383 | ) 384 | } 385 | _ => unreachable!(), 386 | } 387 | 388 | let select_sql = r#"SELECT obj["c"][0] FROM t4"#; 389 | mysql().verified_stmt(select_sql); 390 | 391 | let sql = r#"SELECT * FROM customers WHERE name LIKE "%a" IS NULL"#; 392 | let select = mysql().verified_only_select(sql); 393 | assert_eq!( 394 | Expr::IsNull(Box::new(Expr::BinaryOp { 395 | left: Box::new(Expr::Identifier(Ident::new("name"))), 396 | op: BinaryOperator::Like, 397 | right: Box::new(Expr::Value(Value::DoubleQuotedString("%a".to_string()))), 398 | })), 399 | select.selection.unwrap() 400 | ); 401 | } 402 | 403 | #[test] 404 | fn parse_update_with_joins() { 405 | let sql = "UPDATE orders AS o JOIN customers AS c ON o.customer_id = c.id SET o.completed = true WHERE c.firstname = 'Peter'"; 406 | match mysql().verified_stmt(sql) { 407 | Statement::Update { 408 | table, 409 | assignments, 410 | selection, 411 | } => { 412 | assert_eq!( 413 | TableWithJoins { 414 | relation: TableFactor::Table { 415 | name: ObjectName(vec![Ident::new("orders")]), 416 | alias: Some(TableAlias { 417 | name: Ident::new("o"), 418 | columns: vec![] 419 | }), 420 | args: vec![], 421 | with_hints: vec![], 422 | instant: None, 423 | }, 424 | joins: vec![Join { 425 | relation: TableFactor::Table { 426 | name: ObjectName(vec![Ident::new("customers")]), 427 | alias: Some(TableAlias { 428 | name: Ident::new("c"), 429 | columns: vec![] 430 | }), 431 | args: vec![], 432 | with_hints: vec![], 433 | instant: None, 434 | }, 435 | join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { 436 | left: Box::new(Expr::CompoundIdentifier(vec![ 437 | Ident::new("o"), 438 | Ident::new("customer_id") 439 | ])), 440 | op: BinaryOperator::Eq, 441 | right: Box::new(Expr::CompoundIdentifier(vec![ 442 | Ident::new("c"), 443 | Ident::new("id") 444 | ])) 445 | })), 446 | }] 447 | }, 448 | table 449 | ); 450 | assert_eq!( 451 | vec![Assignment { 452 | id: vec![Ident::new("o"), Ident::new("completed")], 453 | value: Expr::Value(Value::Boolean(true)) 454 | }], 455 | assignments 456 | ); 457 | assert_eq!( 458 | Some(Expr::BinaryOp { 459 | left: Box::new(Expr::CompoundIdentifier(vec![ 460 | Ident::new("c"), 461 | Ident::new("firstname") 462 | ])), 463 | op: BinaryOperator::Eq, 464 | right: Box::new(Expr::Value(Value::SingleQuotedString("Peter".to_string()))) 465 | }), 466 | selection 467 | ); 468 | } 469 | _ => unreachable!(), 470 | } 471 | } 472 | 473 | #[test] 474 | fn parse_alter_table_change_column() { 475 | let expected_name = ObjectName(vec![Ident::new("orders")]); 476 | let expected_operation = AlterTableOperation::ChangeColumn { 477 | old_name: Ident::new("description"), 478 | new_name: Ident::new("desc"), 479 | data_type: DataType::Text, 480 | options: vec![ColumnOption::NotNull], 481 | }; 482 | 483 | let sql1 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL"; 484 | match mysql().verified_stmt(sql1) { 485 | Statement::AlterTable { name, operation } => { 486 | assert_eq!(expected_name, name); 487 | assert_eq!(expected_operation, operation); 488 | } 489 | _ => unreachable!(), 490 | } 491 | 492 | let sql2 = "ALTER TABLE orders CHANGE description desc TEXT NOT NULL"; 493 | match mysql().one_statement_parses_to(sql2, sql1) { 494 | Statement::AlterTable { name, operation } => { 495 | assert_eq!(expected_name, name); 496 | assert_eq!(expected_operation, operation); 497 | } 498 | _ => unreachable!(), 499 | } 500 | } 501 | 502 | #[test] 503 | #[cfg(not(feature = "bigdecimal"))] 504 | fn parse_substring_in_select() { 505 | let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; 506 | match mysql().one_statement_parses_to( 507 | sql, 508 | "SELECT DISTINCT SUBSTRING(description FROM 0 FOR 1) FROM test", 509 | ) { 510 | Statement::Query(query) => { 511 | assert_eq!( 512 | Box::new(Query { 513 | with: None, 514 | body: SetExpr::Select(Box::new(Select { 515 | distinct: true, 516 | top: None, 517 | projection: vec![SelectItem::UnnamedExpr(Expr::Substring { 518 | expr: Box::new(Expr::Identifier(Ident { 519 | value: "description".to_string(), 520 | quote_style: None 521 | })), 522 | substring_from: Some(Box::new(Expr::Value(Value::Number( 523 | "0".to_string(), 524 | false 525 | )))), 526 | substring_for: Some(Box::new(Expr::Value(Value::Number( 527 | "1".to_string(), 528 | false 529 | )))) 530 | })], 531 | from: vec![TableWithJoins { 532 | relation: TableFactor::Table { 533 | name: ObjectName(vec![Ident { 534 | value: "test".to_string(), 535 | quote_style: None 536 | }]), 537 | alias: None, 538 | args: vec![], 539 | with_hints: vec![], 540 | instant: None, 541 | }, 542 | joins: vec![] 543 | }], 544 | lateral_views: vec![], 545 | selection: None, 546 | group_by: vec![], 547 | cluster_by: vec![], 548 | distribute_by: vec![], 549 | sort_by: vec![], 550 | having: None, 551 | })), 552 | order_by: vec![], 553 | limit: None, 554 | offset: None, 555 | fetch: None, 556 | format: None, 557 | }), 558 | query 559 | ); 560 | } 561 | _ => unreachable!(), 562 | } 563 | } 564 | 565 | fn mysql() -> TestedDialects { 566 | TestedDialects { 567 | dialects: vec![Box::new(MySqlDialect {})], 568 | } 569 | } 570 | 571 | fn mysql_and_generic() -> TestedDialects { 572 | TestedDialects { 573 | dialects: vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})], 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /src/ast/query.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | #[cfg(not(feature = "std"))] 14 | use alloc::{boxed::Box, vec::Vec}; 15 | 16 | #[cfg(feature = "serde")] 17 | use serde::{Deserialize, Serialize}; 18 | 19 | use crate::ast::*; 20 | use crate::tokenizer::QueryOffset; 21 | 22 | /// The most complete variant of a `SELECT` query expression, optionally 23 | /// including `WITH`, `UNION` / other set operations, and `ORDER BY`. 24 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 25 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 26 | pub struct Query { 27 | /// WITH (common table expressions, or CTEs) 28 | pub with: Option, 29 | /// SELECT or UNION / EXCEPT / INTERSECT 30 | pub body: SetExpr, 31 | /// ORDER BY 32 | pub order_by: Vec, 33 | /// `LIMIT { | ALL }` 34 | pub limit: Option, 35 | /// `OFFSET [ { ROW | ROWS } ]` 36 | pub offset: Option, 37 | /// `FETCH { FIRST | NEXT } [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES }` 38 | pub fetch: Option, 39 | 40 | /// `FORMAT ` 41 | pub format: Option, 42 | } 43 | 44 | impl fmt::Display for Query { 45 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 46 | if let Some(ref with) = self.with { 47 | write!(f, "{} ", with)?; 48 | } 49 | write!(f, "{}", self.body)?; 50 | if !self.order_by.is_empty() { 51 | write!(f, " ORDER BY {}", display_comma_separated(&self.order_by))?; 52 | } 53 | if let Some(ref limit) = self.limit { 54 | write!(f, " LIMIT {}", limit)?; 55 | } 56 | if let Some(ref offset) = self.offset { 57 | write!(f, " {}", offset)?; 58 | } 59 | if let Some(ref fetch) = self.fetch { 60 | write!(f, " {}", fetch)?; 61 | } 62 | 63 | if let Some(ref format) = self.format { 64 | write!(f, " FORMAT {}", format)?; 65 | } 66 | Ok(()) 67 | } 68 | } 69 | 70 | /// A node in a tree, representing a "query body" expression, roughly: 71 | /// `SELECT ... [ {UNION|EXCEPT|INTERSECT} SELECT ...]` 72 | #[allow(clippy::large_enum_variant)] 73 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 74 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 75 | pub enum SetExpr { 76 | /// Restricted SELECT .. FROM .. HAVING (no ORDER BY or set operations) 77 | Select(Box