Skip to content

Commit 72b7d32

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/upivot_expression_support' into upivot_expression_support
2 parents 81c7308 + 9411459 commit 72b7d32

30 files changed

Lines changed: 2203 additions & 537 deletions

.github/workflows/audit.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
name: Security audit
19+
20+
on:
21+
push:
22+
branches-ignore:
23+
- 'gh-readonly-queue/**'
24+
paths:
25+
- '**/Cargo.toml'
26+
- '**/Cargo.lock'
27+
pull_request:
28+
paths:
29+
- '**/Cargo.toml'
30+
- '**/Cargo.lock'
31+
merge_group:
32+
schedule:
33+
# Run every day so newly published advisories are caught even when the
34+
# dependency tree has not changed.
35+
- cron: '0 0 * * *'
36+
37+
permissions:
38+
contents: read
39+
40+
jobs:
41+
audit:
42+
runs-on: ubuntu-latest
43+
steps:
44+
- uses: actions/checkout@v4
45+
- name: Install cargo-audit
46+
run: cargo install cargo-audit --locked
47+
- name: Run cargo audit
48+
run: cargo audit --deny warnings

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ edition = "2021"
3636
name = "sqlparser"
3737
path = "src/lib.rs"
3838

39+
[lints.rust]
40+
unsafe_code = "forbid"
41+
3942
[features]
4043
default = ["std", "recursive-protection"]
4144
std = []

derive/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ edition = "2021"
3535
[lib]
3636
proc-macro = true
3737

38+
[lints.rust]
39+
unsafe_code = "forbid"
40+
3841
[dependencies]
3942
syn = { version = "2.0", default-features = false, features = ["full", "printing", "parsing", "derive", "proc-macro", "clone-impls"] }
4043
proc-macro2 = "1.0"

