Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Python dict semantics over ordered pairs, and the topological analysis waves.

use std::collections::{BTreeSet, HashMap};
use std::sync::OnceLock;

use sha2::{Digest, Sha256};

Expand Down Expand Up @@ -80,7 +81,7 @@ pub(crate) struct ShapeTable {
/// Each name at its first insertion, Python's dict iteration order.
order: Vec<String>,
/// Digest chained over every insertion in order, so equal chains mean equal tables.
chain: [u8; 32],
chain: OnceLock<[u8; 32]>,
}

impl ShapeTable {
Expand All @@ -92,13 +93,12 @@ impl ShapeTable {
None => table.insert(name, shape.clone()),
}
}
table.chain = shapes_chain(&table.ordered());
table
}

/// The digest of every shape in order, which changes whenever the table does.
pub(crate) fn chain(&self) -> [u8; 32] {
self.chain
*self.chain.get_or_init(|| shapes_chain(&self.ordered()))
}

fn insert(&mut self, name: &str, shape: Pairs) {
Expand Down Expand Up @@ -126,7 +126,9 @@ impl ShapeTable {
/// Python's `setdefault`.
pub(crate) fn set_default(&mut self, name: &str, shape: Pairs) {
if !self.shapes.contains_key(name) {
self.chain = chained(&self.chain, name, &shape);
if let Some(chain) = self.chain.get() {
self.chain = OnceLock::from(chained(chain, name, &shape));
}
self.insert(name, shape);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,18 @@
//! Python's `re.search(r"\b(?:UNION|INTERSECT|EXCEPT)\b", sql, re.IGNORECASE) is not None`.

use sqlbuild_core::text::main::ignorecase_key::ignorecase_key;
use sqlbuild_core::text::main::is_word_char::is_word_char;

use crate::assembly::analysis_session::constants::SET_OPERATION_KEYWORDS;

/// Whether `sql` names a set-operation keyword as a whole word, in any case.
/// Whether a whole word of `sql` is a set-operation keyword; only ASCII letters fold to its letters.
pub fn names_set_operation(sql: &str) -> bool {
let characters: Vec<char> = sql.chars().collect();
for start in 0..characters.len() {
if start > 0 && is_word_char(characters[start - 1]) {
continue;
}
for keyword in SET_OPERATION_KEYWORDS {
let end = start + keyword.len();
if end > characters.len() {
continue;
}
let matches = characters[start..end]
.iter()
.zip(keyword.chars())
.all(|(character, letter)| ignorecase_key(*character) == letter);
let bounded = characters.get(end).is_none_or(|next| !is_word_char(*next));
if matches && bounded {
return true;
}
}
}
false
sql.split(|character: char| !is_word_char(character))
.any(is_set_operation_keyword)
}

/// Whether `word` is a set-operation keyword, ignoring ASCII case.
fn is_set_operation_keyword(word: &str) -> bool {
SET_OPERATION_KEYWORDS
.iter()
.any(|keyword| word.eq_ignore_ascii_case(keyword))
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ mod helpers;
mod mappings;
#[path = "tests/test_model_columns.rs"]
mod model_columns;
#[path = "tests/test_names_set_operation.rs"]
mod names_set_operation;
#[path = "tests/test_native_dialect.rs"]
mod native_dialect;
#[path = "tests/test_reference_facts.rs"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use sqlbuild_core::text::main::ignorecase_key::ignorecase_key;

use crate::assembly::analysis_session::main::names_set_operation::names_set_operation;
use crate::assembly::analysis_session::tests::test_types::{
KeywordFoldingTestCase, NamesSetOperationTestCase,
};

#[test]
fn given_sql_when_checking_for_set_operations_then_matches_whole_words_in_any_case() {
let test_cases = [
NamesSetOperationTestCase {
description: "a lowercase union is found",
sql: "select 1 union all select 2",
expected_set_operation: true,
},
NamesSetOperationTestCase {
description: "a mixed-case intersect is found",
sql: "SELECT a FROM t InTeRsEcT SELECT a FROM u",
expected_set_operation: true,
},
NamesSetOperationTestCase {
description: "a keyword at the end of the text is found",
sql: "select a from t\nexcept",
expected_set_operation: true,
},
NamesSetOperationTestCase {
description: "a keyword inside an identifier is ignored",
sql: "select reunion_id, union_count, except_flag from t",
expected_set_operation: false,
},
NamesSetOperationTestCase {
description: "a keyword followed by a non-ASCII letter is ignored",
sql: "select 1 as unioné",
expected_set_operation: false,
},
NamesSetOperationTestCase {
description: "a keyword after a non-ASCII letter is ignored",
sql: "select 1 as éunion",
expected_set_operation: false,
},
NamesSetOperationTestCase {
description: "a keyword after non-ASCII punctuation is found",
sql: "select 1 «union» select 2",
expected_set_operation: true,
},
NamesSetOperationTestCase {
description: "text without a keyword is not a set operation",
sql: "select ü, ß from t",
expected_set_operation: false,
},
];

for test_case in test_cases {
assert_eq!(
names_set_operation(test_case.sql),
test_case.expected_set_operation,
"{}",
test_case.description
);
}
}

#[test]
fn given_non_ascii_characters_when_folded_then_none_matches_a_keyword_letter() {
let test_cases = [
KeywordFoldingTestCase {
description: "no non-ASCII character folds to a letter of 'union'",
keyword: "union",
expected_non_ascii_folds: Vec::new(),
},
KeywordFoldingTestCase {
description: "no non-ASCII character folds to a letter of 'intersect'",
keyword: "intersect",
expected_non_ascii_folds: Vec::new(),
},
KeywordFoldingTestCase {
description: "no non-ASCII character folds to a letter of 'except'",
keyword: "except",
expected_non_ascii_folds: Vec::new(),
},
];

for test_case in test_cases {
let folds: Vec<char> = (0x80..=0x10FFFF_u32)
.filter_map(char::from_u32)
.filter(|character| test_case.keyword.contains(ignorecase_key(*character)))
.collect();

assert_eq!(
folds, test_case.expected_non_ascii_folds,
"{}",
test_case.description
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@ pub(crate) struct ReferenceFactsTestCase {
pub(crate) expected_required: &'static [&'static str],
}

pub(crate) struct NamesSetOperationTestCase {
pub(crate) description: &'static str,
pub(crate) sql: &'static str,
pub(crate) expected_set_operation: bool,
}

pub(crate) struct KeywordFoldingTestCase {
pub(crate) description: &'static str,
pub(crate) keyword: &'static str,
pub(crate) expected_non_ascii_folds: Vec<char>,
}

pub(crate) struct NativeDialectTestCase {
pub(crate) description: &'static str,
pub(crate) dialect: Option<&'static str>,
Expand Down
22 changes: 19 additions & 3 deletions crates/sqlbuild-analysis/src/compiled_project/_helpers/splice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,29 @@
use crate::compiled_project::models::{DeclarationSplice, SplicedSpan, SplicedSql};

pub(crate) fn splice_declarations(sql: &str, splices: &[DeclarationSplice]) -> SplicedSql {
if sql.is_ascii() {
return spliced(sql.len(), splices, |output, start, end| {
output.push_str(&sql[start..end]);
});
}
let characters: Vec<char> = sql.chars().collect();
let mut output: String = String::with_capacity(sql.len());
spliced(characters.len(), splices, |output, start, end| {
output.extend(&characters[start..end]);
})
}

/// The splice over text of `length` characters, `copy` appending each kept character range.
fn spliced(
length: usize,
splices: &[DeclarationSplice],
copy: impl Fn(&mut String, usize, usize),
) -> SplicedSql {
let mut output: String = String::with_capacity(length);
let mut spans: Vec<SplicedSpan> = Vec::with_capacity(splices.len());
let mut cursor: usize = 0;
let mut output_length: usize = 0;
for splice in splices {
output.extend(&characters[cursor..splice.start]);
copy(&mut output, cursor, splice.start);
output_length += splice.start - cursor;
let replacement_length: usize = splice.replacement.chars().count();
output.push_str(&splice.replacement);
Expand All @@ -22,7 +38,7 @@ pub(crate) fn splice_declarations(sql: &str, splices: &[DeclarationSplice]) -> S
output_length += replacement_length;
cursor = splice.end;
}
output.extend(&characters[cursor..]);
copy(&mut output, cursor, length);
SplicedSql { sql: output, spans }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

use std::collections::{BTreeSet, HashMap, HashSet};

use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use sqlbuild_attachments::cursor_intrinsics::main::replace_intrinsics::replace_intrinsics;
use sqlbuild_core::panics::main::catch_compiler_panic::catch_compiler_panic;

use crate::semantic_checks::_helpers::explanation::opt_outs::unneeded_opt_out;
use crate::semantic_checks::_helpers::resource_sql::help::{HelpContext, resource_sql_help};
Expand Down Expand Up @@ -204,16 +206,31 @@ pub(crate) fn resource_sql_findings(
request: &ResourceSqlRequest,
catalog: &ProjectCatalog,
) -> Result<Vec<ResourceSqlDiagnostic>, ResourceSqlFailure> {
let checked = resources(request);
let pool = || catalog.analysis_pool();
let checked = resources(request, &pool).map_err(ResourceSqlFailure::Internal)?;
if checked.is_empty() {
return Ok(Vec::new());
}
let dialect = request.dialect.as_deref();
let mut normalized: HashMap<&str, String> = HashMap::new();
for resource in &checked {
if !normalized.contains_key(resource.sql.as_str()) {
normalized.insert(&resource.sql, cleaned_sql(&resource.sql, dialect)?);
}
let mut seen: HashSet<&str> = HashSet::with_capacity(checked.len());
let unique: Vec<&str> = checked
.iter()
.map(|resource| resource.sql.as_str())
.filter(|sql| seen.insert(sql))
.collect();
let cleaned: Vec<Result<String, ResourceSqlFailure>> =
pool().map_err(ResourceSqlFailure::Internal)?.install(|| {
unique
.par_iter()
.map(|sql| {
catch_compiler_panic(|| Ok(cleaned_sql(sql, dialect)))
.unwrap_or_else(|panic| Err(ResourceSqlFailure::Internal(panic)))
})
.collect()
});
let mut normalized: HashMap<&str, String> = HashMap::with_capacity(unique.len());
for (sql, cleaned) in unique.into_iter().zip(cleaned) {
normalized.insert(sql, cleaned?);
}
let shapes: HashMap<&str, &[(String, String)]> = request
.shapes
Expand Down
Loading
Loading