Skip to content

Commit 3be87ab

Browse files
committed
Fix rowid injection for nested aggregates
1 parent d35e2d2 commit 3be87ab

4 files changed

Lines changed: 118 additions & 9 deletions

File tree

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ codegen-units = 1
4848
panic = "unwind"
4949
strip = true
5050

51+
# Optimized profile for slow #[ignore] harness tests (e.g. the IntelliSense
52+
# fixture sweep): release-level speed without fat LTO, so rebuilds stay fast.
53+
[profile.sweep]
54+
inherits = "release"
55+
lto = false
56+
codegen-units = 16
57+
strip = false
58+
incremental = true
59+
5160
[build-dependencies]
5261
cc = "1.2"
5362
syn = { version = "2", features = ["full"] }

src/db/query/executor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ impl QueryExecutor {
8686
pub(crate) fn is_retryable_rowid_injection_error(message: &str) -> bool {
8787
message.contains("ORA-01445")
8888
|| message.contains("ORA-01446")
89+
|| message.contains("ORA-00937")
90+
|| message.contains("ORA-00979")
8991
|| (message.contains("ORA-00904") && message.to_ascii_uppercase().contains("ROWID"))
9092
}
9193

src/db/query/query_tests.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2480,6 +2480,34 @@ fn test_maybe_inject_rowid_for_editing_skips_group_by() {
24802480
assert_eq!(rewritten, sql);
24812481
}
24822482

2483+
#[test]
2484+
fn test_maybe_inject_rowid_for_editing_skips_nested_aggregate_wrappers() {
2485+
for sql in [
2486+
r#"SELECT XMLSERIALIZE(
2487+
CONTENT XMLELEMENT("metrics",
2488+
XMLAGG(
2489+
XMLELEMENT("m", XMLFOREST(m.node_id AS "node"))
2490+
ORDER BY m.metric_id))
2491+
AS CLOB) AS xml_report
2492+
FROM sq_hard_metric m"#,
2493+
"SELECT ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sal), 2) FROM emp",
2494+
"SELECT XMLELEMENT(\"stats\", SYS_XMLAGG(XMLELEMENT(\"v\", sal))) FROM emp",
2495+
] {
2496+
let rewritten = QueryExecutor::maybe_inject_rowid_for_editing(sql);
2497+
assert_eq!(rewritten, sql);
2498+
}
2499+
}
2500+
2501+
#[test]
2502+
fn test_maybe_inject_rowid_for_editing_ignores_aggregate_in_scalar_subquery() {
2503+
let sql = "SELECT ENAME, COALESCE((SELECT AVG(SAL) FROM EMP), 0) AS AVG_SAL FROM EMP e";
2504+
let rewritten = QueryExecutor::maybe_inject_rowid_for_editing(sql);
2505+
assert_eq!(
2506+
rewritten,
2507+
"SELECT e.ROWID, ENAME, COALESCE((SELECT AVG(SAL) FROM EMP), 0) AS AVG_SAL FROM EMP e"
2508+
);
2509+
}
2510+
24832511
#[test]
24842512
fn test_maybe_inject_rowid_for_editing_skips_connect_by() {
24852513
let sql =
@@ -2781,6 +2809,16 @@ fn test_retryable_rowid_injection_error_detects_invalid_identifier_rowid() {
27812809
assert!(QueryExecutor::is_retryable_rowid_injection_error(message));
27822810
}
27832811

2812+
#[test]
2813+
fn test_retryable_rowid_injection_error_detects_injected_grouping_errors() {
2814+
for message in [
2815+
"ORA-00937: not a single-group group function",
2816+
"ORA-00979: not a GROUP BY expression",
2817+
] {
2818+
assert!(QueryExecutor::is_retryable_rowid_injection_error(message));
2819+
}
2820+
}
2821+
27842822
#[test]
27852823
fn test_retryable_rowid_injection_error_ignores_other_oracle_errors() {
27862824
let message = "ORA-00942: table or view does not exist";

src/db/query/script.rs

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6662,15 +6662,15 @@ impl QueryExecutor {
66626662
|| Self::has_top_level_identifier_keyword(sql, "MODEL")
66636663
|| !Self::is_single_table_from_clause(sql, from_idx)
66646664
|| Self::select_clause_has_distinct_or_unique(sql, select_idx, from_idx)
6665-
|| Self::select_clause_has_top_level_aggregate(sql, select_idx, from_idx)
6665+
|| Self::select_clause_has_aggregate_outside_subquery(sql, select_idx, from_idx)
66666666
|| Self::select_clause_has_top_level_analytic(sql, select_idx, from_idx)
66676667
{
66686668
return false;
66696669
}
66706670
true
66716671
}
66726672

6673-
fn select_clause_has_top_level_aggregate(
6673+
fn select_clause_has_aggregate_outside_subquery(
66746674
sql: &str,
66756675
select_idx: usize,
66766676
from_idx: usize,
@@ -6688,14 +6688,60 @@ impl QueryExecutor {
66886688
}
66896689

66906690
let select_list = &sql[select_body_start..from_idx];
6691-
let mut scanner = TopLevelScanner::new(select_list);
6692-
while let Some(token) = scanner.next() {
6693-
if let ScanToken::Word { text, .. } = token {
6694-
if Self::is_aggregate_function_name(text)
6695-
&& scanner.peek_next_non_ws_byte() == Some(b'(')
6696-
{
6697-
return true;
6691+
let protected_spans = crate::sql_parser_engine::lexical_spans(select_list, false);
6692+
let bytes = select_list.as_bytes();
6693+
let mut protected_idx = 0usize;
6694+
let mut pos = 0usize;
6695+
let mut paren_frames = Vec::<(bool, bool)>::new();
6696+
6697+
while pos < bytes.len() {
6698+
while protected_idx < protected_spans.len() && protected_spans[protected_idx].end <= pos
6699+
{
6700+
protected_idx = protected_idx.saturating_add(1);
6701+
}
6702+
if protected_spans
6703+
.get(protected_idx)
6704+
.is_some_and(|span| span.start <= pos && pos < span.end)
6705+
{
6706+
pos = protected_spans[protected_idx].end;
6707+
continue;
6708+
}
6709+
6710+
match bytes[pos] {
6711+
b'(' => {
6712+
paren_frames.push((false, false));
6713+
pos = pos.saturating_add(1);
6714+
}
6715+
b')' => {
6716+
paren_frames.pop();
6717+
pos = pos.saturating_add(1);
6718+
}
6719+
byte if sql_text::is_identifier_start_byte(byte) => {
6720+
let word_start = pos;
6721+
pos = pos.saturating_add(1);
6722+
while pos < bytes.len() && sql_text::is_identifier_byte(bytes[pos]) {
6723+
pos = pos.saturating_add(1);
6724+
}
6725+
let word = &select_list[word_start..pos];
6726+
if let Some((saw_first_word, is_subquery)) = paren_frames.last_mut() {
6727+
if !*saw_first_word {
6728+
*saw_first_word = true;
6729+
*is_subquery = word.eq_ignore_ascii_case("SELECT")
6730+
|| word.eq_ignore_ascii_case("WITH");
6731+
}
6732+
}
6733+
let next_byte = bytes[pos..]
6734+
.iter()
6735+
.copied()
6736+
.find(|byte| !byte.is_ascii_whitespace());
6737+
if !paren_frames.iter().any(|(_, is_subquery)| *is_subquery)
6738+
&& Self::is_aggregate_function_name(word)
6739+
&& next_byte == Some(b'(')
6740+
{
6741+
return true;
6742+
}
66986743
}
6744+
_ => pos = pos.saturating_add(1),
66996745
}
67006746
}
67016747
false
@@ -6709,16 +6755,30 @@ impl QueryExecutor {
67096755
| "AVG"
67106756
| "MIN"
67116757
| "MAX"
6758+
| "ANY_VALUE"
6759+
| "APPROX_COUNT_DISTINCT"
6760+
| "APPROX_MEDIAN"
6761+
| "APPROX_PERCENTILE"
6762+
| "COLLECT"
6763+
| "CUME_DIST"
6764+
| "DENSE_RANK"
67126765
| "LISTAGG"
67136766
| "JSON_ARRAYAGG"
67146767
| "JSON_OBJECTAGG"
6768+
| "PERCENT_RANK"
6769+
| "PERCENTILE_CONT"
6770+
| "PERCENTILE_DISC"
6771+
| "RANK"
6772+
| "STATS_MODE"
67156773
| "STDDEV"
67166774
| "STDDEV_POP"
67176775
| "STDDEV_SAMP"
6776+
| "SYS_XMLAGG"
67186777
| "VARIANCE"
67196778
| "VAR_POP"
67206779
| "VAR_SAMP"
67216780
| "MEDIAN"
6781+
| "XMLAGG"
67226782
| "CORR"
67236783
| "COVAR_POP"
67246784
| "COVAR_SAMP"

0 commit comments

Comments
 (0)