examples/cli.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ $ cargo run --example cli - [--dialectname]
7171
.expect("failed to read from stdin");
7272
String::from_utf8(buf).expect("stdin content wasn't valid utf8")
7373
} else {
74-
println!("Parsing from file '{}' using {:?}", &filename, dialect);
74+
println!("Parsing from file '{}' using {:?}", filename, dialect);
7575
fs::read_to_string(&filename)
76-
.unwrap_or_else(|_| panic!("Unable to read the file {}", &filename))
76+
.unwrap_or_else(|_| panic!("Unable to read the file {}", filename))
7777
};
7878
let without_bom = if contents.chars().next().unwrap() as u64 != 0xfeff {
7979
contents.as_str()

sqlparser_bench/benches/sqlparser_bench.rs

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// under the License.
1717

1818
use criterion::{criterion_group, criterion_main, Criterion};
19-
use sqlparser::dialect::GenericDialect;
19+
use sqlparser::dialect::{GenericDialect, PostgreSqlDialect, SQLiteDialect};
2020
use sqlparser::keywords::Keyword;
2121
use sqlparser::parser::Parser;
2222
use sqlparser::tokenizer::{Span, Word};
@@ -177,11 +177,136 @@ fn parse_compound_chain(c: &mut Criterion) {
177177
group.finish();
178178
}
179179

180+
/// Benchmark parsing pathological compound chains with a reserved keyword in
181+
/// field position, like `SELECT x.not-b.not-b...`. The `.not-b` shape used to
182+
/// cause 2^N work in `parse_compound_expr` because `parse_prefix` descended
183+
/// into `parse_not` -> `parse_subexpr`, re-walking the remaining chain at
184+
/// every segment.
185+
fn parse_compound_keyword_chain(c: &mut Criterion) {
186+
let mut group = c.benchmark_group("parse_compound_keyword_chain");
187+
let dialect = GenericDialect {};
188+
189+
for &n in &[5usize, 10, 15] {
190+
let body = std::iter::repeat_n(".not-b", n).collect::<String>();
191+
let sql = format!("SELECT x{body}");
192+
193+
group.bench_function(format!("chain_{n}"), |b| {
194+
b.iter(|| {
195+
let _ = Parser::parse_sql(&dialect, std::hint::black_box(&sql));
196+
});
197+
});
198+
}
199+
200+
group.finish();
201+
}
202+
203+
/// Benchmark parsing pathological `IF(<keyword-fn>(<keyword-fn>(...x` chains
204+
/// that previously caused 2^N work in `parse_prefix`. Each nested
205+
/// `current_time(` segment used to be explored twice at every level (once via
206+
/// the speculative reserved-word arm, once via the unreserved-word fallback),
207+
/// doubling work per level. Post-fix the cost is linear in chain length.
208+
fn parse_prefix_keyword_call_chain(c: &mut Criterion) {
209+
let mut group = c.benchmark_group("parse_prefix_keyword_call_chain");
210+
let dialect = PostgreSqlDialect {};
211+
212+
for &n in &[10usize, 20, 30] {
213+
let sql = String::from("if(") + &"current_time(".repeat(n) + "x";
214+
215+
group.bench_function(format!("chain_{n}"), |b| {
216+
b.iter(|| {
217+
let _ = Parser::parse_sql(&dialect, std::hint::black_box(&sql));
218+
});
219+
});
220+
}
221+
222+
group.finish();
223+
}
224+
225+
/// Benchmark parsing pathological `case-case-case-...c` chains that
226+
/// previously caused 2^N work in `parse_prefix`. Each `case` token used to
227+
/// trigger a speculative `parse_case_expr` that recursively descends the
228+
/// chain, but the unreserved-word fallback returns `Identifier(case)` so the
229+
/// overall `parse_prefix` succeeds and the failure cache never fires.
230+
/// Post-fix the per-arm cache short-circuits the speculative descent.
231+
fn parse_prefix_case_chain(c: &mut Criterion) {
232+
let mut group = c.benchmark_group("parse_prefix_case_chain");
233+
let dialect = SQLiteDialect {};
234+
235+
for &n in &[10usize, 20, 30] {
236+
let sql = "case\t-".repeat(n) + "c";
237+
238+
group.bench_function(format!("chain_{n}"), |b| {
239+
b.iter(|| {
240+
let _ = Parser::parse_sql(&dialect, std::hint::black_box(&sql));
241+
});
242+
});
243+
}
244+
245+
group.finish();
246+
}
247+
248+
/// Benchmark parsing pathological paren chains that previously caused 2^N
249+
/// work in `parse_table_factor`. The input `SELECT 1 FROM ((((...` rejects
250+
/// at EOF, which used to force exponential backtracking through the chain.
251+
fn parse_table_factor_paren_chain(c: &mut Criterion) {
252+
let mut group = c.benchmark_group("parse_table_factor_paren_chain");
253+
let dialect = GenericDialect {};
254+
255+
for &n in &[10usize, 20, 30] {
256+
let mut sql = String::from("SELECT 1 ");
257+
for _ in 0..5 {
258+
sql.push_str("FROM ");
259+
sql.push_str(&"(".repeat(n));
260+
sql.push(' ');
261+
}
262+
263+
group.bench_function(format!("chain_{n}"), |b| {
264+
b.iter(|| {
265+
let _ = Parser::new(&dialect)
266+
.with_recursion_limit(256)
267+
.try_with_sql(std::hint::black_box(&sql))
268+
.and_then(|mut p| p.parse_statements());
269+
});
270+
});
271+
}
272+
273+
group.finish();
274+
}
275+
276+
/// Benchmark parsing pathological `CAST(CASE (CAST(CASE (...` chains that
277+
/// previously caused 2^N work in `parse_function_args` on dialects with
278+
/// expression-named function arguments (the argument expression was parsed
279+
/// once to detect the named form, then re-parsed on the unnamed path).
280+
fn parse_function_arg_call_chain(c: &mut Criterion) {
281+
let mut group = c.benchmark_group("parse_function_arg_call_chain");
282+
let dialect = PostgreSqlDialect {};
283+
284+
for &n in &[10usize, 20, 30] {
285+
let sql = String::from("SELECT ") + &"CAST(CASE (".repeat(n) + &")".repeat(n);
286+
287+
group.bench_function(format!("chain_{n}"), |b| {
288+
b.iter(|| {
289+
let _ = Parser::new(&dialect)
290+
.with_recursion_limit(256)
291+
.try_with_sql(std::hint::black_box(&sql))
292+
.and_then(|mut p| p.parse_statements());
293+
});
294+
});
295+
}
296+
297+
group.finish();
298+
}
299+
180300
criterion_group!(
181301
benches,
182302
basic_queries,
183303
word_to_ident,
184304
parse_many_identifiers,
185-
parse_compound_chain
305+
parse_compound_chain,
306+
parse_compound_keyword_chain,
307+
parse_prefix_keyword_call_chain,
308+
parse_prefix_case_chain,
309+
parse_table_factor_paren_chain,
310+
parse_function_arg_call_chain
186311
);
187312
criterion_main!(benches);

0 commit comments

Comments
 (0)