From 13d8e2b78c5d31ada1a0eca96ce65c47561442a2 Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Thu, 22 Oct 2026 16:56:04 +0330 Subject: [PATCH 01/11] Flat utils folder. Write new core struct `Template` --- src/dialect/config.rs | 14 +- src/expr/enum_/func.rs | 4 +- src/expr/enum_/ident.rs | 8 +- src/internal/mod.rs | 2 + src/internal/{utils => }/submodule_ext.rs | 0 src/internal/template.rs | 397 ++++++++++++++++++++++ src/internal/{utils/mod.rs => utils.rs} | 6 +- src/internal/utils/strings.rs | 301 ---------------- src/lib.rs | 2 +- 9 files changed, 414 insertions(+), 320 deletions(-) rename src/internal/{utils => }/submodule_ext.rs (100%) create mode 100644 src/internal/template.rs rename src/internal/{utils/mod.rs => utils.rs} (97%) delete mode 100644 src/internal/utils/strings.rs diff --git a/src/dialect/config.rs b/src/dialect/config.rs index 664ea10..2bc7b3c 100644 --- a/src/dialect/config.rs +++ b/src/dialect/config.rs @@ -1,4 +1,4 @@ -use crate::internal::utils; +use crate::internal::template; use super::errors::FormatError; @@ -358,7 +358,7 @@ impl DialectConfig { #[inline] pub(super) fn escape_string(&self, writer: &mut W, string: &str) -> Result<(), FormatError> { let result = match self.string_escape { - StringEscape::Quote => utils::replace_to(string, '\'', "''", writer), + StringEscape::Quote => template::replace_to(string, '\'', "''", writer), StringEscape::BackslashHex => write_backslash_hex(writer, string), StringEscape::BackslashMysql => write_backslash_mysql(writer, string), StringEscape::BackslashUnicode => write_backslash_unicode(writer, string, false), @@ -381,24 +381,24 @@ impl DialectConfig { match self.identifier_escape { IdentifierEscape::Double => match self.identifier_format { IdentifierFormat::DoubleQuote => { - utils::replace_to(ident, '"', "\"\"", writer)?; + template::replace_to(ident, '"', "\"\"", writer)?; } IdentifierFormat::Backtick => { - utils::replace_to(ident, '`', "``", writer)?; + template::replace_to(ident, '`', "``", writer)?; } IdentifierFormat::SquareBracket => { - utils::replace_to(ident, ']', "]]", writer)?; + template::replace_to(ident, ']', "]]", writer)?; } }, IdentifierEscape::Backslash => match self.identifier_format { IdentifierFormat::DoubleQuote => { - utils::replace_to(ident, '"', "\\\"", writer)?; + template::replace_to(ident, '"', "\\\"", writer)?; } IdentifierFormat::Backtick => { write_identifier_backtick_escape(writer, ident)?; } IdentifierFormat::SquareBracket => { - utils::replace_to(ident, ']', "\\]", writer)?; + template::replace_to(ident, ']', "\\]", writer)?; } }, } diff --git a/src/expr/enum_/func.rs b/src/expr/enum_/func.rs index e26e086..4f8718f 100644 --- a/src/expr/enum_/func.rs +++ b/src/expr/enum_/func.rs @@ -98,8 +98,8 @@ impl Function { impl WriteSql for Function { fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> PrepareResult<()> { - // "name"([DISTINCT] args [IGNORE|RESPECT NULLS] [WHERE expr]) - // [FILTER ([WHERE] expr)] [IGNORE|RESPECT NULLS] + // "name"([DISTINCT] args [{IGNORE|RESPECT} NULLS] [WHERE expr]) + // [FILTER ([WHERE] expr)] [{IGNORE|RESPECT} NULLS] self.name.write_sql(dialect, out)?; let args_available = !self.args.is_empty(); diff --git a/src/expr/enum_/ident.rs b/src/expr/enum_/ident.rs index 70888d5..e486408 100644 --- a/src/expr/enum_/ident.rs +++ b/src/expr/enum_/ident.rs @@ -101,7 +101,7 @@ impl WriteSql for IdentPart { /// assert_eq!(ident.len(), 2); /// ``` #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Ident(smallvec::SmallVec<[IdentPart; 4]>); +pub struct Ident(smallvec::SmallVec<[IdentPart; 3]>); /// Why building an [`Ident`] failed. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -168,7 +168,7 @@ impl Ident { /// Consume and yield the parts. #[inline] - pub fn into_parts(self) -> smallvec::SmallVec<[IdentPart; 4]> { + pub fn into_parts(self) -> smallvec::SmallVec<[IdentPart; 3]> { self.0 } @@ -208,7 +208,7 @@ impl Ident { /// Unquoted charset is enforced by [`IdentPart::unquoted`], not here. #[cfg_attr(feature = "nightly", optimize(speed))] pub fn from_parts(iter: impl IntoIterator) -> Result { - let mut parts = smallvec::SmallVec::<[IdentPart; 4]>::new(); + let mut parts = smallvec::SmallVec::<[IdentPart; 3]>::new(); for part in iter { if let Some(IdentPart::Asterisk) = parts.last() { @@ -243,7 +243,7 @@ impl std::str::FromStr for Ident { return Err(ParseIdentError::Empty); } - let mut parts = smallvec::SmallVec::<[IdentPart; 4]>::new(); + let mut parts = smallvec::SmallVec::<[IdentPart; 3]>::new(); for chunk in s.splitn(Self::MAX_PARTS + 1, '.') { if chunk.is_empty() { diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 1eb9c2d..14d0c82 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -2,4 +2,6 @@ pub mod alias; pub mod modules; pub mod repr; pub mod sql; +pub mod submodule_ext; +pub mod template; pub mod utils; diff --git a/src/internal/utils/submodule_ext.rs b/src/internal/submodule_ext.rs similarity index 100% rename from src/internal/utils/submodule_ext.rs rename to src/internal/submodule_ext.rs diff --git a/src/internal/template.rs b/src/internal/template.rs new file mode 100644 index 0000000..b9561d4 --- /dev/null +++ b/src/internal/template.rs @@ -0,0 +1,397 @@ +//! Allocation-conscious string replacement that writes directly to a [`fmt::Write`]. + +// FIXME: Use `std::str::pattern::Pattern` when it's stable +/// A search needle that can be located inside a UTF-8 haystack. +pub trait ReplacePattern: Copy { + /// First match of this pattern in `haystack`. + /// + /// Returns `(byte_offset, match_len_in_bytes)`. + fn find(self, haystack: &str) -> Option<(usize, usize)>; + + /// If this pattern is exactly one ASCII byte, return that byte. + /// + /// Used to prove that match boundaries are UTF-8 char boundaries without + /// inspecting the haystack. + fn as_ascii_byte(self) -> Option { + None + } +} + +impl ReplacePattern for char { + #[inline] + fn find(self, haystack: &str) -> Option<(usize, usize)> { + haystack.find(self).map(|i| (i, self.len_utf8())) + } + + #[inline] + fn as_ascii_byte(self) -> Option { + self.is_ascii().then_some(self as u8) + } +} + +impl ReplacePattern for &str { + #[inline] + fn find(self, haystack: &str) -> Option<(usize, usize)> { + // An empty needle matches at every byte offset and would loop forever + // (or insert between every character). Treat it as "no match". + if self.is_empty() { + return None; + } + haystack.find(self).map(|i| (i, self.len())) + } + + #[inline] + fn as_ascii_byte(self) -> Option { + let bytes = self.as_bytes(); + match bytes { + [b] if b.is_ascii() => Some(*b), + _ => None, + } + } +} +/// Replace every occurrence of `from` with `to`, writing the result to `out`. +/// +/// Matches are non-overlapping and searched left to right, same as [`str::replace`]. +/// Empty `&str` needles are ignored (no infinite loop, input is copied as-is). +#[inline] +pub fn replace_to(haystack: &str, from: P, to: &str, out: &mut W) -> std::fmt::Result +where + P: ReplacePattern, + W: std::fmt::Write, +{ + // Single ASCII byte → single ASCII byte: still go through the generic loop. + // `str::find(char)` already uses memchr, which beats a hand-rolled byte scan + // when matches are sparse, and the generic loop is correct for every other case. + + let mut last_end = 0; + let mut search_at = 0; + + while let Some((rel, len)) = from.find(&haystack[search_at..]) { + let start = search_at + rel; + out.write_str(&haystack[last_end..start])?; + out.write_str(to)?; + last_end = start + len; + search_at = last_end; + + // Defensive: a zero-width match would otherwise spin. + if len == 0 { + break; + } + } + + out.write_str(&haystack[last_end..]) +} + +#[derive(Debug, Clone, Copy, derive_more::Display)] +pub enum TemplateParseError { + #[display("template is too long (maximum is 256 characters)")] + TemplateIsTooLong, + #[display("one of required keys not found: {_0}")] + RequiredKeyNotFound(&'static str), + #[display("placeholders are missing (at least {_0} placeholder is required, {_1} found)")] + MissingPlaceholder(u8, usize), +} +impl std::error::Error for TemplateParseError {} + +#[derive(Debug, Clone, Copy)] +struct TemplateMatches { + start: u16, + end: u16, + key: &'static str, +} + +/// A small, allocation-conscious template that expands `{key}` placeholders +/// by writing directly to a [`fmt::Write`]. +/// +/// The template string is stored as an [`ArcStr`]. Placeholder positions are +/// stored as plain numbers. Keys are `'static`. +#[derive(Debug, Clone)] +pub struct Template { + tmpl: arcstr::ArcStr, + matches: smallvec::SmallVec<[TemplateMatches; 1]>, +} + +impl Template { + /// Parses a template string, locating every `{key}` whose name appears in `keys`. + /// + /// # Arguments + /// + /// * `tmpl` - The template text. + /// * `keys` - Static list of `(name, required)`. + /// * `minimum` - Minimum number of placeholders that must be found. + /// + /// # Errors + /// + /// - Template longer than 256 bytes. + /// - Fewer than `minimum` placeholders found. + /// - A required key is missing. + #[cold] + pub fn parse(tmpl: &str, keys: &[(&'static str, bool)], minimum: u8) -> Result { + debug_assert!(keys.len() <= 16, "too many keys"); + + if tmpl.len() > 256 { + return Err(TemplateParseError::TemplateIsTooLong); + } + + let mut matches: smallvec::SmallVec<[TemplateMatches; 1]> = smallvec::SmallVec::new(); + let bytes = tmpl.as_bytes(); + let mut required_mask = 0usize; + let mut i = 0; + + while i < bytes.len() { + if bytes[i] == b'{' + && let Some(rel_end) = tmpl[i + 1..].find('}') + { + let key = &tmpl[i + 1..i + 1 + rel_end]; + + if let Some(idx) = keys.iter().position(|(k, _)| *k == key) { + let start = i; + let end = i + 1 + rel_end + 1; + + matches.push(TemplateMatches { + start: start as u16, + end: end as u16, + key: keys[idx].0, + }); + + if keys[idx].1 { + required_mask |= 1 << idx; + } + + i = end; + continue; + } + } + + i += 1; + } + + if matches.len() < minimum as usize { + return Err(TemplateParseError::MissingPlaceholder(minimum, matches.len())); + } + + for (idx, (key, required)) in keys.iter().enumerate() { + if *required && (required_mask & (1 << idx)) == 0 { + return Err(TemplateParseError::RequiredKeyNotFound(key)); + } + } + + Ok(Self { + tmpl: tmpl.into(), + matches, + }) + } + + /// Writes the expanded template to `out`. + /// + /// `fill` is called for each placeholder with its key name and the writer. + /// It is responsible for writing the replacement (or nothing). + #[inline] + #[cfg_attr(feature = "nightly", optimize(speed))] + pub fn write_to(&self, out: &mut W, mut fill: F) -> std::fmt::Result + where + W: std::fmt::Write, + F: FnMut(&'static str, &mut W) -> std::fmt::Result, + { + let s = self.tmpl.as_str(); + let mut last: u16 = 0; + + for m in self.matches.iter() { + if last < m.start { + out.write_str(&s[last as usize..m.start as usize])?; + } + + fill(m.key, out)?; + last = m.end; + } + + if (last as usize) < s.len() { + out.write_str(&s[last as usize..])?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt::Write; + + fn replace(haystack: &str, from: impl ReplacePattern, to: &str) -> String { + let mut out = String::with_capacity(haystack.len()); + replace_to(haystack, from, to, &mut out).unwrap(); + out + } + + #[test] + fn str_pattern() { + assert_eq!(replace("hello world", "o", "0"), "hell0 w0rld"); + } + + #[test] + fn char_pattern() { + assert_eq!(replace("banana", 'a', "AA"), "bAAnAAnAA"); + } + + #[test] + fn ascii_char_to_ascii_str() { + assert_eq!(replace("banana", 'a', "_"), "b_n_n_"); + } + + #[test] + fn no_match() { + assert_eq!(replace("hello", "z", "x"), "hello"); + } + + #[test] + fn unicode_pattern() { + assert_eq!(replace("café café", "é", "e"), "cafe cafe"); + } + + #[test] + fn multi_char_string_pattern() { + assert_eq!(replace("aaaa", "aa", "b"), "bb"); + } + + #[test] + fn overlapping_is_non_overlapping_left_to_right() { + assert_eq!(replace("aaa", "aa", "b"), "ba"); + } + + #[test] + fn empty_haystack() { + assert_eq!(replace("", "a", "b"), ""); + } + + #[test] + fn empty_needle_is_ignored() { + assert_eq!(replace("keep", "", "x"), "keep"); + } + + #[test] + fn delete_via_empty_replacement() { + assert_eq!(replace("a_b_c", "_", ""), "abc"); + } + + #[test] + fn replacement_longer_than_match() { + assert_eq!(replace("ab", "b", "BBB"), "aBBB"); + } + + #[test] + fn matches_std_replace_for_char() { + let samples = ["", "a", "aaa", "café", "hello\0world", "🇺🇸"]; + for s in samples { + assert_eq!(replace(s, 'a', "AA"), s.replace('a', "AA")); + assert_eq!(replace(s, "l", "_"), s.replace("l", "_")); + } + } + + #[test] + fn single_required_key() { + let t = Template::parse("{hex}::BLOB", &[("hex", true), ("raw", false)], 1).unwrap(); + + let mut out = String::new(); + t.write_to(&mut out, |key, w| { + if key == "hex" { + w.write_str("0xDEADBEEF") + } else { + Ok(()) + } + }) + .unwrap(); + + assert_eq!(out, "0xDEADBEEF::BLOB"); + } + + #[test] + fn multiple_keys() { + let t = Template::parse( + "regexp_matches({expr}, {pattern})", + &[("expr", true), ("pattern", true), ("flags", false)], + 2, + ) + .unwrap(); + + let mut out = String::new(); + t.write_to(&mut out, |key, w| match key { + "expr" => w.write_str("col"), + "pattern" => w.write_str("'^foo.*'"), + _ => Ok(()), + }) + .unwrap(); + + assert_eq!(out, "regexp_matches(col, '^foo.*')"); + } + + #[test] + fn optional_key_omitted() { + let t = Template::parse("func({a}, {b})", &[("a", true), ("b", false)], 1).unwrap(); + + let mut out = String::new(); + t.write_to(&mut out, |key, w| if key == "a" { w.write_str("1") } else { Ok(()) }) + .unwrap(); + + assert_eq!(out, "func(1, )"); + } + + #[test] + fn required_key_missing_at_parse_fails() { + let err = Template::parse("no placeholders", &[("missing", true)], 0); + assert!(matches!(err, Err(TemplateParseError::RequiredKeyNotFound("missing")))); + } + + #[test] + fn minimum_placeholders_not_met() { + let err = Template::parse("only {one}", &[("one", false)], 2); + assert!(matches!(err, Err(TemplateParseError::MissingPlaceholder(2, 1)))); + } + + #[test] + fn unknown_placeholders_are_left_literal() { + let t = Template::parse("hello {unknown} world", &[("known", false)], 0).unwrap(); + + let mut out = String::new(); + t.write_to(&mut out, |_, _| Ok(())).unwrap(); + + assert_eq!(out, "hello {unknown} world"); + } + + #[test] + fn repeated_keys() { + let t = Template::parse("{x} + {x}", &[("x", true)], 2).unwrap(); + + let mut out = String::new(); + t.write_to(&mut out, |_, w| w.write_str("1")).unwrap(); + + assert_eq!(out, "1 + 1"); + } + + #[test] + fn empty_template() { + let t = Template::parse("", &[], 0).unwrap(); + + let mut out = String::new(); + t.write_to(&mut out, |_, _| Ok(())).unwrap(); + + assert_eq!(out, ""); + } + + #[test] + fn no_placeholders() { + let t = Template::parse("plain text", &[("unused", false)], 0).unwrap(); + + let mut out = String::new(); + t.write_to(&mut out, |_, _| Ok(())).unwrap(); + + assert_eq!(out, "plain text"); + } + + #[test] + fn template_too_long() { + let long = "a".repeat(257); + let err = Template::parse(&long, &[], 0); + assert!(matches!(err, Err(TemplateParseError::TemplateIsTooLong))); + } +} diff --git a/src/internal/utils/mod.rs b/src/internal/utils.rs similarity index 97% rename from src/internal/utils/mod.rs rename to src/internal/utils.rs index 4043190..f26cdc5 100644 --- a/src/internal/utils/mod.rs +++ b/src/internal/utils.rs @@ -1,8 +1,4 @@ -mod strings; -mod submodule_ext; - -pub use strings::*; -pub use submodule_ext::*; +//! Utility functions /// Borrow the cached UTF-8 buffer of a Python `str`. /// diff --git a/src/internal/utils/strings.rs b/src/internal/utils/strings.rs deleted file mode 100644 index 515e32f..0000000 --- a/src/internal/utils/strings.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Allocation-conscious string replacement that writes directly to a [`fmt::Write`]. -// Assisted-by: Grok - -use std::fmt::Write; -use std::fmt::{self}; - -/// A search needle that can be located inside a UTF-8 haystack. -pub trait ReplacePattern: Copy { - /// First match of this pattern in `haystack`. - /// - /// Returns `(byte_offset, match_len_in_bytes)`. - fn find(self, haystack: &str) -> Option<(usize, usize)>; - - /// If this pattern is exactly one ASCII byte, return that byte. - /// - /// Used to prove that match boundaries are UTF-8 char boundaries without - /// inspecting the haystack. - fn as_ascii_byte(self) -> Option { - None - } -} - -impl ReplacePattern for char { - #[inline] - fn find(self, haystack: &str) -> Option<(usize, usize)> { - haystack.find(self).map(|i| (i, self.len_utf8())) - } - - #[inline] - fn as_ascii_byte(self) -> Option { - self.is_ascii().then_some(self as u8) - } -} - -impl ReplacePattern for &str { - #[inline] - fn find(self, haystack: &str) -> Option<(usize, usize)> { - // An empty needle matches at every byte offset and would loop forever - // (or insert between every character). Treat it as "no match". - if self.is_empty() { - return None; - } - haystack.find(self).map(|i| (i, self.len())) - } - - #[inline] - fn as_ascii_byte(self) -> Option { - let bytes = self.as_bytes(); - match bytes { - [b] if b.is_ascii() => Some(*b), - _ => None, - } - } -} - -/// Replace every occurrence of `from` with `to`, writing the result to `out`. -/// -/// Matches are non-overlapping and searched left to right, same as [`str::replace`]. -/// Empty `&str` needles are ignored (no infinite loop, input is copied as-is). -#[inline] -pub fn replace_to(haystack: &str, from: P, to: &str, out: &mut W) -> fmt::Result -where - P: ReplacePattern, - W: Write, -{ - // Single ASCII byte → single ASCII byte: still go through the generic loop. - // `str::find(char)` already uses memchr, which beats a hand-rolled byte scan - // when matches are sparse, and the generic loop is correct for every other case. - let _ = from.as_ascii_byte(); - - let mut last_end = 0; - let mut search_at = 0; - - while let Some((rel, len)) = from.find(&haystack[search_at..]) { - let start = search_at + rel; - out.write_str(&haystack[last_end..start])?; - out.write_str(to)?; - last_end = start + len; - search_at = last_end; - - // Defensive: a zero-width match would otherwise spin. - if len == 0 { - break; - } - } - - out.write_str(&haystack[last_end..]) -} - -/// Replace multiple single ASCII bytes in one pass over `haystack`. -/// -/// Equivalent to applying each pair as a simultaneous substitution against the -/// *original* input (not a chain of sequential [`str::replace`] calls). If the -/// same byte appears more than once, the last pair wins. -/// -/// # Constraints -/// - Each key must be an ASCII byte (`0..=127`). Non-ASCII keys are skipped so -/// that splits cannot land inside a multi-byte UTF-8 scalar. -/// - Replacement values may be any string, including empty (deletion). -/// -/// # Complexity -/// O(n + k) where n is the input length and k is the number of pairs. -pub fn replace_ascii_to(haystack: &str, replacements: &[(u8, &str)], out: &mut W) -> fmt::Result { - if replacements.is_empty() { - return out.write_str(haystack); - } - - // table[b] == 0 → no replacement - // table[b] == i + 1 → replacements[i] - // 512 bytes on the stack, no heap, last write wins. - let mut table = [0u16; 256]; - for (i, &(byte, _)) in replacements.iter().enumerate() { - if !byte.is_ascii() { - continue; - } - table[byte as usize] = i as u16 + 1; - } - - let bytes = haystack.as_bytes(); - let mut last_end = 0; - - for (i, &b) in bytes.iter().enumerate() { - let slot = table[b as usize]; - if slot == 0 { - continue; - } - - write_utf8_prefix(out, bytes, last_end, i)?; - out.write_str(replacements[slot as usize - 1].1)?; - last_end = i + 1; - } - - write_utf8_prefix(out, bytes, last_end, bytes.len()) -} - -/// Write `bytes[from..to]` as a string. -/// -/// # Safety invariant -/// `bytes` is the raw view of a valid `&str`, and `from..to` only starts/ends on -/// ASCII bytes or the slice ends — hence a char boundary. -#[inline] -fn write_utf8_prefix(out: &mut W, bytes: &[u8], from: usize, to: usize) -> fmt::Result { - if from == to { - return Ok(()); - } - // SAFETY: see invariant above. Callers only advance `from` past ASCII matches - // or to `bytes.len()`. - let chunk = unsafe { bytes.get_unchecked(from..to) }; - out.write_str(unsafe { std::str::from_utf8_unchecked(chunk) }) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn replace(haystack: &str, from: impl ReplacePattern, to: &str) -> String { - let mut out = String::with_capacity(haystack.len()); - replace_to(haystack, from, to, &mut out).unwrap(); - out - } - - fn replace_ascii(s: &str, replacements: &[(u8, &str)]) -> String { - let mut out = String::with_capacity(s.len()); - replace_ascii_to(s, replacements, &mut out).unwrap(); - out - } - - #[test] - fn str_pattern() { - assert_eq!(replace("hello world", "o", "0"), "hell0 w0rld"); - } - - #[test] - fn char_pattern() { - assert_eq!(replace("banana", 'a', "AA"), "bAAnAAnAA"); - } - - #[test] - fn ascii_char_to_ascii_str() { - assert_eq!(replace("banana", 'a', "_"), "b_n_n_"); - } - - #[test] - fn no_match() { - assert_eq!(replace("hello", "z", "x"), "hello"); - } - - #[test] - fn unicode_pattern() { - assert_eq!(replace("café café", "é", "e"), "cafe cafe"); - } - - #[test] - fn multi_char_string_pattern() { - assert_eq!(replace("aaaa", "aa", "b"), "bb"); - } - - #[test] - fn overlapping_is_non_overlapping_left_to_right() { - assert_eq!(replace("aaa", "aa", "b"), "ba"); - } - - #[test] - fn empty_haystack() { - assert_eq!(replace("", "a", "b"), ""); - } - - #[test] - fn empty_needle_is_ignored() { - assert_eq!(replace("keep", "", "x"), "keep"); - } - - #[test] - fn delete_via_empty_replacement() { - assert_eq!(replace("a_b_c", "_", ""), "abc"); - } - - #[test] - fn replacement_longer_than_match() { - assert_eq!(replace("ab", "b", "BBB"), "aBBB"); - } - - #[test] - fn quote_and_nul() { - let input = "O'Brien\0's \0dog's\0 name"; - let expected = input.replace('\'', "''").replace('\0', ""); - let got = replace_ascii(input, &[(b'\'', "''"), (b'\0', "")]); - assert_eq!(got, expected); - } - - #[test] - fn single_replacement_matches_naive() { - let input = "hello world"; - assert_eq!(replace_ascii(input, &[(b'o', "0")]), input.replace('o', "0")); - } - - #[test] - fn no_matches_ascii() { - assert_eq!(replace_ascii("hello world", &[(b'z', "Z"), (b'x', "X")]), "hello world"); - } - - #[test] - fn empty_input_ascii() { - assert_eq!(replace_ascii("", &[(b'a', "b")]), ""); - } - - #[test] - fn empty_replacements_list() { - assert_eq!(replace_ascii("unchanged string", &[]), "unchanged string"); - } - - #[test] - fn many_pairs_at_once() { - let input = "a-b_c.d,e;f:g"; - let expected = input - .replace('-', "_DASH_") - .replace('.', "_DOT_") - .replace(',', "_COMMA_") - .replace(';', "_SEMI_") - .replace(':', "_COLON_"); - - let got = replace_ascii( - input, - &[ - (b'-', "_DASH_"), - (b'.', "_DOT_"), - (b',', "_COMMA_"), - (b';', "_SEMI_"), - (b':', "_COLON_"), - ], - ); - assert_eq!(got, expected); - } - - #[test] - fn unicode_content_preserved() { - let input = "café\0naïve'test"; - let expected = input.replace('\'', "''").replace('\0', ""); - assert_eq!(replace_ascii(input, &[(b'\'', "''"), (b'\0', "")]), expected); - } - - #[test] - fn last_duplicate_wins() { - assert_eq!(replace_ascii("abc", &[(b'a', "X"), (b'a', "Y")]), "Ybc"); - } - - #[test] - fn non_ascii_keys_are_skipped() { - // 0xC3 is a UTF-8 lead byte in "é". Treating it as a split point would be UB. - assert_eq!(replace_ascii("café", &[(0xC3, "X")]), "café"); - } - - #[test] - fn matches_std_replace_for_char() { - let samples = ["", "a", "aaa", "café", "hello\0world", "🇺🇸"]; - for s in samples { - assert_eq!(replace(s, 'a', "AA"), s.replace('a', "AA")); - assert_eq!(replace(s, "l", "_"), s.replace("l", "_")); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index d9d852d..4ca20f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,7 @@ mod _core { #[pymodule_init] #[cold] fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { - use crate::internal::utils::PyModuleSubmoduleExt; + use crate::internal::submodule_ext::PyModuleSubmoduleExt; use pyo3::types::PyModuleMethods; crate::typeref::initialize_typeref(m.py()); From cabff4abe6b5aa802da2ff3345e19496021d7216 Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Thu, 22 Oct 2026 17:28:02 +0330 Subject: [PATCH 02/11] Update `Template` tests --- src/internal/template.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/internal/template.rs b/src/internal/template.rs index b9561d4..bfc67dd 100644 --- a/src/internal/template.rs +++ b/src/internal/template.rs @@ -93,7 +93,7 @@ pub enum TemplateParseError { } impl std::error::Error for TemplateParseError {} -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct TemplateMatches { start: u16, end: u16, @@ -102,10 +102,7 @@ struct TemplateMatches { /// A small, allocation-conscious template that expands `{key}` placeholders /// by writing directly to a [`fmt::Write`]. -/// -/// The template string is stored as an [`ArcStr`]. Placeholder positions are -/// stored as plain numbers. Keys are `'static`. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Template { tmpl: arcstr::ArcStr, matches: smallvec::SmallVec<[TemplateMatches; 1]>, @@ -114,12 +111,6 @@ pub struct Template { impl Template { /// Parses a template string, locating every `{key}` whose name appears in `keys`. /// - /// # Arguments - /// - /// * `tmpl` - The template text. - /// * `keys` - Static list of `(name, required)`. - /// * `minimum` - Minimum number of placeholders that must be found. - /// /// # Errors /// /// - Template longer than 256 bytes. @@ -133,7 +124,7 @@ impl Template { return Err(TemplateParseError::TemplateIsTooLong); } - let mut matches: smallvec::SmallVec<[TemplateMatches; 1]> = smallvec::SmallVec::new(); + let mut matches: smallvec::SmallVec<[TemplateMatches; 1]> = smallvec::SmallVec::with_capacity(minimum as usize); let bytes = tmpl.as_bytes(); let mut required_mask = 0usize; let mut i = 0; @@ -378,6 +369,16 @@ mod tests { assert_eq!(out, ""); } + #[test] + fn empty_key() { + let t = Template::parse(":param_{}", &[("", true)], 1).unwrap(); + + let mut out = String::new(); + t.write_to(&mut out, |_, w| w.write_char('1')).unwrap(); + + assert_eq!(out, ":param_1"); + } + #[test] fn no_placeholders() { let t = Template::parse("plain text", &[("unused", false)], 0).unwrap(); From e880fcf1bc1efed984c3f3530d1496e60c372f91 Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Fri, 23 Oct 2026 18:44:17 +0330 Subject: [PATCH 03/11] Commit to keep changes --- dialect/base.rs | 198 +++++++ {src/dialect => dialect}/builtin.rs | 0 {src/dialect => dialect}/config.rs | 10 +- dialect/errors.rs | 31 + dialect/mod.rs | 86 +++ {src/dialect => dialect}/tests.rs | 0 rapidquery-macros/define_options.rs | 271 ++++++--- rapidquery-macros/lib.rs | 89 ++- rapidquery-macros/pydialect_constructor.rs | 238 -------- rapidquery-macros/pyo3_constructor.rs | 623 +++++++++++++++++++++ rapidquery/dialect.py | 570 +++++++++---------- rapidquery/dialect.pyi | 370 +----------- src/ddl/schema.rs | 76 +-- src/dialect/base.rs | 579 ++++++++++++++----- src/dialect/escape.rs | 283 ++++++++++ src/dialect/mod.rs | 79 +-- src/expr/enum_/func.rs | 20 +- src/expr/enum_/ident.rs | 2 +- src/expr/enum_/like.rs | 75 +-- src/expr/enum_/nodes.rs | 175 +++--- src/internal/repr.rs | 2 +- src/internal/sql.rs | 72 +-- src/internal/template.rs | 137 +++-- src/serializers/functions.rs | 260 ++++----- src/serializers/inner.rs | 22 +- 25 files changed, 2652 insertions(+), 1616 deletions(-) create mode 100644 dialect/base.rs rename {src/dialect => dialect}/builtin.rs (100%) rename {src/dialect => dialect}/config.rs (98%) create mode 100644 dialect/errors.rs create mode 100644 dialect/mod.rs rename {src/dialect => dialect}/tests.rs (100%) delete mode 100644 rapidquery-macros/pydialect_constructor.rs create mode 100644 rapidquery-macros/pyo3_constructor.rs create mode 100644 src/dialect/escape.rs diff --git a/dialect/base.rs b/dialect/base.rs new file mode 100644 index 0000000..55e509f --- /dev/null +++ b/dialect/base.rs @@ -0,0 +1,198 @@ +use crate::internal::utils; + +use super::config::*; + +/// SQL dialect: quoting, literals, and which syntax is legal. +/// +/// Use a builtin such as `PostgreSql`, or build one from `OPT_*` flags. +#[rapidquery_macros::pyclass("rapidquery.dialect.Dialect", eq, hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct PyDialect(pub DialectConfig); + +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyDialect { + type Error = pyo3::PyErr; + + fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { + unsafe { + // SAFETY: `obj` is a borrowed `&Bound`, so `as_ptr()` is a + // live `PyObject*`. + if crate::typeref::check_exact(obj.as_ptr(), crate::typeref::DIALECT_TYPE) { + let cast = obj.cast_unchecked::(); + return Ok(Self(cast.get().0)); + } + + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected Dialect, got {}", + utils::get_type_name(obj.py(), obj.as_ptr()) + ) + } + } +} + +// FIXME: Set dialect default values as disabled/unsupported when possible, +// otherwise set a traditional one. So developer can predict the values +// without needing to specify all arguments. +rapidquery_macros::pydialect_constructor!( + placeholder_character: char = '?', + + #[enum_(Simple, Numbered)] + placeholder_syntax: PlaceholderSyntax = PlaceholderSyntax::default(), + + #[bit(SCALAR, ANY)] + like_escape: LikeEscape = LikeEscape::SCALAR.bits(), + + #[enum_(OrExpansion, ArrayConstructor, PatternList)] + like_any: LikeAny = LikeAny::default(), + + #[enum_(Standard, UseLikeLower, UseLike)] + ilike_expression: ILikeExpression = ILikeExpression::default(), + + #[enum_(Unsupported, Standard, Tilde, RegexpLike, RegexpContains, RegexpMatches)] + rlike_expression: RLikeExpression = RLikeExpression::default(), + + supports_similar_to: bool = false, + + #[enum_(Unsupported, InsideParens, Postfix)] + null_treatment: NullTreatment = NullTreatment::default(), + + #[enum_(Unsupported, Filter, FilterWhere, InsideWhere)] + function_filter: FunctionFilter = FunctionFilter::default(), + + #[bit(AND, OR, NOT, SHL, SHR)] + bitwise_operations: BitwiseOperations = BitwiseOperations::AND + .union(BitwiseOperations::OR) + .union(BitwiseOperations::NOT) + .union(BitwiseOperations::SHL) + .union(BitwiseOperations::SHR) + .bits(), + + #[enum_(Infix, BitFunction, ShiftFunction)] + bitwise_call_syntax: BitwiseCallSyntax = BitwiseCallSyntax::default(), + + #[enum_(Unsupported, Caret, Hash, Function, BitFunction)] + bitwise_xor_spelling: BitwiseXorSpelling = BitwiseXorSpelling::default(), + + #[enum_(DoubleQuote, Backtick, SquareBracket)] + identifier_format: IdentifierFormat = IdentifierFormat::default(), + + #[enum_(Double, Backslash)] + identifier_escape: IdentifierEscape = IdentifierEscape::default(), + + #[enum_(SingleQuote, PrefixBigE, PrefixBigN)] + string_format: StringFormat = StringFormat::default(), + + #[enum_(Quote, BackslashHex, BackslashMysql, BackslashUnicode, BackslashHexZero, BackslashUnicodeQuote)] + string_escape: StringEscape = StringEscape::default(), + + #[enum_(Pipes, Plus, ConcatFunction)] + string_concat: StringConcat = StringConcat::default(), + + #[enum_(Unsupported, TryCast, TryCastWord, SafeCast)] + cast_try_cast: TryCast = TryCast::default(), + + cast_double_colon: bool = false, + + #[enum_(Unsupported, Row, Struct, Parentheses)] + tuple_syntax: TupleSyntax = TupleSyntax::default(), + + #[bit(EMPTY, ONE)] + tuple_arity: TupleArity = TupleArity::EMPTY.union(TupleArity::ONE).bits(), + + #[enum_(Quoted, Keyword)] + date_format: DateFormat = DateFormat::default(), + + #[enum_(Quoted, Keyword)] + time_format: TimeFormat = TimeFormat::default(), + + #[enum_(Quoted, Timestamptz, WithTimezone)] + timestamp_format: TimestampFormat = TimestampFormat::default(), + + #[enum_(Keyword, Integer)] + bool_format: BoolFormat = BoolFormat::default(), + + #[enum_(Unsupported, PrefixBigB, PrefixSmallB, PostfixBigB)] + bit_format: BitFormat = BitFormat::default(), + + #[enum_(PrefixBigX, PrefixZeroX, SlashXBytea, PrefixBigBSlashX, HexToRaw, Unhex, ToVarbyte, PostfixXbv, ToBinary, SlashXBlob)] + bytes_format: BytesFormat = BytesFormat::default(), + + #[enum_(Unsupported, Quoted, Cast)] + inet_format: InetFormat = InetFormat::default(), + + #[enum_(Unsupported, Quoted, Cast)] + macaddr_format: MacAddrFormat = MacAddrFormat::default(), + + #[enum_(Quoted, UuidKeyword, CastUuid)] + uuid_format: UuidFormat = UuidFormat::default(), + + #[enum_(Quoted, JsonKeyword, ParseJson)] + json_format: JsonFormat = JsonFormat::default(), + + #[enum_(Unsupported, Constructor, List, Function)] + array_format: ArrayFormat = ArrayFormat::default(), + + #[enum_(Percent, Function, Infix)] + modulo_spelling: ModuloSpelling = ModuloSpelling::default(), + + #[enum_(Unsupported, Standard, NullSafeEqual)] + is_distinct_from_spelling: IsDistinctFromSpelling = IsDistinctFromSpelling::default(), + + #[enum_(SqlFrom, Function, SpacesOnly)] + trim_spelling: TrimSpelling = TrimSpelling::default(), + + #[enum_(Unsupported, InfixIdent, FunctionString)] + collate_spelling: CollateSpelling = CollateSpelling::default(), + + #[bit(UNSUPPORTED, IF_NOT_EXISTS, EMBEDDED_DDL, IS_DATABASE, DROP_CASCADE, DROP_RESTRICT)] + schema_features: SchemaFeatures = SchemaFeatures::empty().bits(), + + #[enum_(Unsupported, Optional)] + schema_authorization_style: SchemaAuthorizationStyle = SchemaAuthorizationStyle::default(), + + #[enum_(Unsupported, LocationKeyword, ManagedLocationKeyword, ExternalVolume)] + schema_location_style: SchemaLocationStyle = SchemaLocationStyle::default(), +); + +#[pyo3::pymethods] +impl PyDialect { + /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). + fn write_boolean(&self, value: bool) -> pyo3::PyResult { + let mut buf = String::new(); + self.0.write_boolean(&mut buf, value)?; + Ok(buf) + } + + /// Escape `ident` and wrap it in this dialect’s identifier delimiters. + fn write_identifier(&self, value: &str) -> pyo3::PyResult { + let mut buf = String::new(); + let _ = self.0.write_identifier(&mut buf, value); + Ok(buf) + } + + /// Escape `value` and wrap it as a complete string literal. + fn write_string_literal(&self, value: &str) -> pyo3::PyResult { + let mut buf = String::new(); + self.0.write_string_literal(&mut buf, value)?; + Ok(buf) + } + + /// Write a bit-string ('0'/'1' characters) as a native literal. + fn write_bits(&self, value: &str) -> pyo3::PyResult { + if value.chars().any(|c| c != '0' && c != '1') { + return rapidquery_macros::pyerr!(Err(PyValueError), "invalid input: {}", value); + } + + let mut buf = String::new(); + self.0.write_bits(&mut buf, value)?; + Ok(buf) + } + + /// Write bytes as a native blob/bytea/varbyte literal. + fn write_bytes(&self, value: &[u8]) -> pyo3::PyResult { + let mut buf = String::new(); + self.0.write_bytes(&mut buf, value)?; + Ok(buf) + } +} diff --git a/src/dialect/builtin.rs b/dialect/builtin.rs similarity index 100% rename from src/dialect/builtin.rs rename to dialect/builtin.rs diff --git a/src/dialect/config.rs b/dialect/config.rs similarity index 98% rename from src/dialect/config.rs rename to dialect/config.rs index 2bc7b3c..ca80cdc 100644 --- a/src/dialect/config.rs +++ b/dialect/config.rs @@ -343,12 +343,12 @@ pub struct DialectConfig { impl DialectConfig { /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). #[inline] - pub fn write_boolean(&self, writer: &mut W, value: bool) -> Result<(), FormatError> { + pub fn write_boolean(&self, writer: &mut String, value: bool) { match (self.bool_format, value) { - (BoolFormat::Integer, false) => writer.write_str("0")?, - (BoolFormat::Integer, true) => writer.write_str("1")?, - (BoolFormat::Keyword, false) => writer.write_str("FALSE")?, - (BoolFormat::Keyword, true) => writer.write_str("TRUE")?, + (BoolFormat::Integer, false) => writer.push_("0")?, + (BoolFormat::Integer, true) => writer.push_("1")?, + (BoolFormat::Keyword, false) => writer.push_("FALSE")?, + (BoolFormat::Keyword, true) => writer.push_("TRUE")?, } Ok(()) diff --git a/dialect/errors.rs b/dialect/errors.rs new file mode 100644 index 0000000..aab5729 --- /dev/null +++ b/dialect/errors.rs @@ -0,0 +1,31 @@ +use std::fmt; + +/// Formatting / serializing errors +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatError { + /// Equals to [`fmt::Write`] + Write, + /// Means a `NUL` character has found in string during escaping/serializing a string value. + NulInString, + /// Means something is not supported by a dialect + Unsupported(&'static str), +} + +impl From for FormatError { + #[inline] + fn from(_: fmt::Error) -> Self { + Self::Write + } +} + +impl fmt::Display for FormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Write => f.write_str("failed to write SQL to memory"), + Self::NulInString => f.write_str("NUL is not allowed in a string literal"), + Self::Unsupported(name) => write!(f, "this dialect does not support {name}"), + } + } +} + +impl std::error::Error for FormatError {} diff --git a/dialect/mod.rs b/dialect/mod.rs new file mode 100644 index 0000000..acb6a81 --- /dev/null +++ b/dialect/mod.rs @@ -0,0 +1,86 @@ +pub mod config; +pub mod errors; + +mod base; +mod builtin; + +#[cfg(test)] +mod tests; + +pub use base::PyDialect; +pub use builtin::BIGQUERY_DIALECT; +pub use builtin::DATABRICKS_DIALECT; +pub use builtin::DUCKDB_DIALECT; +pub use builtin::HIVE_DIALECT; +pub use builtin::MSSQL_DIALECT; +pub use builtin::MYSQL_DIALECT; +pub use builtin::ORACLE_DIALECT; +pub use builtin::POSTGRESQL_DIALECT; +pub use builtin::REDSHIFT_DIALECT; +pub use builtin::SNOWFLAKE_DIALECT; +pub use builtin::SPARK_DIALECT; +pub use builtin::SQLITE_DIALECT; +pub use builtin::TERADATA_DIALECT; + +#[pyo3::pymodule(name = "dialect", gil_used = false)] +pub mod dialect_mod { + #[pymodule_export] + use super::PyDialect; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const BigQuery: super::PyDialect = super::BIGQUERY_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Databricks: super::PyDialect = super::DATABRICKS_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const DuckDb: super::PyDialect = super::DUCKDB_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Hive: super::PyDialect = super::HIVE_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const MsSql: super::PyDialect = super::MSSQL_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const MySql: super::PyDialect = super::MYSQL_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Oracle: super::PyDialect = super::ORACLE_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const PostgreSql: super::PyDialect = super::POSTGRESQL_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Redshift: super::PyDialect = super::REDSHIFT_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Snowflake: super::PyDialect = super::SNOWFLAKE_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Spark: super::PyDialect = super::SPARK_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Sqlite: super::PyDialect = super::SQLITE_DIALECT; + + #[pymodule_export] + #[allow(non_upper_case_globals)] + const Teradata: super::PyDialect = super::TERADATA_DIALECT; + + #[pymodule_init] + fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { + super::base::export_options(m) + } +} diff --git a/src/dialect/tests.rs b/dialect/tests.rs similarity index 100% rename from src/dialect/tests.rs rename to dialect/tests.rs diff --git a/rapidquery-macros/define_options.rs b/rapidquery-macros/define_options.rs index 14f6b39..4791bb5 100644 --- a/rapidquery-macros/define_options.rs +++ b/rapidquery-macros/define_options.rs @@ -1,3 +1,6 @@ +//! Assisted-by: Claude + +use proc_macro2::Literal; use proc_macro2::TokenStream; use quote::quote; use syn::Attribute; @@ -8,17 +11,52 @@ use syn::Ident; use syn::Token; use syn::Type; use syn::Visibility; +use syn::ext::IdentExt; use syn::parse::Parse; use syn::parse::ParseStream; use syn::punctuated::Punctuated; -use syn::token::Comma; -struct EnumVariant { +/// Name of the helper attribute placed on fields. +const HELPER_ATTR: &str = "define"; + +/// Bitflags types are `u8`, so at most this many flags fit. +const MAX_FLAGS: usize = u8::BITS as usize; + +// ============================================================================ +// AST +// ============================================================================ + +struct Variant { attrs: Vec, ident: Ident, } -impl Parse for EnumVariant { +struct Flag { + attrs: Vec, + ident: Ident, + /// Explicit value; `None` means "next bit by position". + value: Option, +} + +/// The parsed body of `#[define(..)]`. +enum Body { + Enum(Vec), + Bits(Vec), +} + +/// A type that must be emitted next to the struct. +struct GeneratedType { + name: Ident, + vis: Visibility, + docs: Vec, + body: Body, +} + +// ============================================================================ +// Parsing +// ============================================================================ + +impl Parse for Variant { fn parse(input: ParseStream) -> syn::Result { Ok(Self { attrs: input.call(Attribute::parse_outer)?, @@ -27,44 +65,73 @@ impl Parse for EnumVariant { } } -struct Bitflag { - attrs: Vec, - ident: Ident, - value: Expr, -} - -impl Parse for Bitflag { +impl Parse for Flag { fn parse(input: ParseStream) -> syn::Result { let attrs = input.call(Attribute::parse_outer)?; let ident: Ident = input.parse()?; - input.parse::()?; - let value: Expr = input.parse()?; + let value = if input.peek(Token![=]) { + input.parse::()?; + Some(input.parse()?) + } else { + None + }; Ok(Self { attrs, ident, value }) } } -struct GeneratedType { - name: Ident, - vis: Visibility, - docs: Vec, - kind: GeneratedKind, +/// Parses `{ item, item, .. }` with at least one item. +fn parse_braced(input: ParseStream, what: &str) -> syn::Result> { + let content; + let brace = syn::braced!(content in input); + let items: Vec = Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); + + if items.is_empty() { + return Err(syn::Error::new( + brace.span.join(), + format!("expected at least one {what}"), + )); + } + Ok(items) } -enum GeneratedKind { - Enum(Punctuated), - Bitflags(Punctuated), +impl Parse for Body { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(Token![enum]) { + input.parse::()?; + return Ok(Self::Enum(parse_braced(input, "variant")?)); + } + + let word = Ident::parse_any(input)?; + if word != "bits" { + return Err(syn::Error::new_spanned(word, "expected `enum { .. }` or `bits { .. }`")); + } + + let flags: Vec = parse_braced(input, "flag")?; + if flags.len() > MAX_FLAGS { + return Err(syn::Error::new_spanned( + &flags[MAX_FLAGS].ident, + format!("a `u8` bitflags type holds at most {MAX_FLAGS} flags"), + )); + } + Ok(Self::Bits(flags)) + } } +// ============================================================================ +// Code generation +// ============================================================================ + impl GeneratedType { - fn into_tokens(self) -> proc_macro2::TokenStream { - let Self { name, vis, docs, kind } = self; - match kind { - GeneratedKind::Enum(variants) => { - let variants = variants.into_iter().map(|v| { - let attrs = v.attrs; - let ident = v.ident; - quote!( #(#attrs)* #ident ) - }); + fn into_tokens(self) -> TokenStream { + let Self { name, vis, docs, body } = self; + + match body { + Body::Enum(variants) => { + let variants = variants + .into_iter() + .map(|Variant { attrs, ident }| quote!(#(#attrs)* #ident)); quote! { #(#docs)* #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] @@ -73,15 +140,21 @@ impl GeneratedType { } } } - GeneratedKind::Bitflags(flags) => { - let flags = flags.into_iter().map(|f| { - let attrs = f.attrs; - let ident = f.ident; - let value = f.value; - quote!( #(#attrs)* const #ident = #value; ) + + Body::Bits(flags) => { + let flags = flags.into_iter().enumerate().map(|(index, flag)| { + let Flag { attrs, ident, value } = flag; + let value = value.map_or_else( + || { + let bit = Literal::usize_unsuffixed(index); + quote!(1 << #bit) + }, + |expr| quote!(#expr), + ); + quote!(#(#attrs)* const #ident = #value;) }); quote! { - bitflags::bitflags! { + ::bitflags::bitflags! { #(#docs)* #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #vis struct #name: u8 { @@ -94,75 +167,93 @@ impl GeneratedType { } } -fn extract_generated_type(field: &mut syn::Field) -> syn::Result> { - let docs: Vec = field - .attrs - .iter() - .filter(|a| a.path().is_ident("doc")) - .cloned() - .collect(); - - let ty_name = match &field.ty { - Type::Path(p) => p.path.get_ident().cloned(), - _ => None, - }; +/// Returns `T` for `Option`, otherwise the type itself. +fn unwrap_option(ty: &Type) -> &Type { + if let Type::Path(path) = ty + && let Some(segment) = path.path.segments.last() + && segment.ident == "Option" + && let syn::PathArguments::AngleBracketed(args) = &segment.arguments + && args.args.len() == 1 + && let syn::GenericArgument::Type(inner) = &args.args[0] + { + return inner; + } + ty +} - let Some(name) = ty_name else { - // Still strip any helper attributes so the compiler doesn’t complain - field - .attrs - .retain(|a| !a.path().is_ident("enum_") && !a.path().is_ident("bit")); +/// Reads and strips the `#[define(..)]` attribute of `field`. +/// +/// Returns the type to generate, or `None` if the field has no such attribute. +/// The attribute is always removed so the compiler never sees it. +fn take_generated_type(field: &mut syn::Field) -> syn::Result> { + let mut bodies = Vec::new(); + for attr in &field.attrs { + if attr.path().is_ident(HELPER_ATTR) { + bodies.push((attr.clone(), attr.parse_args::()?)); + } + } + field.attrs.retain(|attr| !attr.path().is_ident(HELPER_ATTR)); + let Some((attr, body)) = bodies.pop() else { return Ok(None); }; + if !bodies.is_empty() { + return Err(syn::Error::new_spanned( + attr, + format!("a field can have at most one `#[{HELPER_ATTR}(..)]`"), + )); + } - let mut generated = None; - - for attr in &field.attrs { - if attr.path().is_ident("enum_") { - let variants = attr.parse_args_with(Punctuated::::parse_terminated)?; - generated = Some(GeneratedType { - name: name.clone(), - vis: field.vis.clone(), - docs: docs.clone(), - kind: GeneratedKind::Enum(variants), - }); - break; - } - - if attr.path().is_ident("bit") { - let flags = attr.parse_args_with(Punctuated::::parse_terminated)?; - generated = Some(GeneratedType { - name, - vis: field.vis.clone(), - docs, - kind: GeneratedKind::Bitflags(flags), - }); - break; - } + let name = match unwrap_option(&field.ty) { + Type::Path(path) => path.path.get_ident().cloned(), + _ => None, } + .ok_or_else(|| { + syn::Error::new_spanned( + &field.ty, + format!("`#[{HELPER_ATTR}(..)]` needs a plain type name (or `Option`)"), + ) + })?; - // Strip the helper attributes after we’re done reading them - field + let docs = field .attrs - .retain(|a| !a.path().is_ident("enum_") && !a.path().is_ident("bit")); + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .cloned() + .collect(); - Ok(generated) + Ok(Some(GeneratedType { + name, + vis: field.vis.clone(), + docs, + body, + })) } +// ============================================================================ +// Entry point +// ============================================================================ + pub fn expand(_attr: TokenStream, item: TokenStream) -> syn::Result { let mut input: syn::DeriveInput = syn::parse2(item)?; let mut generated = Vec::new(); - if let Data::Struct(data) = &mut input.data { - // Only named fields are supported - if let Fields::Named(fields) = &mut data.fields { - for field in &mut fields.named { - // Generate type - if let Some(ty) = extract_generated_type(field)? { - generated.push(ty.into_tokens()); - } - } + let Data::Struct(data) = &mut input.data else { + return Err(syn::Error::new_spanned( + &input.ident, + "`#[define_options]` can only be used on structs", + )); + }; + let Fields::Named(fields) = &mut data.fields else { + return Err(syn::Error::new_spanned( + &input.ident, + "`#[define_options]` requires a struct with named fields", + )); + }; + + for field in &mut fields.named { + if let Some(ty) = take_generated_type(field)? { + generated.push(ty.into_tokens()); } } diff --git a/rapidquery-macros/lib.rs b/rapidquery-macros/lib.rs index 36f466b..419118f 100644 --- a/rapidquery-macros/lib.rs +++ b/rapidquery-macros/lib.rs @@ -3,8 +3,8 @@ use proc_macro::TokenStream; mod cust_protocol; mod define_options; mod pyclass; -mod pydialect_constructor; mod pyerr; +mod pyo3_constructor; /// Defines a pyo3 class with a required module path. /// @@ -84,20 +84,35 @@ pub fn cust_protocol(input: TokenStream) -> TokenStream { } } -/// Helps you to create DialectFlags. +/// generates the `enum` / `bitflags` types that the +/// fields of a config struct refer to. +/// +/// Each field declares its type's body with a `#[define(..)]` attribute. The +/// type name is taken from the field's type (`Option<..>` is looked through), +/// and the field's doc comments become the type's docs. /// /// ```ignore -/// #[rapidquery_macros::dialect_flags] -/// pub struct DialectFlags { -/// pub placeholder_character: char, -/// /// Positional parameter syntax -/// #[enum_(#[default] Simple, Numbered)] -/// pub placeholder_syntax: PlaceholderSyntax, -/// /// Like escape. -/// #[bit(SCALAR = 1 << 0, ANY = 1 << 1)] -/// pub like_escape: LikeEscape, +/// #[define_options] +/// pub struct LikeConfig { +/// /// Where an inlined `ESCAPE` clause is legal. +/// #[define(bits { SCALAR, ANY })] +/// pub escape: LikeEscape, +/// +/// /// How `LIKE ANY` is rendered. +/// #[define(enum { #[default] OrExpansion, ArrayConstructor, PatternList })] +/// pub any: LikeAny, /// } /// ``` +/// +/// This mirrors `pyo3_constructor!`, where the same fields are written as +/// `bits LikeEscape { SCALAR, ANY }` / `enum LikeAny { .. }`. +/// +/// * `enum { A, B }` — a fieldless enum (derives `Default`, so mark one +/// variant `#[default]` if the enum is ever defaulted). +/// * `bits { A, B }` — a `u8` bitflags type. Flags get bit `1 << index` +/// automatically; write `A = ` to override the value. +/// +/// Attributes (e.g. docs, `#[default]`) on variants and flags are kept. #[proc_macro_attribute] pub fn define_options(attr: TokenStream, item: TokenStream) -> TokenStream { match define_options::expand(attr.into(), item.into()) { @@ -106,9 +121,57 @@ pub fn define_options(attr: TokenStream, item: TokenStream) -> TokenStream { } } +/// generates the PyO3 keyword-only constructor, getters, +/// `Default` impl and exported `OPT_*` constants for a "config" struct. +/// +/// # Syntax +/// +/// ```ignore +/// pyo3_constructor! { +/// StructName => export_function_name { +/// field: [= ], +/// ... +/// } +/// } +/// ``` +/// +/// ## Field specs +/// +/// | Spec | Python type | Default syntax | +/// |----------------------------------------|-------------|-------------------------| +/// | `Type` (e.g. `bool`, `u32`) | `Type` | any Rust expression | +/// | `class Type` | pyclass | any Rust expression | +/// | `enum Type { A, B }` | `int` | a variant: `A` | +/// | `bits Type { X, Y }` | `int` | `empty` or `X \| Y` | +/// | `template(key, opt?; 1)` | `str` | a string literal | +/// +/// * Wrap any spec in `Option<..>` to make the field optional. For optional +/// fields the default is `None`, or the *inner* value (it is wrapped in +/// `Some` for you). +/// * `enum` / `bits` also export one `OPT__` module constant per +/// variant/flag. +/// * `template(..)` lists the placeholders. `_` is the positional `{}`, +/// `name?` marks an optional placeholder (otherwise it is required). +/// After `;` comes how many placeholders the template may contain: `2` or +/// a range `1..=3`. A template field always needs a default. +/// +/// ## Example +/// +/// ```ignore +/// pyo3_constructor! { +/// LikeConfig => export_like_config_options { +/// escape: bits LikeEscape { SCALAR, ANY } = empty, +/// any: enum LikeAny { OrExpansion, PatternList } = OrExpansion, +/// bit: Option = None, +/// concat: template(left, right; 2) = "{left} || {right}", +/// strict: bool = false, +/// nested: class OtherConfig = OtherConfig::default(), +/// } +/// } +/// ``` #[proc_macro] -pub fn pydialect_constructor(input: TokenStream) -> TokenStream { - match pydialect_constructor::expand(input.into()) { +pub fn pyo3_constructor(input: TokenStream) -> TokenStream { + match pyo3_constructor::expand(input.into()) { Ok(tokens) => tokens.into(), Err(err) => err.to_compile_error().into(), } diff --git a/rapidquery-macros/pydialect_constructor.rs b/rapidquery-macros/pydialect_constructor.rs deleted file mode 100644 index 1e4734a..0000000 --- a/rapidquery-macros/pydialect_constructor.rs +++ /dev/null @@ -1,238 +0,0 @@ -use proc_macro2::Span; -use proc_macro2::TokenStream; -use quote::quote; -use syn::Attribute; -use syn::Error; -use syn::Expr; -use syn::Ident; -use syn::Token; -use syn::Type; -use syn::ext::IdentExt; -use syn::parse::Parse; -use syn::parse::ParseStream; -use syn::punctuated::Punctuated; - -enum Kind { - /// `name: Ty = default` - Plain, - /// `#[enum_(A, B)] name: Ty = A` - Enum { variants: Vec }, - /// `#[bit(A, B)] name: Ty = 0u8` - Bit { flags: Vec }, -} - -struct Field { - name: Ident, - ty: Type, - default: Expr, - kind: Kind, -} - -struct Fields { - fields: Punctuated, -} - -impl Parse for Fields { - fn parse(input: ParseStream) -> syn::Result { - Ok(Self { - fields: Punctuated::parse_terminated(input)?, - }) - } -} - -impl Parse for Field { - fn parse(input: ParseStream) -> syn::Result { - let attrs = input.call(Attribute::parse_outer)?; - let name: Ident = input.parse()?; - input.parse::()?; - let ty: Type = input.parse()?; - input.parse::()?; - let default: Expr = input.parse()?; - - let mut kind = Kind::Plain; - let mut seen = false; - - for attr in &attrs { - if seen { - return Err(Error::new_spanned( - attr, - "a field can have at most one of `#[enum_(..)]` / `#[bit(..)]`", - )); - } - let is_enum = if attr.path().is_ident("enum_") { - true - } else if attr.path().is_ident("bit") { - false - } else { - return Err(Error::new_spanned( - attr, - "unknown attribute, expected `#[enum_(..)]` or `#[bit(..)]`", - )); - }; - - let names: Vec = attr - .parse_args_with(Punctuated::::parse_terminated)? - .into_iter() - .collect(); - - if names.is_empty() { - return Err(Error::new_spanned(attr, "expected at least one name")); - } - - kind = if is_enum { - Kind::Enum { variants: names } - } else { - Kind::Bit { flags: names } - }; - - seen = true; - } - - Ok(Self { - name, - ty, - default, - kind, - }) - } -} - -/// `placeholder_syntax` -> `PLACEHOLDER_SYNTAX`, `FooBar` -> `FOO_BAR`, `SCALAR` -> `SCALAR`, -/// `HTTPServer` -> `HTTP_SERVER`. -fn screaming_snake(s: &str) -> String { - let chars: Vec = s.chars().collect(); - let mut out = String::with_capacity(s.len() + 4); - for (i, &c) in chars.iter().enumerate() { - if i > 0 && c.is_uppercase() { - let prev = chars[i - 1]; - let next_is_lower = chars.get(i + 1).is_some_and(|n| n.is_lowercase()); - if prev.is_lowercase() || prev.is_ascii_digit() || (prev.is_uppercase() && next_is_lower) { - out.push('_'); - } - } - out.extend(c.to_uppercase()); - } - out -} - -fn const_name(field: &Ident, item: &Ident) -> String { - format!( - "OPT_{}_{}", - screaming_snake(&field.unraw().to_string()), - screaming_snake(&item.unraw().to_string()), - ) -} - -pub fn expand(input: TokenStream) -> syn::Result { - let input: Fields = syn::parse2(input)?; - - if input.fields.is_empty() { - return Err(Error::new(Span::call_site(), "expected at least one field declaration")); - } - - let mut exports = Vec::::new(); // `module.add(..)?;` statements - let mut sig = Vec::::new(); // entries of `signature = (*, ..)` - let mut params = Vec::::new(); // parameters of `__new__` - let mut inits = Vec::::new(); // fields of the `DialectConfig { .. }` literal - let mut getters = Vec::::new(); // `#[getter]` methods - - for f in &input.fields { - let name = &f.name; - let ty = &f.ty; - let name_str = name.unraw().to_string(); - - match &f.kind { - Kind::Plain => { - let default = &f.default; - sig.push(quote!(#name = #default)); - params.push(quote!(#name: #ty)); - inits.push(quote!(#name)); - getters.push(quote! { - #[getter] - fn #name(&self) -> #ty { self.0.#name } - }); - } - - Kind::Enum { variants } => { - for v in variants { - let c = const_name(name, v); - exports.push(quote!(module.add(#c, #ty::#v as u8)?;)); - } - - let default = &f.default; - - sig.push(quote!(#name = #default as u8)); - params.push(quote!(#name: u8)); - - // `X as u8` is not a valid pattern, so compare in match guards instead. - inits.push(quote! { - #name: match #name { - #( __v if __v == #ty::#variants as u8 => #ty::#variants, )* - __v => return ::core::result::Result::Err( - pyo3::exceptions::PyValueError::new_err( - format!("invalid value {} for `{}`", __v, #name_str), - ), - ), - } - }); - - getters.push(quote! { - #[getter] - fn #name(&self) -> u8 { self.0.#name as u8 } - }); - } - - Kind::Bit { flags } => { - let default = &f.default; - - for flag in flags { - let c = const_name(name, flag); - exports.push(quote!(module.add(#c, #ty::#flag.bits())?;)); - } - - sig.push(quote!(#name = #default)); - params.push(quote!(#name: u8)); - - inits.push(quote! { - #name: #ty::from_bits(#name).ok_or_else(|| { - pyo3::exceptions::PyValueError::new_err( - format!("invalid bits {:#b} for `{}`", #name, #name_str), - ) - })? - }); - - getters.push(quote! { - #[getter] - fn #name(&self) -> u8 { self.0.#name.bits() } - }); - } - } - } - - Ok(quote! { - pub(super) fn export_options( - module: &pyo3::Bound<'_, pyo3::types::PyModule>, - ) -> pyo3::PyResult<()> { - use pyo3::types::PyModuleMethods; - #(#exports)* - Ok(()) - } - - #[pyo3::pymethods] - impl PyDialect { - #[new] - #[pyo3(signature = (*, #(#sig),*))] - #[allow( - clippy::too_many_arguments, - clippy::fn_params_excessive_bools, - clippy::cognitive_complexity - )] - fn __new__(#(#params),*) -> pyo3::PyResult { - let inner = DialectConfig { #(#inits),* }; - Ok(Self(inner)) - } - - #(#getters)* - } - }) -} diff --git a/rapidquery-macros/pyo3_constructor.rs b/rapidquery-macros/pyo3_constructor.rs new file mode 100644 index 0000000..98ca9d6 --- /dev/null +++ b/rapidquery-macros/pyo3_constructor.rs @@ -0,0 +1,623 @@ +//! Assisted-by: Claude + +use proc_macro2::Span; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Expr; +use syn::Ident; +use syn::LitInt; +use syn::Token; +use syn::Type; +use syn::ext::IdentExt; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::punctuated::Punctuated; + +/// One placeholder of a template, e.g. `left` or `quote?`. +struct TemplateKey { + /// Placeholder name. Empty for the positional `{}`. + name: String, + required: bool, +} + +/// What kind of value a field holds. Decides conversion, getter and exports. +enum Kind { + /// `name: Ty` — passed through unchanged. + Plain(Type), + /// `name: class Ty` — a `#[pyclass]` value, returned to Python by clone. + Class(Type), + /// `name: enum Ty { A, B }` — a Rust enum exposed to Python as `u8`. + Enum { ty: Type, variants: Vec }, + /// `name: bits Ty { X, Y }` — a bitflags type exposed to Python as `u8`. + Bits { ty: Type, flags: Vec }, + /// `name: template(..; n)` — `template::Template`, exposed as `str`. + Template { + keys: Vec, + min: LitInt, + max: LitInt, + }, +} + +/// The `= ...` part of a field. Its syntax depends on the field's [`Kind`]. +enum DefaultValue { + /// No default: the argument is required in Python. + Required, + /// `None` (only valid for `Option<..>` fields). + Null, + /// A Rust expression (plain / class / template fields). + Expr(Expr), + /// An enum variant name. + Variant(Ident), + /// A set of bit flags. Empty means no flags (`empty`). + Flags(Vec), +} + +struct Field { + name: Ident, + is_option: bool, + kind: Kind, + default: DefaultValue, +} + +struct MacroInput { + /// Struct to generate the constructor for. + target: Ident, + /// Name of the generated function that registers the `OPT_*` constants. + export_fn: Ident, + fields: Punctuated, +} + +/// Returns the next token as a string if it is an identifier, without consuming it. +fn peek_word(input: ParseStream) -> Option { + input.cursor().ident().map(|(ident, _)| ident.to_string()) +} + +/// Parses `{ A, B, C }` (at least one item). +fn parse_braced_idents(input: ParseStream, what: &str) -> syn::Result> { + let content; + let brace = syn::braced!(content in input); + let items: Vec = Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); + + if items.is_empty() { + return Err(syn::Error::new( + brace.span.join(), + format!("expected at least one {what}"), + )); + } + Ok(items) +} + +/// Errors if `item` is not listed in `members`. +fn check_member(item: &Ident, members: &[Ident], what: &str) -> syn::Result<()> { + if members.contains(item) { + return Ok(()); + } + let list = members.iter().map(Ident::to_string).collect::>().join(", "); + Err(syn::Error::new_spanned( + item, + format!("`{item}` is not a declared {what}; expected one of: {list}"), + )) +} + +impl Parse for TemplateKey { + fn parse(input: ParseStream) -> syn::Result { + let name = if input.peek(Token![_]) { + input.parse::()?; + String::new() + } else { + // `parse_any` so that keywords such as `type` can be used as keys. + Ident::parse_any(input)?.unraw().to_string() + }; + + let optional = input.peek(Token![?]); + if optional { + input.parse::()?; + } + + Ok(Self { + name, + required: !optional, + }) + } +} + +/// Parses the `(keys; count)` part after the `template` keyword. +fn parse_template(input: ParseStream) -> syn::Result { + let args; + let paren = syn::parenthesized!(args in input); + + let mut keys = Vec::new(); + while !args.peek(Token![;]) { + keys.push(args.parse::()?); + if args.peek(Token![,]) { + args.parse::()?; + } else { + break; + } + } + if keys.is_empty() { + return Err(syn::Error::new( + paren.span.join(), + "expected at least one template key (use `_` for a positional `{}`)", + )); + } + + args.parse::()?; + let min: LitInt = args.parse()?; + let max: LitInt = if args.peek(Token![..=]) { + args.parse::()?; + args.parse()? + } else { + min.clone() + }; + + Ok(Kind::Template { keys, min, max }) +} + +/// Parses a [`Kind`] (without the `Option<..>` wrapper). +fn parse_kind(input: ParseStream) -> syn::Result { + if input.peek(Token![enum]) { + input.parse::()?; + let ty: Type = input.parse()?; + let variants = parse_braced_idents(input, "variant")?; + return Ok(Kind::Enum { ty, variants }); + } + + match peek_word(input).as_deref() { + Some("bits") => { + input.parse::()?; + let ty: Type = input.parse()?; + let flags = parse_braced_idents(input, "flag")?; + Ok(Kind::Bits { ty, flags }) + } + Some("class") => { + input.parse::()?; + Ok(Kind::Class(input.parse()?)) + } + Some("template") => { + input.parse::()?; + parse_template(input) + } + _ => Ok(Kind::Plain(input.parse()?)), + } +} + +/// Parses a field spec; returns `(is_option, kind)`. +fn parse_spec(input: ParseStream) -> syn::Result<(bool, Kind)> { + if peek_word(input).as_deref() == Some("Option") && input.peek2(Token![<]) { + input.parse::()?; + input.parse::()?; + let kind = parse_kind(input)?; + input.parse::]>()?; + Ok((true, kind)) + } else { + Ok((false, parse_kind(input)?)) + } +} + +impl DefaultValue { + fn parse(input: ParseStream, kind: &Kind, is_option: bool) -> syn::Result { + if !input.peek(Token![=]) { + return Ok(Self::Required); + } + input.parse::()?; + + if peek_word(input).as_deref() == Some("None") { + let none: Ident = input.parse()?; + if !is_option { + return Err(syn::Error::new_spanned( + none, + "`None` is only valid for `Option<..>` fields", + )); + } + return Ok(Self::Null); + } + + match kind { + Kind::Enum { variants, .. } => { + let variant: Ident = input.parse()?; + check_member(&variant, variants, "variant")?; + Ok(Self::Variant(variant)) + } + Kind::Bits { flags, .. } => { + if peek_word(input).as_deref() == Some("empty") { + input.parse::()?; + return Ok(Self::Flags(Vec::new())); + } + let used = Punctuated::::parse_separated_nonempty(input)?; + let used: Vec = used.into_iter().collect(); + for flag in &used { + check_member(flag, flags, "flag")?; + } + Ok(Self::Flags(used)) + } + _ => Ok(Self::Expr(input.parse()?)), + } + } +} + +impl Parse for Field { + fn parse(input: ParseStream) -> syn::Result { + let name: Ident = input.parse()?; + input.parse::()?; + let (is_option, kind) = parse_spec(input)?; + let default = DefaultValue::parse(input, &kind, is_option)?; + + Ok(Self { + name, + is_option, + kind, + default, + }) + } +} + +impl Parse for MacroInput { + fn parse(input: ParseStream) -> syn::Result { + let target: Ident = input.parse()?; + input.parse::]>()?; + let export_fn: Ident = input.parse()?; + + let content; + syn::braced!(content in input); + let fields = Punctuated::parse_terminated(&content)?; + + Ok(Self { + target, + export_fn, + fields, + }) + } +} + +/// `placeholder_syntax` -> `PLACEHOLDER_SYNTAX`, `FooBar` -> `FOO_BAR`, +/// `SCALAR` -> `SCALAR`, `HTTPServer` -> `HTTP_SERVER`. +fn screaming_snake(s: &str) -> String { + let chars: Vec = s.chars().collect(); + let mut out = String::with_capacity(s.len() + 4); + + for (i, &c) in chars.iter().enumerate() { + if i > 0 && c.is_uppercase() { + let prev = chars[i - 1]; + let next_is_lower = chars.get(i + 1).is_some_and(|n| n.is_lowercase()); + let word_boundary = prev.is_lowercase() || prev.is_ascii_digit() || (prev.is_uppercase() && next_is_lower); + if word_boundary { + out.push('_'); + } + } + out.extend(c.to_uppercase()); + } + out +} + +/// Name of the exported module constant: `OPT__`. +fn const_name(field: &Ident, item: &Ident) -> String { + format!( + "OPT_{}_{}", + screaming_snake(&field.unraw().to_string()), + screaming_snake(&item.unraw().to_string()), + ) +} + +/// Everything one field contributes to the final expansion. +struct Generated { + /// `module.add(..)` statements for the export function. + exports: Vec, + /// Entry in `#[pyo3(signature = (*, ..))]`: `name` or `name = default`. + signature: TokenStream, + /// Parameter of `__new__`: `name: Type`. + param: TokenStream, + /// Field initializer inside `__new__`: `name: `. + init: TokenStream, + /// The `#[getter]` method. + getter: TokenStream, + /// Field initializer inside `Default::default`. + default_init: TokenStream, +} + +/// `&[("key", required), ...]`, the form `Template::parse` expects. +fn template_keys(keys: &[TemplateKey]) -> TokenStream { + let items = keys.iter().map(|k| { + let (name, required) = (&k.name, k.required); + quote!((#name, #required)) + }); + quote!(&[#(#items),*]) +} + +impl Field { + /// The Rust type written by the user, for kinds that have one. + fn ty(&self) -> Option<&Type> { + match &self.kind { + Kind::Plain(ty) | Kind::Class(ty) | Kind::Enum { ty, .. } | Kind::Bits { ty, .. } => Some(ty), + Kind::Template { .. } => None, + } + } + + fn wrap_option(&self, inner: TokenStream) -> TokenStream { + if self.is_option { quote!(Option<#inner>) } else { inner } + } + + /// Type of the struct field. + fn rust_ty(&self) -> TokenStream { + let inner = match self.ty() { + Some(ty) => quote!(#ty), + None => quote!(template::Template), + }; + self.wrap_option(inner) + } + + /// Type of the `__new__` parameter (what Python passes in). + fn param_ty(&self) -> TokenStream { + let inner = match &self.kind { + Kind::Plain(ty) | Kind::Class(ty) => quote!(#ty), + Kind::Enum { .. } | Kind::Bits { .. } => quote!(u8), + Kind::Template { .. } => quote!(&str), + }; + self.wrap_option(inner) + } + + /// Turns the Python-side `value` expression into the Rust field value. + /// May `return Err(..)` / use `?`, so it must be used inside `__new__`. + fn convert(&self, value: &TokenStream) -> TokenStream { + let name_str = self.name.unraw().to_string(); + let value_error = quote!(::pyo3::exceptions::PyValueError); + + match &self.kind { + Kind::Plain(_) | Kind::Class(_) => value.clone(), + + Kind::Enum { ty, variants } => quote! { + match #value { + #( __x if __x == <#ty>::#variants as u8 => <#ty>::#variants, )* + __x => return ::core::result::Result::Err( + #value_error::new_err(format!("invalid value {} for `{}`", __x, #name_str)), + ), + } + }, + + Kind::Bits { ty, .. } => quote! { + <#ty>::from_bits(#value).ok_or_else(|| { + #value_error::new_err(format!("invalid bits {:#b} for `{}`", #value, #name_str)) + })? + }, + + Kind::Template { keys, min, max } => { + let keys = template_keys(keys); + quote! { + template::Template::parse(#value, #keys, #min, #max) + .map_err(|e| #value_error::new_err(e.to_string()))? + } + } + } + } + + /// `name: ` for `__new__`; unwraps `Option` around the conversion. + fn init(&self) -> TokenStream { + let name = &self.name; + + if matches!(self.kind, Kind::Plain(_) | Kind::Class(_)) { + return quote!(#name: #name); + } + + if self.is_option { + let converted = self.convert("e!(__value)); + quote!(#name: match #name { None => None, Some(__value) => Some(#converted) }) + } else { + let converted = self.convert("e!(#name)); + quote!(#name: #converted) + } + } + + /// A getter that maps `&field` through `body` (written in terms of `__x`). + fn mapped_getter(&self, ret: TokenStream, body: TokenStream) -> TokenStream { + let name = &self.name; + if self.is_option { + quote! { + #[getter] + fn #name(&self) -> Option<#ret> { self.#name.as_ref().map(|__x| #body) } + } + } else { + quote! { + #[getter] + fn #name(&self) -> #ret { let __x = &self.#name; #body } + } + } + } + + fn getter(&self) -> TokenStream { + let name = &self.name; + + match &self.kind { + Kind::Plain(_) => { + let ty = self.rust_ty(); + quote! { #[getter] fn #name(&self) -> #ty { self.#name } } + } + + Kind::Class(_) => { + let signature = quote! { + fn #name<'a>(&self, py: ::pyo3::Python<'a>) + -> ::pyo3::PyResult<::pyo3::Bound<'a, ::pyo3::PyAny>> + }; + if self.is_option { + quote! { + #[getter] + #signature { + match self.#name.as_ref() { + Some(x) => ::pyo3::Bound::new(py, x.clone()).map(|x| x.into_any()), + None => Ok(py.None().into_bound(py)), + } + } + } + } else { + quote! { + #[getter] + #signature { + ::pyo3::Bound::new(py, self.#name.clone()).map(|x| x.into_any()) + } + } + } + } + + Kind::Enum { .. } => self.mapped_getter(quote!(u8), quote!(*__x as u8)), + Kind::Bits { .. } => self.mapped_getter(quote!(u8), quote!(__x.bits())), + Kind::Template { .. } => self.mapped_getter(quote!(&str), quote!(__x.as_str())), + } + } + + /// `(python_default, rust_default)` or `None` if the field is required. + /// + /// * `python_default` is what goes into the `#[pyo3(signature)]` + /// (same type as the `__new__` parameter). + /// * `rust_default` is the struct-field value used by `Default`. + fn defaults(&self) -> Option<(TokenStream, TokenStream)> { + let (py, rust) = match (&self.kind, &self.default) { + (_, DefaultValue::Required) => return None, + (_, DefaultValue::Null) => return Some((quote!(None), quote!(None))), + + (Kind::Enum { ty, .. }, DefaultValue::Variant(v)) => (quote!(<#ty>::#v as u8), quote!(<#ty>::#v)), + + (Kind::Bits { ty, .. }, DefaultValue::Flags(flags)) => { + let bits = if flags.is_empty() { + quote!(0u8) + } else { + quote!(#(<#ty>::#flags.bits())|*) + }; + (bits.clone(), quote!(<#ty>::from_bits_truncate(#bits))) + } + + (Kind::Template { keys, min, max }, DefaultValue::Expr(e)) => { + let keys = template_keys(keys); + let msg = format!("invalid default template for `{}`", self.name.unraw()); + ( + quote!(#e), + quote!(template::Template::parse(#e, #keys, #min, #max).expect(#msg)), + ) + } + + (_, DefaultValue::Expr(e)) => (quote!(#e), quote!(#e)), + + _ => unreachable!("the parser ties the default syntax to the field kind"), + }; + + if self.is_option { + Some((quote!(Some(#py)), quote!(Some(#rust)))) + } else { + Some((py, rust)) + } + } + + fn exports(&self) -> Vec { + let name = &self.name; + match &self.kind { + Kind::Enum { ty, variants } => variants + .iter() + .map(|v| { + let c = const_name(name, v); + quote!(module.add(#c, <#ty>::#v as u8)?;) + }) + .collect(), + Kind::Bits { ty, flags } => flags + .iter() + .map(|f| { + let c = const_name(name, f); + quote!(module.add(#c, <#ty>::#f.bits())?;) + }) + .collect(), + _ => Vec::new(), + } + } + + fn generate(&self) -> syn::Result { + let name = &self.name; + let defaults = self.defaults(); + + let signature = match &defaults { + Some((py, _)) => quote!(#name = #py), + None => quote!(#name), + }; + + let default_init = match defaults { + Some((_, rust)) => quote!(#name: #rust), + None if matches!(self.kind, Kind::Template { .. }) => { + return Err(syn::Error::new_spanned( + name, + "a default value is required for template fields", + )); + } + None => { + let ty = self.rust_ty(); + quote!(#name: <#ty as ::core::default::Default>::default()) + } + }; + + let param_ty = self.param_ty(); + + Ok(Generated { + exports: self.exports(), + signature, + param: quote!(#name: #param_ty), + init: self.init(), + getter: self.getter(), + default_init, + }) + } +} + +pub fn expand(input: TokenStream) -> syn::Result { + let MacroInput { + target, + export_fn, + fields, + } = syn::parse2(input)?; + + if fields.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "expected at least one field declaration", + )); + } + + let generated = fields.iter().map(Field::generate).collect::>>()?; + + let exports: Vec<_> = generated.iter().flat_map(|g| g.exports.iter()).collect(); + let signatures: Vec<_> = generated.iter().map(|g| &g.signature).collect(); + let params: Vec<_> = generated.iter().map(|g| &g.param).collect(); + let inits: Vec<_> = generated.iter().map(|g| &g.init).collect(); + let getters: Vec<_> = generated.iter().map(|g| &g.getter).collect(); + let default_inits: Vec<_> = generated.iter().map(|g| &g.default_init).collect(); + + Ok(quote! { + impl ::core::default::Default for #target { + fn default() -> Self { + Self { #(#default_inits),* } + } + } + + /// Registers the `OPT_*` constants of this type on the Python module. + #[cold] + fn #export_fn( + module: &::pyo3::Bound<'_, ::pyo3::types::PyModule>, + ) -> ::pyo3::PyResult<()> { + use ::pyo3::types::PyModuleMethods; + #(#exports)* + Ok(()) + } + + #[::pyo3::pymethods] + impl #target { + #[new] + #[pyo3(signature = (*, #(#signatures),*))] + #[allow( + clippy::too_many_arguments, + clippy::fn_params_excessive_bools, + clippy::cognitive_complexity + )] + fn __new__(#(#params),*) -> ::pyo3::PyResult { + Ok(Self { #(#inits),* }) + } + + #(#getters)* + } + }) +} diff --git a/rapidquery/dialect.py b/rapidquery/dialect.py index 693bf36..da3c793 100644 --- a/rapidquery/dialect.py +++ b/rapidquery/dialect.py @@ -1,285 +1,285 @@ -from ._core.dialect import ( - OPT_ARRAY_FORMAT_CONSTRUCTOR, - OPT_ARRAY_FORMAT_FUNCTION, - OPT_ARRAY_FORMAT_LIST, - OPT_ARRAY_FORMAT_UNSUPPORTED, - OPT_BIT_FORMAT_POSTFIX_BIG_B, - OPT_BIT_FORMAT_PREFIX_BIG_B, - OPT_BIT_FORMAT_PREFIX_SMALL_B, - OPT_BIT_FORMAT_UNSUPPORTED, - OPT_BITWISE_CALL_SYNTAX_BIT_FUNCTION, - OPT_BITWISE_CALL_SYNTAX_INFIX, - OPT_BITWISE_CALL_SYNTAX_SHIFT_FUNCTION, - OPT_BITWISE_OPERATIONS_AND, - OPT_BITWISE_OPERATIONS_NOT, - OPT_BITWISE_OPERATIONS_OR, - OPT_BITWISE_OPERATIONS_SHL, - OPT_BITWISE_OPERATIONS_SHR, - OPT_BITWISE_XOR_SPELLING_BIT_FUNCTION, - OPT_BITWISE_XOR_SPELLING_CARET, - OPT_BITWISE_XOR_SPELLING_FUNCTION, - OPT_BITWISE_XOR_SPELLING_HASH, - OPT_BITWISE_XOR_SPELLING_UNSUPPORTED, - OPT_BOOL_FORMAT_INTEGER, - OPT_BOOL_FORMAT_KEYWORD, - OPT_BYTES_FORMAT_HEX_TO_RAW, - OPT_BYTES_FORMAT_POSTFIX_XBV, - OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X, - OPT_BYTES_FORMAT_PREFIX_BIG_X, - OPT_BYTES_FORMAT_PREFIX_ZERO_X, - OPT_BYTES_FORMAT_SLASH_X_BLOB, - OPT_BYTES_FORMAT_SLASH_X_BYTEA, - OPT_BYTES_FORMAT_TO_BINARY, - OPT_BYTES_FORMAT_TO_VARBYTE, - OPT_BYTES_FORMAT_UNHEX, - OPT_CAST_TRY_CAST_SAFE_CAST, - OPT_CAST_TRY_CAST_TRY_CAST, - OPT_CAST_TRY_CAST_TRY_CAST_WORD, - OPT_CAST_TRY_CAST_UNSUPPORTED, - OPT_COLLATE_SPELLING_FUNCTION_STRING, - OPT_COLLATE_SPELLING_INFIX_IDENT, - OPT_COLLATE_SPELLING_UNSUPPORTED, - OPT_DATE_FORMAT_KEYWORD, - OPT_DATE_FORMAT_QUOTED, - OPT_FUNCTION_FILTER_FILTER, - OPT_FUNCTION_FILTER_FILTER_WHERE, - OPT_FUNCTION_FILTER_INSIDE_WHERE, - OPT_FUNCTION_FILTER_UNSUPPORTED, - OPT_IDENTIFIER_ESCAPE_BACKSLASH, - OPT_IDENTIFIER_ESCAPE_DOUBLE, - OPT_IDENTIFIER_FORMAT_BACKTICK, - OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE, - OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET, - OPT_ILIKE_EXPRESSION_STANDARD, - OPT_ILIKE_EXPRESSION_USE_LIKE, - OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER, - OPT_INET_FORMAT_CAST, - OPT_INET_FORMAT_QUOTED, - OPT_INET_FORMAT_UNSUPPORTED, - OPT_IS_DISTINCT_FROM_SPELLING_NULL_SAFE_EQUAL, - OPT_IS_DISTINCT_FROM_SPELLING_STANDARD, - OPT_IS_DISTINCT_FROM_SPELLING_UNSUPPORTED, - OPT_JSON_FORMAT_JSON_KEYWORD, - OPT_JSON_FORMAT_PARSE_JSON, - OPT_JSON_FORMAT_QUOTED, - OPT_LIKE_ANY_ARRAY_CONSTRUCTOR, - OPT_LIKE_ANY_OR_EXPANSION, - OPT_LIKE_ANY_PATTERN_LIST, - OPT_LIKE_ESCAPE_ANY, - OPT_LIKE_ESCAPE_SCALAR, - OPT_MACADDR_FORMAT_CAST, - OPT_MACADDR_FORMAT_QUOTED, - OPT_MACADDR_FORMAT_UNSUPPORTED, - OPT_MODULO_SPELLING_FUNCTION, - OPT_MODULO_SPELLING_INFIX, - OPT_MODULO_SPELLING_PERCENT, - OPT_NULL_TREATMENT_INSIDE_PARENS, - OPT_NULL_TREATMENT_POSTFIX, - OPT_NULL_TREATMENT_UNSUPPORTED, - OPT_PLACEHOLDER_SYNTAX_NUMBERED, - OPT_PLACEHOLDER_SYNTAX_SIMPLE, - OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS, - OPT_RLIKE_EXPRESSION_REGEXP_LIKE, - OPT_RLIKE_EXPRESSION_REGEXP_MATCHES, - OPT_RLIKE_EXPRESSION_STANDARD, - OPT_RLIKE_EXPRESSION_TILDE, - OPT_RLIKE_EXPRESSION_UNSUPPORTED, - OPT_SCHEMA_AUTHORIZATION_STYLE_OPTIONAL, - OPT_SCHEMA_AUTHORIZATION_STYLE_UNSUPPORTED, - OPT_SCHEMA_FEATURES_DROP_CASCADE, - OPT_SCHEMA_FEATURES_DROP_RESTRICT, - OPT_SCHEMA_FEATURES_EMBEDDED_DDL, - OPT_SCHEMA_FEATURES_IF_NOT_EXISTS, - OPT_SCHEMA_FEATURES_IS_DATABASE, - OPT_SCHEMA_FEATURES_UNSUPPORTED, - OPT_SCHEMA_LOCATION_STYLE_EXTERNAL_VOLUME, - OPT_SCHEMA_LOCATION_STYLE_LOCATION_KEYWORD, - OPT_SCHEMA_LOCATION_STYLE_MANAGED_LOCATION_KEYWORD, - OPT_SCHEMA_LOCATION_STYLE_UNSUPPORTED, - OPT_STRING_CONCAT_CONCAT_FUNCTION, - OPT_STRING_CONCAT_PIPES, - OPT_STRING_CONCAT_PLUS, - OPT_STRING_ESCAPE_BACKSLASH_HEX, - OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO, - OPT_STRING_ESCAPE_BACKSLASH_MYSQL, - OPT_STRING_ESCAPE_BACKSLASH_UNICODE, - OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE, - OPT_STRING_ESCAPE_QUOTE, - OPT_STRING_FORMAT_PREFIX_BIG_E, - OPT_STRING_FORMAT_PREFIX_BIG_N, - OPT_STRING_FORMAT_SINGLE_QUOTE, - OPT_TIME_FORMAT_KEYWORD, - OPT_TIME_FORMAT_QUOTED, - OPT_TIMESTAMP_FORMAT_QUOTED, - OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ, - OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE, - OPT_TRIM_SPELLING_FUNCTION, - OPT_TRIM_SPELLING_SPACES_ONLY, - OPT_TRIM_SPELLING_SQL_FROM, - OPT_TUPLE_ARITY_EMPTY, - OPT_TUPLE_ARITY_ONE, - OPT_TUPLE_SYNTAX_PARENTHESES, - OPT_TUPLE_SYNTAX_ROW, - OPT_TUPLE_SYNTAX_STRUCT, - OPT_TUPLE_SYNTAX_UNSUPPORTED, - OPT_UUID_FORMAT_CAST_UUID, - OPT_UUID_FORMAT_QUOTED, - OPT_UUID_FORMAT_UUID_KEYWORD, - BigQuery, - Databricks, - Dialect, - DuckDb, - Hive, - MsSql, - MySql, - Oracle, - PostgreSql, - Redshift, - Snowflake, - Spark, - Sqlite, - Teradata, -) - -__all__ = [ - "OPT_ARRAY_FORMAT_CONSTRUCTOR", - "OPT_ARRAY_FORMAT_FUNCTION", - "OPT_ARRAY_FORMAT_LIST", - "OPT_ARRAY_FORMAT_UNSUPPORTED", - "OPT_BITWISE_CALL_SYNTAX_BIT_FUNCTION", - "OPT_BITWISE_CALL_SYNTAX_INFIX", - "OPT_BITWISE_CALL_SYNTAX_SHIFT_FUNCTION", - "OPT_BITWISE_OPERATIONS_AND", - "OPT_BITWISE_OPERATIONS_NOT", - "OPT_BITWISE_OPERATIONS_OR", - "OPT_BITWISE_OPERATIONS_SHL", - "OPT_BITWISE_OPERATIONS_SHR", - "OPT_BITWISE_XOR_SPELLING_BIT_FUNCTION", - "OPT_BITWISE_XOR_SPELLING_CARET", - "OPT_BITWISE_XOR_SPELLING_FUNCTION", - "OPT_BITWISE_XOR_SPELLING_HASH", - "OPT_BITWISE_XOR_SPELLING_UNSUPPORTED", - "OPT_BIT_FORMAT_POSTFIX_BIG_B", - "OPT_BIT_FORMAT_PREFIX_BIG_B", - "OPT_BIT_FORMAT_PREFIX_SMALL_B", - "OPT_BIT_FORMAT_UNSUPPORTED", - "OPT_BOOL_FORMAT_INTEGER", - "OPT_BOOL_FORMAT_KEYWORD", - "OPT_BYTES_FORMAT_HEX_TO_RAW", - "OPT_BYTES_FORMAT_POSTFIX_XBV", - "OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X", - "OPT_BYTES_FORMAT_PREFIX_BIG_X", - "OPT_BYTES_FORMAT_PREFIX_ZERO_X", - "OPT_BYTES_FORMAT_SLASH_X_BLOB", - "OPT_BYTES_FORMAT_SLASH_X_BYTEA", - "OPT_BYTES_FORMAT_TO_BINARY", - "OPT_BYTES_FORMAT_TO_VARBYTE", - "OPT_BYTES_FORMAT_UNHEX", - "OPT_CAST_TRY_CAST_SAFE_CAST", - "OPT_CAST_TRY_CAST_TRY_CAST", - "OPT_CAST_TRY_CAST_TRY_CAST_WORD", - "OPT_CAST_TRY_CAST_UNSUPPORTED", - "OPT_COLLATE_SPELLING_FUNCTION_STRING", - "OPT_COLLATE_SPELLING_INFIX_IDENT", - "OPT_COLLATE_SPELLING_UNSUPPORTED", - "OPT_DATE_FORMAT_KEYWORD", - "OPT_DATE_FORMAT_QUOTED", - "OPT_FUNCTION_FILTER_FILTER", - "OPT_FUNCTION_FILTER_FILTER_WHERE", - "OPT_FUNCTION_FILTER_INSIDE_WHERE", - "OPT_FUNCTION_FILTER_UNSUPPORTED", - "OPT_IDENTIFIER_ESCAPE_BACKSLASH", - "OPT_IDENTIFIER_ESCAPE_DOUBLE", - "OPT_IDENTIFIER_FORMAT_BACKTICK", - "OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE", - "OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET", - "OPT_ILIKE_EXPRESSION_STANDARD", - "OPT_ILIKE_EXPRESSION_USE_LIKE", - "OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER", - "OPT_INET_FORMAT_CAST", - "OPT_INET_FORMAT_QUOTED", - "OPT_INET_FORMAT_UNSUPPORTED", - "OPT_IS_DISTINCT_FROM_SPELLING_NULL_SAFE_EQUAL", - "OPT_IS_DISTINCT_FROM_SPELLING_STANDARD", - "OPT_IS_DISTINCT_FROM_SPELLING_UNSUPPORTED", - "OPT_JSON_FORMAT_JSON_KEYWORD", - "OPT_JSON_FORMAT_PARSE_JSON", - "OPT_JSON_FORMAT_QUOTED", - "OPT_LIKE_ANY_ARRAY_CONSTRUCTOR", - "OPT_LIKE_ANY_OR_EXPANSION", - "OPT_LIKE_ANY_PATTERN_LIST", - "OPT_LIKE_ESCAPE_ANY", - "OPT_LIKE_ESCAPE_SCALAR", - "OPT_MACADDR_FORMAT_CAST", - "OPT_MACADDR_FORMAT_QUOTED", - "OPT_MACADDR_FORMAT_UNSUPPORTED", - "OPT_MODULO_SPELLING_FUNCTION", - "OPT_MODULO_SPELLING_INFIX", - "OPT_MODULO_SPELLING_PERCENT", - "OPT_NULL_TREATMENT_INSIDE_PARENS", - "OPT_NULL_TREATMENT_POSTFIX", - "OPT_NULL_TREATMENT_UNSUPPORTED", - "OPT_PLACEHOLDER_SYNTAX_NUMBERED", - "OPT_PLACEHOLDER_SYNTAX_SIMPLE", - "OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS", - "OPT_RLIKE_EXPRESSION_REGEXP_LIKE", - "OPT_RLIKE_EXPRESSION_REGEXP_MATCHES", - "OPT_RLIKE_EXPRESSION_STANDARD", - "OPT_RLIKE_EXPRESSION_TILDE", - "OPT_RLIKE_EXPRESSION_UNSUPPORTED", - "OPT_SCHEMA_AUTHORIZATION_STYLE_OPTIONAL", - "OPT_SCHEMA_AUTHORIZATION_STYLE_UNSUPPORTED", - "OPT_SCHEMA_FEATURES_DROP_CASCADE", - "OPT_SCHEMA_FEATURES_DROP_RESTRICT", - "OPT_SCHEMA_FEATURES_EMBEDDED_DDL", - "OPT_SCHEMA_FEATURES_IF_NOT_EXISTS", - "OPT_SCHEMA_FEATURES_IS_DATABASE", - "OPT_SCHEMA_FEATURES_UNSUPPORTED", - "OPT_SCHEMA_LOCATION_STYLE_EXTERNAL_VOLUME", - "OPT_SCHEMA_LOCATION_STYLE_LOCATION_KEYWORD", - "OPT_SCHEMA_LOCATION_STYLE_MANAGED_LOCATION_KEYWORD", - "OPT_SCHEMA_LOCATION_STYLE_UNSUPPORTED", - "OPT_STRING_CONCAT_CONCAT_FUNCTION", - "OPT_STRING_CONCAT_PIPES", - "OPT_STRING_CONCAT_PLUS", - "OPT_STRING_ESCAPE_BACKSLASH_HEX", - "OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO", - "OPT_STRING_ESCAPE_BACKSLASH_MYSQL", - "OPT_STRING_ESCAPE_BACKSLASH_UNICODE", - "OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE", - "OPT_STRING_ESCAPE_QUOTE", - "OPT_STRING_FORMAT_PREFIX_BIG_E", - "OPT_STRING_FORMAT_PREFIX_BIG_N", - "OPT_STRING_FORMAT_SINGLE_QUOTE", - "OPT_TIMESTAMP_FORMAT_QUOTED", - "OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ", - "OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE", - "OPT_TIME_FORMAT_KEYWORD", - "OPT_TIME_FORMAT_QUOTED", - "OPT_TRIM_SPELLING_FUNCTION", - "OPT_TRIM_SPELLING_SPACES_ONLY", - "OPT_TRIM_SPELLING_SQL_FROM", - "OPT_TUPLE_ARITY_EMPTY", - "OPT_TUPLE_ARITY_ONE", - "OPT_TUPLE_SYNTAX_PARENTHESES", - "OPT_TUPLE_SYNTAX_ROW", - "OPT_TUPLE_SYNTAX_STRUCT", - "OPT_TUPLE_SYNTAX_UNSUPPORTED", - "OPT_UUID_FORMAT_CAST_UUID", - "OPT_UUID_FORMAT_QUOTED", - "OPT_UUID_FORMAT_UUID_KEYWORD", - "BigQuery", - "Databricks", - "Dialect", - "DuckDb", - "Hive", - "MsSql", - "MySql", - "Oracle", - "PostgreSql", - "Redshift", - "Snowflake", - "Spark", - "Sqlite", - "Teradata", -] +# from ._core.dialect import ( +# OPT_ARRAY_FORMAT_CONSTRUCTOR, +# OPT_ARRAY_FORMAT_FUNCTION, +# OPT_ARRAY_FORMAT_LIST, +# OPT_ARRAY_FORMAT_UNSUPPORTED, +# OPT_BIT_FORMAT_POSTFIX_BIG_B, +# OPT_BIT_FORMAT_PREFIX_BIG_B, +# OPT_BIT_FORMAT_PREFIX_SMALL_B, +# OPT_BIT_FORMAT_UNSUPPORTED, +# OPT_BITWISE_CALL_SYNTAX_BIT_FUNCTION, +# OPT_BITWISE_CALL_SYNTAX_INFIX, +# OPT_BITWISE_CALL_SYNTAX_SHIFT_FUNCTION, +# OPT_BITWISE_OPERATIONS_AND, +# OPT_BITWISE_OPERATIONS_NOT, +# OPT_BITWISE_OPERATIONS_OR, +# OPT_BITWISE_OPERATIONS_SHL, +# OPT_BITWISE_OPERATIONS_SHR, +# OPT_BITWISE_XOR_SPELLING_BIT_FUNCTION, +# OPT_BITWISE_XOR_SPELLING_CARET, +# OPT_BITWISE_XOR_SPELLING_FUNCTION, +# OPT_BITWISE_XOR_SPELLING_HASH, +# OPT_BITWISE_XOR_SPELLING_UNSUPPORTED, +# OPT_BOOL_FORMAT_INTEGER, +# OPT_BOOL_FORMAT_KEYWORD, +# OPT_BYTES_FORMAT_HEX_TO_RAW, +# OPT_BYTES_FORMAT_POSTFIX_XBV, +# OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X, +# OPT_BYTES_FORMAT_PREFIX_BIG_X, +# OPT_BYTES_FORMAT_PREFIX_ZERO_X, +# OPT_BYTES_FORMAT_SLASH_X_BLOB, +# OPT_BYTES_FORMAT_SLASH_X_BYTEA, +# OPT_BYTES_FORMAT_TO_BINARY, +# OPT_BYTES_FORMAT_TO_VARBYTE, +# OPT_BYTES_FORMAT_UNHEX, +# OPT_CAST_TRY_CAST_SAFE_CAST, +# OPT_CAST_TRY_CAST_TRY_CAST, +# OPT_CAST_TRY_CAST_TRY_CAST_WORD, +# OPT_CAST_TRY_CAST_UNSUPPORTED, +# OPT_COLLATE_SPELLING_FUNCTION_STRING, +# OPT_COLLATE_SPELLING_INFIX_IDENT, +# OPT_COLLATE_SPELLING_UNSUPPORTED, +# OPT_DATE_FORMAT_KEYWORD, +# OPT_DATE_FORMAT_QUOTED, +# OPT_FUNCTION_FILTER_FILTER, +# OPT_FUNCTION_FILTER_FILTER_WHERE, +# OPT_FUNCTION_FILTER_INSIDE_WHERE, +# OPT_FUNCTION_FILTER_UNSUPPORTED, +# OPT_IDENTIFIER_ESCAPE_BACKSLASH, +# OPT_IDENTIFIER_ESCAPE_DOUBLE, +# OPT_IDENTIFIER_FORMAT_BACKTICK, +# OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE, +# OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET, +# OPT_ILIKE_EXPRESSION_STANDARD, +# OPT_ILIKE_EXPRESSION_USE_LIKE, +# OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER, +# OPT_INET_FORMAT_CAST, +# OPT_INET_FORMAT_QUOTED, +# OPT_INET_FORMAT_UNSUPPORTED, +# OPT_IS_DISTINCT_FROM_SPELLING_NULL_SAFE_EQUAL, +# OPT_IS_DISTINCT_FROM_SPELLING_STANDARD, +# OPT_IS_DISTINCT_FROM_SPELLING_UNSUPPORTED, +# OPT_JSON_FORMAT_JSON_KEYWORD, +# OPT_JSON_FORMAT_PARSE_JSON, +# OPT_JSON_FORMAT_QUOTED, +# OPT_LIKE_ANY_ARRAY_CONSTRUCTOR, +# OPT_LIKE_ANY_OR_EXPANSION, +# OPT_LIKE_ANY_PATTERN_LIST, +# OPT_LIKE_ESCAPE_ANY, +# OPT_LIKE_ESCAPE_SCALAR, +# OPT_MACADDR_FORMAT_CAST, +# OPT_MACADDR_FORMAT_QUOTED, +# OPT_MACADDR_FORMAT_UNSUPPORTED, +# OPT_MODULO_SPELLING_FUNCTION, +# OPT_MODULO_SPELLING_INFIX, +# OPT_MODULO_SPELLING_PERCENT, +# OPT_NULL_TREATMENT_INSIDE_PARENS, +# OPT_NULL_TREATMENT_POSTFIX, +# OPT_NULL_TREATMENT_UNSUPPORTED, +# OPT_PLACEHOLDER_SYNTAX_NUMBERED, +# OPT_PLACEHOLDER_SYNTAX_SIMPLE, +# OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS, +# OPT_RLIKE_EXPRESSION_REGEXP_LIKE, +# OPT_RLIKE_EXPRESSION_REGEXP_MATCHES, +# OPT_RLIKE_EXPRESSION_STANDARD, +# OPT_RLIKE_EXPRESSION_TILDE, +# OPT_RLIKE_EXPRESSION_UNSUPPORTED, +# OPT_SCHEMA_AUTHORIZATION_STYLE_OPTIONAL, +# OPT_SCHEMA_AUTHORIZATION_STYLE_UNSUPPORTED, +# OPT_SCHEMA_FEATURES_DROP_CASCADE, +# OPT_SCHEMA_FEATURES_DROP_RESTRICT, +# OPT_SCHEMA_FEATURES_EMBEDDED_DDL, +# OPT_SCHEMA_FEATURES_IF_NOT_EXISTS, +# OPT_SCHEMA_FEATURES_IS_DATABASE, +# OPT_SCHEMA_FEATURES_UNSUPPORTED, +# OPT_SCHEMA_LOCATION_STYLE_EXTERNAL_VOLUME, +# OPT_SCHEMA_LOCATION_STYLE_LOCATION_KEYWORD, +# OPT_SCHEMA_LOCATION_STYLE_MANAGED_LOCATION_KEYWORD, +# OPT_SCHEMA_LOCATION_STYLE_UNSUPPORTED, +# OPT_STRING_CONCAT_CONCAT_FUNCTION, +# OPT_STRING_CONCAT_PIPES, +# OPT_STRING_CONCAT_PLUS, +# OPT_STRING_ESCAPE_BACKSLASH_HEX, +# OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO, +# OPT_STRING_ESCAPE_BACKSLASH_MYSQL, +# OPT_STRING_ESCAPE_BACKSLASH_UNICODE, +# OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE, +# OPT_STRING_ESCAPE_QUOTE, +# OPT_STRING_FORMAT_PREFIX_BIG_E, +# OPT_STRING_FORMAT_PREFIX_BIG_N, +# OPT_STRING_FORMAT_SINGLE_QUOTE, +# OPT_TIME_FORMAT_KEYWORD, +# OPT_TIME_FORMAT_QUOTED, +# OPT_TIMESTAMP_FORMAT_QUOTED, +# OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ, +# OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE, +# OPT_TRIM_SPELLING_FUNCTION, +# OPT_TRIM_SPELLING_SPACES_ONLY, +# OPT_TRIM_SPELLING_SQL_FROM, +# OPT_TUPLE_ARITY_EMPTY, +# OPT_TUPLE_ARITY_ONE, +# OPT_TUPLE_SYNTAX_PARENTHESES, +# OPT_TUPLE_SYNTAX_ROW, +# OPT_TUPLE_SYNTAX_STRUCT, +# OPT_TUPLE_SYNTAX_UNSUPPORTED, +# OPT_UUID_FORMAT_CAST_UUID, +# OPT_UUID_FORMAT_QUOTED, +# OPT_UUID_FORMAT_UUID_KEYWORD, +# BigQuery, +# Databricks, +# Dialect, +# DuckDb, +# Hive, +# MsSql, +# MySql, +# Oracle, +# PostgreSql, +# Redshift, +# Snowflake, +# Spark, +# Sqlite, +# Teradata, +# ) +# +# __all__ = [ +# "OPT_ARRAY_FORMAT_CONSTRUCTOR", +# "OPT_ARRAY_FORMAT_FUNCTION", +# "OPT_ARRAY_FORMAT_LIST", +# "OPT_ARRAY_FORMAT_UNSUPPORTED", +# "OPT_BITWISE_CALL_SYNTAX_BIT_FUNCTION", +# "OPT_BITWISE_CALL_SYNTAX_INFIX", +# "OPT_BITWISE_CALL_SYNTAX_SHIFT_FUNCTION", +# "OPT_BITWISE_OPERATIONS_AND", +# "OPT_BITWISE_OPERATIONS_NOT", +# "OPT_BITWISE_OPERATIONS_OR", +# "OPT_BITWISE_OPERATIONS_SHL", +# "OPT_BITWISE_OPERATIONS_SHR", +# "OPT_BITWISE_XOR_SPELLING_BIT_FUNCTION", +# "OPT_BITWISE_XOR_SPELLING_CARET", +# "OPT_BITWISE_XOR_SPELLING_FUNCTION", +# "OPT_BITWISE_XOR_SPELLING_HASH", +# "OPT_BITWISE_XOR_SPELLING_UNSUPPORTED", +# "OPT_BIT_FORMAT_POSTFIX_BIG_B", +# "OPT_BIT_FORMAT_PREFIX_BIG_B", +# "OPT_BIT_FORMAT_PREFIX_SMALL_B", +# "OPT_BIT_FORMAT_UNSUPPORTED", +# "OPT_BOOL_FORMAT_INTEGER", +# "OPT_BOOL_FORMAT_KEYWORD", +# "OPT_BYTES_FORMAT_HEX_TO_RAW", +# "OPT_BYTES_FORMAT_POSTFIX_XBV", +# "OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X", +# "OPT_BYTES_FORMAT_PREFIX_BIG_X", +# "OPT_BYTES_FORMAT_PREFIX_ZERO_X", +# "OPT_BYTES_FORMAT_SLASH_X_BLOB", +# "OPT_BYTES_FORMAT_SLASH_X_BYTEA", +# "OPT_BYTES_FORMAT_TO_BINARY", +# "OPT_BYTES_FORMAT_TO_VARBYTE", +# "OPT_BYTES_FORMAT_UNHEX", +# "OPT_CAST_TRY_CAST_SAFE_CAST", +# "OPT_CAST_TRY_CAST_TRY_CAST", +# "OPT_CAST_TRY_CAST_TRY_CAST_WORD", +# "OPT_CAST_TRY_CAST_UNSUPPORTED", +# "OPT_COLLATE_SPELLING_FUNCTION_STRING", +# "OPT_COLLATE_SPELLING_INFIX_IDENT", +# "OPT_COLLATE_SPELLING_UNSUPPORTED", +# "OPT_DATE_FORMAT_KEYWORD", +# "OPT_DATE_FORMAT_QUOTED", +# "OPT_FUNCTION_FILTER_FILTER", +# "OPT_FUNCTION_FILTER_FILTER_WHERE", +# "OPT_FUNCTION_FILTER_INSIDE_WHERE", +# "OPT_FUNCTION_FILTER_UNSUPPORTED", +# "OPT_IDENTIFIER_ESCAPE_BACKSLASH", +# "OPT_IDENTIFIER_ESCAPE_DOUBLE", +# "OPT_IDENTIFIER_FORMAT_BACKTICK", +# "OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE", +# "OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET", +# "OPT_ILIKE_EXPRESSION_STANDARD", +# "OPT_ILIKE_EXPRESSION_USE_LIKE", +# "OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER", +# "OPT_INET_FORMAT_CAST", +# "OPT_INET_FORMAT_QUOTED", +# "OPT_INET_FORMAT_UNSUPPORTED", +# "OPT_IS_DISTINCT_FROM_SPELLING_NULL_SAFE_EQUAL", +# "OPT_IS_DISTINCT_FROM_SPELLING_STANDARD", +# "OPT_IS_DISTINCT_FROM_SPELLING_UNSUPPORTED", +# "OPT_JSON_FORMAT_JSON_KEYWORD", +# "OPT_JSON_FORMAT_PARSE_JSON", +# "OPT_JSON_FORMAT_QUOTED", +# "OPT_LIKE_ANY_ARRAY_CONSTRUCTOR", +# "OPT_LIKE_ANY_OR_EXPANSION", +# "OPT_LIKE_ANY_PATTERN_LIST", +# "OPT_LIKE_ESCAPE_ANY", +# "OPT_LIKE_ESCAPE_SCALAR", +# "OPT_MACADDR_FORMAT_CAST", +# "OPT_MACADDR_FORMAT_QUOTED", +# "OPT_MACADDR_FORMAT_UNSUPPORTED", +# "OPT_MODULO_SPELLING_FUNCTION", +# "OPT_MODULO_SPELLING_INFIX", +# "OPT_MODULO_SPELLING_PERCENT", +# "OPT_NULL_TREATMENT_INSIDE_PARENS", +# "OPT_NULL_TREATMENT_POSTFIX", +# "OPT_NULL_TREATMENT_UNSUPPORTED", +# "OPT_PLACEHOLDER_SYNTAX_NUMBERED", +# "OPT_PLACEHOLDER_SYNTAX_SIMPLE", +# "OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS", +# "OPT_RLIKE_EXPRESSION_REGEXP_LIKE", +# "OPT_RLIKE_EXPRESSION_REGEXP_MATCHES", +# "OPT_RLIKE_EXPRESSION_STANDARD", +# "OPT_RLIKE_EXPRESSION_TILDE", +# "OPT_RLIKE_EXPRESSION_UNSUPPORTED", +# "OPT_SCHEMA_AUTHORIZATION_STYLE_OPTIONAL", +# "OPT_SCHEMA_AUTHORIZATION_STYLE_UNSUPPORTED", +# "OPT_SCHEMA_FEATURES_DROP_CASCADE", +# "OPT_SCHEMA_FEATURES_DROP_RESTRICT", +# "OPT_SCHEMA_FEATURES_EMBEDDED_DDL", +# "OPT_SCHEMA_FEATURES_IF_NOT_EXISTS", +# "OPT_SCHEMA_FEATURES_IS_DATABASE", +# "OPT_SCHEMA_FEATURES_UNSUPPORTED", +# "OPT_SCHEMA_LOCATION_STYLE_EXTERNAL_VOLUME", +# "OPT_SCHEMA_LOCATION_STYLE_LOCATION_KEYWORD", +# "OPT_SCHEMA_LOCATION_STYLE_MANAGED_LOCATION_KEYWORD", +# "OPT_SCHEMA_LOCATION_STYLE_UNSUPPORTED", +# "OPT_STRING_CONCAT_CONCAT_FUNCTION", +# "OPT_STRING_CONCAT_PIPES", +# "OPT_STRING_CONCAT_PLUS", +# "OPT_STRING_ESCAPE_BACKSLASH_HEX", +# "OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO", +# "OPT_STRING_ESCAPE_BACKSLASH_MYSQL", +# "OPT_STRING_ESCAPE_BACKSLASH_UNICODE", +# "OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE", +# "OPT_STRING_ESCAPE_QUOTE", +# "OPT_STRING_FORMAT_PREFIX_BIG_E", +# "OPT_STRING_FORMAT_PREFIX_BIG_N", +# "OPT_STRING_FORMAT_SINGLE_QUOTE", +# "OPT_TIMESTAMP_FORMAT_QUOTED", +# "OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ", +# "OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE", +# "OPT_TIME_FORMAT_KEYWORD", +# "OPT_TIME_FORMAT_QUOTED", +# "OPT_TRIM_SPELLING_FUNCTION", +# "OPT_TRIM_SPELLING_SPACES_ONLY", +# "OPT_TRIM_SPELLING_SQL_FROM", +# "OPT_TUPLE_ARITY_EMPTY", +# "OPT_TUPLE_ARITY_ONE", +# "OPT_TUPLE_SYNTAX_PARENTHESES", +# "OPT_TUPLE_SYNTAX_ROW", +# "OPT_TUPLE_SYNTAX_STRUCT", +# "OPT_TUPLE_SYNTAX_UNSUPPORTED", +# "OPT_UUID_FORMAT_CAST_UUID", +# "OPT_UUID_FORMAT_QUOTED", +# "OPT_UUID_FORMAT_UUID_KEYWORD", +# "BigQuery", +# "Databricks", +# "Dialect", +# "DuckDb", +# "Hive", +# "MsSql", +# "MySql", +# "Oracle", +# "PostgreSql", +# "Redshift", +# "Snowflake", +# "Spark", +# "Sqlite", +# "Teradata", +# ] diff --git a/rapidquery/dialect.pyi b/rapidquery/dialect.pyi index 0b0aeef..871dd15 100644 --- a/rapidquery/dialect.pyi +++ b/rapidquery/dialect.pyi @@ -4,366 +4,22 @@ SQL dialects and the integer flags that configure one. import typing -OPT_ARRAY_FORMAT_CONSTRUCTOR: typing.Final[int] -OPT_ARRAY_FORMAT_FUNCTION: typing.Final[int] -OPT_ARRAY_FORMAT_LIST: typing.Final[int] -OPT_ARRAY_FORMAT_UNSUPPORTED: typing.Final[int] -OPT_BIT_FORMAT_POSTFIX_BIG_B: typing.Final[int] -OPT_BIT_FORMAT_PREFIX_BIG_B: typing.Final[int] -OPT_BIT_FORMAT_PREFIX_SMALL_B: typing.Final[int] -OPT_BIT_FORMAT_UNSUPPORTED: typing.Final[int] -OPT_BITWISE_CALL_SYNTAX_BIT_FUNCTION: typing.Final[int] -OPT_BITWISE_CALL_SYNTAX_INFIX: typing.Final[int] -OPT_BITWISE_CALL_SYNTAX_SHIFT_FUNCTION: typing.Final[int] -OPT_BITWISE_OPERATIONS_AND: typing.Final[int] -OPT_BITWISE_OPERATIONS_NOT: typing.Final[int] -OPT_BITWISE_OPERATIONS_OR: typing.Final[int] -OPT_BITWISE_OPERATIONS_SHL: typing.Final[int] -OPT_BITWISE_OPERATIONS_SHR: typing.Final[int] -OPT_BITWISE_XOR_SPELLING_BIT_FUNCTION: typing.Final[int] -OPT_BITWISE_XOR_SPELLING_CARET: typing.Final[int] -OPT_BITWISE_XOR_SPELLING_FUNCTION: typing.Final[int] -OPT_BITWISE_XOR_SPELLING_HASH: typing.Final[int] -OPT_BITWISE_XOR_SPELLING_UNSUPPORTED: typing.Final[int] -OPT_BOOL_FORMAT_INTEGER: typing.Final[int] -OPT_BOOL_FORMAT_KEYWORD: typing.Final[int] -OPT_BYTES_FORMAT_HEX_TO_RAW: typing.Final[int] -OPT_BYTES_FORMAT_POSTFIX_XBV: typing.Final[int] -OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X: typing.Final[int] -OPT_BYTES_FORMAT_PREFIX_BIG_X: typing.Final[int] -OPT_BYTES_FORMAT_PREFIX_ZERO_X: typing.Final[int] -OPT_BYTES_FORMAT_SLASH_X_BLOB: typing.Final[int] -OPT_BYTES_FORMAT_SLASH_X_BYTEA: typing.Final[int] -OPT_BYTES_FORMAT_TO_BINARY: typing.Final[int] -OPT_BYTES_FORMAT_TO_VARBYTE: typing.Final[int] -OPT_BYTES_FORMAT_UNHEX: typing.Final[int] -OPT_CAST_TRY_CAST_SAFE_CAST: typing.Final[int] -OPT_CAST_TRY_CAST_TRY_CAST: typing.Final[int] -OPT_CAST_TRY_CAST_TRY_CAST_WORD: typing.Final[int] -OPT_CAST_TRY_CAST_UNSUPPORTED: typing.Final[int] -OPT_COLLATE_SPELLING_FUNCTION_STRING: typing.Final[int] -OPT_COLLATE_SPELLING_INFIX_IDENT: typing.Final[int] -OPT_COLLATE_SPELLING_UNSUPPORTED: typing.Final[int] -OPT_DATE_FORMAT_KEYWORD: typing.Final[int] -OPT_DATE_FORMAT_QUOTED: typing.Final[int] -OPT_FUNCTION_FILTER_FILTER: typing.Final[int] -OPT_FUNCTION_FILTER_FILTER_WHERE: typing.Final[int] -OPT_FUNCTION_FILTER_INSIDE_WHERE: typing.Final[int] -OPT_FUNCTION_FILTER_UNSUPPORTED: typing.Final[int] -OPT_IDENTIFIER_ESCAPE_BACKSLASH: typing.Final[int] -OPT_IDENTIFIER_ESCAPE_DOUBLE: typing.Final[int] -OPT_IDENTIFIER_FORMAT_BACKTICK: typing.Final[int] -OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE: typing.Final[int] -OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET: typing.Final[int] -OPT_ILIKE_EXPRESSION_STANDARD: typing.Final[int] -OPT_ILIKE_EXPRESSION_USE_LIKE: typing.Final[int] -OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER: typing.Final[int] -OPT_INET_FORMAT_CAST: typing.Final[int] -OPT_INET_FORMAT_QUOTED: typing.Final[int] -OPT_INET_FORMAT_UNSUPPORTED: typing.Final[int] -OPT_IS_DISTINCT_FROM_SPELLING_NULL_SAFE_EQUAL: typing.Final[int] -OPT_IS_DISTINCT_FROM_SPELLING_STANDARD: typing.Final[int] -OPT_IS_DISTINCT_FROM_SPELLING_UNSUPPORTED: typing.Final[int] -OPT_JSON_FORMAT_JSON_KEYWORD: typing.Final[int] -OPT_JSON_FORMAT_PARSE_JSON: typing.Final[int] -OPT_JSON_FORMAT_QUOTED: typing.Final[int] -OPT_LIKE_ANY_ARRAY_CONSTRUCTOR: typing.Final[int] -OPT_LIKE_ANY_OR_EXPANSION: typing.Final[int] -OPT_LIKE_ANY_PATTERN_LIST: typing.Final[int] -OPT_LIKE_ESCAPE_ANY: typing.Final[int] -OPT_LIKE_ESCAPE_SCALAR: typing.Final[int] -OPT_MACADDR_FORMAT_CAST: typing.Final[int] -OPT_MACADDR_FORMAT_QUOTED: typing.Final[int] -OPT_MACADDR_FORMAT_UNSUPPORTED: typing.Final[int] -OPT_MODULO_SPELLING_FUNCTION: typing.Final[int] -OPT_MODULO_SPELLING_INFIX: typing.Final[int] -OPT_MODULO_SPELLING_PERCENT: typing.Final[int] -OPT_NULL_TREATMENT_INSIDE_PARENS: typing.Final[int] -OPT_NULL_TREATMENT_POSTFIX: typing.Final[int] -OPT_NULL_TREATMENT_UNSUPPORTED: typing.Final[int] -OPT_PLACEHOLDER_SYNTAX_NUMBERED: typing.Final[int] -OPT_PLACEHOLDER_SYNTAX_SIMPLE: typing.Final[int] -OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS: typing.Final[int] -OPT_RLIKE_EXPRESSION_REGEXP_LIKE: typing.Final[int] -OPT_RLIKE_EXPRESSION_REGEXP_MATCHES: typing.Final[int] -OPT_RLIKE_EXPRESSION_STANDARD: typing.Final[int] -OPT_RLIKE_EXPRESSION_TILDE: typing.Final[int] -OPT_RLIKE_EXPRESSION_UNSUPPORTED: typing.Final[int] -OPT_SCHEMA_AUTHORIZATION_STYLE_OPTIONAL: typing.Final[int] -OPT_SCHEMA_AUTHORIZATION_STYLE_UNSUPPORTED: typing.Final[int] -OPT_SCHEMA_FEATURES_EMBEDDED_DDL: typing.Final[int] -OPT_SCHEMA_FEATURES_IF_NOT_EXISTS: typing.Final[int] -OPT_SCHEMA_FEATURES_IS_DATABASE: typing.Final[int] -OPT_SCHEMA_FEATURES_DROP_CASCADE: typing.Final[int] -OPT_SCHEMA_FEATURES_DROP_RESTRICT: typing.Final[int] -OPT_SCHEMA_FEATURES_UNSUPPORTED: typing.Final[int] -OPT_SCHEMA_LOCATION_STYLE_EXTERNAL_VOLUME: typing.Final[int] -OPT_SCHEMA_LOCATION_STYLE_LOCATION_KEYWORD: typing.Final[int] -OPT_SCHEMA_LOCATION_STYLE_MANAGED_LOCATION_KEYWORD: typing.Final[int] -OPT_SCHEMA_LOCATION_STYLE_UNSUPPORTED: typing.Final[int] -OPT_STRING_CONCAT_CONCAT_FUNCTION: typing.Final[int] -OPT_STRING_CONCAT_PIPES: typing.Final[int] -OPT_STRING_CONCAT_PLUS: typing.Final[int] -OPT_STRING_ESCAPE_BACKSLASH_HEX: typing.Final[int] -OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO: typing.Final[int] -OPT_STRING_ESCAPE_BACKSLASH_MYSQL: typing.Final[int] -OPT_STRING_ESCAPE_BACKSLASH_UNICODE: typing.Final[int] -OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE: typing.Final[int] -OPT_STRING_ESCAPE_QUOTE: typing.Final[int] -OPT_STRING_FORMAT_PREFIX_BIG_E: typing.Final[int] -OPT_STRING_FORMAT_PREFIX_BIG_N: typing.Final[int] -OPT_STRING_FORMAT_SINGLE_QUOTE: typing.Final[int] -OPT_TIME_FORMAT_KEYWORD: typing.Final[int] -OPT_TIME_FORMAT_QUOTED: typing.Final[int] -OPT_TIMESTAMP_FORMAT_QUOTED: typing.Final[int] -OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ: typing.Final[int] -OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE: typing.Final[int] -OPT_TRIM_SPELLING_FUNCTION: typing.Final[int] -OPT_TRIM_SPELLING_SPACES_ONLY: typing.Final[int] -OPT_TRIM_SPELLING_SQL_FROM: typing.Final[int] -OPT_TUPLE_ARITY_EMPTY: typing.Final[int] -OPT_TUPLE_ARITY_ONE: typing.Final[int] -OPT_TUPLE_SYNTAX_PARENTHESES: typing.Final[int] -OPT_TUPLE_SYNTAX_ROW: typing.Final[int] -OPT_TUPLE_SYNTAX_STRUCT: typing.Final[int] -OPT_TUPLE_SYNTAX_UNSUPPORTED: typing.Final[int] -OPT_UUID_FORMAT_CAST_UUID: typing.Final[int] -OPT_UUID_FORMAT_QUOTED: typing.Final[int] -OPT_UUID_FORMAT_UUID_KEYWORD: typing.Final[int] - -class Dialect: - """Quoting, literal spelling, and which SQL constructs are legal.""" - - def __new__( - cls, - *, - placeholder_character: str = "?", - placeholder_syntax: int = ..., - like_escape: int = ..., - like_any: int = ..., - ilike_expression: int = ..., - rlike_expression: int = ..., - supports_similar_to: bool = False, - null_treatment: int = ..., - function_filter: int = ..., - bitwise_operations: int = ..., - bitwise_call_syntax: int = ..., - bitwise_xor_spelling: int = ..., - identifier_format: int = ..., - identifier_escape: int = ..., - string_format: int = ..., - string_escape: int = ..., - string_concat: int = ..., - cast_try_cast: int = ..., - cast_double_colon: bool = False, - tuple_syntax: int = ..., - tuple_arity: int = ..., - date_format: int = ..., - time_format: int = ..., - timestamp_format: int = ..., - bool_format: int = ..., - bit_format: int = ..., - bytes_format: int = ..., - inet_format: int = ..., - macaddr_format: int = ..., - uuid_format: int = ..., - json_format: int = ..., - array_format: int = ..., - modulo_spelling: int = ..., - is_distinct_from_spelling: int = ..., - trim_spelling: int = ..., - collate_spelling: int = ..., - schema_features: int = ..., - schema_authorization_style: int = ..., - schema_location_style: int = ..., - ) -> typing.Self: - """ - Build a dialect from flags. - - An exclusive spelling is one `OPT_*` constant. A mask is several - `OPT_*` constants combined with `|`. - """ - - @property - def array_format(self) -> int: ... - @property - def bit_format(self) -> int: ... - @property - def bitwise_call_syntax(self) -> int: ... - @property - def bitwise_operations(self) -> int: ... - @property - def bitwise_xor_spelling(self) -> int: ... - @property - def bool_format(self) -> int: ... - @property - def bytes_format(self) -> int: ... - @property - def cast_double_colon(self) -> bool: ... - @property - def cast_try_cast(self) -> int: ... - @property - def collate_spelling(self) -> int: ... +@typing.final +class AggregateConfig: + def __new__(cls, *, null_treatment: int = ..., function_filter: int = ...) -> typing.Self: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... @property - def date_format(self) -> int: ... + def null_treatment(self) -> int: ... @property def function_filter(self) -> int: ... - @property - def identifier_escape(self) -> int: ... - @property - def identifier_format(self) -> int: ... - @property - def ilike_expression(self) -> int: ... - @property - def inet_format(self) -> int: ... - @property - def is_distinct_from_spelling(self) -> int: ... - @property - def json_format(self) -> int: ... - @property - def like_any(self) -> int: ... - @property - def like_escape(self) -> int: ... - @property - def macaddr_format(self) -> int: ... - @property - def modulo_spelling(self) -> int: ... + +@typing.final +class BitwiseConfig: + def __new__(cls, *, operations: int = 0, syntax: int = ..., xor: int = ...) -> typing.Self: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... @property def null_treatment(self) -> int: ... @property - def placeholder_character(self) -> str: ... - @property - def placeholder_syntax(self) -> int: ... - @property - def rlike_expression(self) -> int: ... - @property - def string_concat(self) -> int: ... - @property - def string_escape(self) -> int: ... - @property - def string_format(self) -> int: ... - @property - def supports_similar_to(self) -> bool: ... - @property - def time_format(self) -> int: ... - @property - def timestamp_format(self) -> int: ... - @property - def trim_spelling(self) -> int: ... - @property - def tuple_arity(self) -> int: ... - @property - def tuple_syntax(self) -> int: ... - @property - def uuid_format(self) -> int: ... - @property - def schema_features(self) -> int: ... - @property - def schema_authorization_style(self) -> int: ... - @property - def schema_location_style(self) -> int: ... - def write_boolean(self, value: bool) -> str: - """ - Write `value` as this dialect's boolean literal. - - Args: - value: Python boolean. - - Returns: - `TRUE`, `FALSE`, `1`, or `0`. - """ - - def write_identifier(self, value: str) -> str: - """Quote `value` with this dialect's identifier delimiters. - - Args: - value: Raw identifier body. - - Returns: - The delimited identifier. - """ - - def write_string_literal(self, value: str) -> str: - """ - Escape `value` and wrap it as a complete string literal. - - Args: - value: Text to embed. A NUL character is rejected. - - Returns: - The SQL string literal. - - Raises: - ValueError: `value` contains a NUL character. - """ - - def write_bits(self, value: str) -> str: - """ - Write a bit string of `0` and `1` characters. - - Args: - value: Bit characters. Empty is legal. Any other character is not. - - Returns: - The dialect's bit literal. - - Raises: - ValueError: `value` contains a character other than `0` or `1`. - NotImplementedError: This dialect has no bit-string literal. - """ - - def write_bytes(self, value: bytes) -> str: - """Write `value` as this dialect's blob or bytea literal. - - Args: - value: Raw bytes. - - Returns: - The SQL literal. - - Raises: - NotImplementedError: This dialect has no byte literal of the - configured form. - """ - - def __eq__(self, other: object) -> bool: ... - def __hash__(self) -> int: ... - -BigQuery: typing.Final[Dialect] = ... -"""BigQuery standard SQL.""" - -Databricks: typing.Final[Dialect] = ... -"""Databricks SQL.""" - -DuckDb: typing.Final[Dialect] = ... -"""DuckDB SQL.""" - -Hive: typing.Final[Dialect] = ... -"""HiveQL.""" - -MsSql: typing.Final[Dialect] = ... -"""Microsoft SQL Server.""" - -MySql: typing.Final[Dialect] = ... -"""MySQL.""" - -Oracle: typing.Final[Dialect] = ... -"""Oracle SQL.""" - -PostgreSql: typing.Final[Dialect] = ... -"""PostgreSQL.""" - -Redshift: typing.Final[Dialect] = ... -"""Amazon Redshift.""" - -Snowflake: typing.Final[Dialect] = ... -"""Snowflake SQL.""" - -Spark: typing.Final[Dialect] = ... -"""Spark SQL.""" - -Sqlite: typing.Final[Dialect] = ... -"""SQLite.""" - -Teradata: typing.Final[Dialect] = ... -"""Teradata SQL.""" + def function_filter(self) -> int: ... diff --git a/src/ddl/schema.rs b/src/ddl/schema.rs index 6f1f719..7da02c3 100644 --- a/src/ddl/schema.rs +++ b/src/ddl/schema.rs @@ -1,4 +1,4 @@ -use crate::dialect::config; +use crate::dialect; use crate::expr::enum_::Ident; use crate::expr::extractors::IdentExtractor; use crate::internal::alias; @@ -24,8 +24,8 @@ pub struct PyCreateSchema { } impl sql::WriteSql for PyCreateSchema { - fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> sql::PrepareResult<()> { - if dialect.0.schema_features.contains(config::SchemaFeatures::UNSUPPORTED) { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> sql::PrepareResult<()> { + if !dialect.schema.features.contains(dialect::SchemaFeatures::SUPPORTED) { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "CREATE SCHEMA statement is not supported by this dialect" @@ -34,25 +34,20 @@ impl sql::WriteSql for PyCreateSchema { } // CREATE {SCHEMA | DATABASE} - if dialect.0.schema_features.contains(config::SchemaFeatures::IS_DATABASE) { + if dialect.schema.features.contains(dialect::SchemaFeatures::IS_DATABASE) { out.push_str("CREATE DATABASE "); } else { out.push_str("CREATE SCHEMA "); } // ... [IF NOT EXISTS] - if self.if_not_exists - && dialect - .0 - .schema_features - .contains(config::SchemaFeatures::IF_NOT_EXISTS) - { + if self.if_not_exists && dialect.schema.features.contains(dialect::SchemaFeatures::IF_NOT_EXISTS) { out.push_str("IF NOT EXISTS "); } // ... [] [AUTHORIZATION ] - match dialect.0.schema_authorization_style { - config::SchemaAuthorizationStyle::Unsupported => { + match dialect.schema.authorization { + dialect::SchemaAuthorizationStyle::Unsupported => { if self.authorization.is_some() { return Err(rapidquery_macros::pyerr!( PyRuntimeError, @@ -63,39 +58,31 @@ impl sql::WriteSql for PyCreateSchema { self.name.write_sql(dialect, out)?; } - config::SchemaAuthorizationStyle::Optional => { + dialect::SchemaAuthorizationStyle::Optional => { self.name.write_sql(dialect, out)?; if let Some(x) = &self.authorization { out.push_str(" AUTHORIZATION "); - dialect.0.write_identifier(out, x)?; + dialect.write_identifier(out.sql_mut(), x); } } } if let Some(x) = &self.location { // ... {LOCATION | MANAGED LOCATION | EXTERNAL_VOLUME =} - match dialect.0.schema_location_style { - config::SchemaLocationStyle::Unsupported => { - return Err(rapidquery_macros::pyerr!( - PyRuntimeError, - "location is not supported in CREATE SCHEMA by this dialect" - ) - .into()); - } - config::SchemaLocationStyle::LocationKeyword => { - out.push_str(" LOCATION "); - dialect.0.write_string_literal(out, x)?; - } - config::SchemaLocationStyle::ManagedLocationKeyword => { - out.push_str(" MANAGED LOCATION "); - dialect.0.write_string_literal(out, x)?; - } - config::SchemaLocationStyle::ExternalVolume => { - out.push_str(" EXTERNAL_VOLUME = "); - dialect.0.write_string_literal(out, x)?; - } - } + let Some(tmpl) = &dialect.schema.location else { + return Err(rapidquery_macros::pyerr!( + PyRuntimeError, + "location is not supported in CREATE SCHEMA by this dialect" + ) + .into()); + }; + + tmpl.write_to_result( + out, + // no special key + |_, out| dialect.string.write_string_literal(out.sql_mut(), x.as_str()), + )?; } Ok(()) @@ -185,7 +172,7 @@ pub struct PyDropSchema { impl sql::WriteSql for PyDropSchema { fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> sql::PrepareResult<()> { - if dialect.0.schema_features.contains(config::SchemaFeatures::UNSUPPORTED) { + if !dialect.schema.features.contains(dialect::SchemaFeatures::SUPPORTED) { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "DROP SCHEMA statement is not supported by this dialect" @@ -194,19 +181,14 @@ impl sql::WriteSql for PyDropSchema { } // DROP {SCHEMA | DATABASE} - if dialect.0.schema_features.contains(config::SchemaFeatures::IS_DATABASE) { + if dialect.schema.features.contains(dialect::SchemaFeatures::IS_DATABASE) { out.push_str("DROP DATABASE "); } else { out.push_str("DROP SCHEMA "); } // ... [IF EXISTS] - if self.if_exists - && dialect - .0 - .schema_features - .contains(config::SchemaFeatures::IF_NOT_EXISTS) - { + if self.if_exists && dialect.schema.features.contains(dialect::SchemaFeatures::IF_NOT_EXISTS) { out.push_str("IF EXISTS "); } @@ -217,7 +199,7 @@ impl sql::WriteSql for PyDropSchema { match self.behavior { None => {} Some(DropSchemaBehavior::Cascade) => { - if !dialect.0.schema_features.contains(config::SchemaFeatures::DROP_CASCADE) { + if !dialect.schema.features.contains(dialect::SchemaFeatures::DROP_CASCADE) { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "This dialect does not support CASCADE keyword in DROP SCHEMA statement" @@ -228,11 +210,7 @@ impl sql::WriteSql for PyDropSchema { out.push_str(" CASCADE"); } Some(DropSchemaBehavior::Restrict) => { - if !dialect - .0 - .schema_features - .contains(config::SchemaFeatures::DROP_RESTRICT) - { + if !dialect.schema.features.contains(dialect::SchemaFeatures::DROP_RESTRICT) { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "This dialect does not support RESTRICT keyword in DROP SCHEMA statement" diff --git a/src/dialect/base.rs b/src/dialect/base.rs index 55e509f..01e948b 100644 --- a/src/dialect/base.rs +++ b/src/dialect/base.rs @@ -1,198 +1,475 @@ +use crate::internal::template; use crate::internal::utils; -use super::config::*; - -/// SQL dialect: quoting, literals, and which syntax is legal. -/// -/// Use a builtin such as `PostgreSql`, or build one from `OPT_*` flags. -#[rapidquery_macros::pyclass("rapidquery.dialect.Dialect", eq, hash)] +#[rapidquery_macros::pyclass("rapidquery.dialect.LikeConfig", from_py_object, eq, hash)] +#[rapidquery_macros::define_options] #[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct PyDialect(pub DialectConfig); - -impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyDialect { - type Error = pyo3::PyErr; - - fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { - unsafe { - // SAFETY: `obj` is a borrowed `&Bound`, so `as_ptr()` is a - // live `PyObject*`. - if crate::typeref::check_exact(obj.as_ptr(), crate::typeref::DIALECT_TYPE) { - let cast = obj.cast_unchecked::(); - return Ok(Self(cast.get().0)); - } +pub struct LikeConfig { + /// Where an inlined `LIKE` `ESCAPE` clause is legal. + /// + /// Empty writes no `ESCAPE` and rejects a set escape character. + /// `SCALAR | ANY` allows both a scalar pattern and native `LIKE ANY`. + /// + /// - SCALAR: Scalar `LIKE` and an `OR` expansion. + /// - ANY: Native `LIKE ANY (p, …) ESCAPE`. + #[define(bits { SCALAR, ANY })] + pub escape: LikeEscape, + + /// How `LIKE ANY` / `ILIKE ANY` is rendered. + /// + /// - OrExpansion: `(x LIKE p1 OR x LIKE p2)`. Useful when it isn't supported natively by the dialect. + /// - ArrayConstructor: `LIKE ANY (ARRAY[p, …])`. + /// - PatternList: `LIKE ANY (p, …)`. + #[define(enum { #[default] OrExpansion, ArrayConstructor, PatternList })] + pub any: LikeAny, + + /// Does the dialect support `ILIKE`, or should we fall back? + /// + /// - Standard: Native `ILIKE`. + /// - UseLikeLower: Use `LIKE` with `LOWER()`. + /// - UseLike: Fall back to case-sensitive `LIKE`. + #[define(enum { #[default] Unsupported, Standard, UseLikeLower })] + pub ilike: ILikeStrategy, + + /// How regex (`RLIKE` / `~` / `REGEXP_LIKE`, …) is spelled. + /// + /// - Unsupported: Is not supported. + /// - Standard: `expr [NOT] RLIKE pattern`. + /// - Tilde: `expr ~ pattern` / `expr !~ pattern`. + /// - RegexpLike: `REGEXP_LIKE(expr, pattern)`. + /// - RegexpContains: `REGEXP_CONTAINS(expr, pattern)`. + /// - RegexpMatches: `regexp_matches(expr, pattern)`. + #[define(enum { #[default] Unsupported, Standard, Tilde, RegexpLike, RegexpContains, RegexpMatches })] + pub regex: RegexSpelling, + + /// Whether the dialect supports the `SIMILAR TO` predicate. + pub supports_similar_to: bool, +} - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected Dialect, got {}", - utils::get_type_name(obj.py(), obj.as_ptr()) - ) - } +rapidquery_macros::pyo3_constructor! { + LikeConfig => export_like_config_options { + escape: bits LikeEscape { SCALAR, ANY } = empty, + any: enum LikeAny { OrExpansion, ArrayConstructor, PatternList } = OrExpansion, + ilike: enum ILikeStrategy { Unsupported, Standard, UseLikeLower } = Unsupported, + regex: enum RegexSpelling { + Unsupported, Standard, Tilde, RegexpLike, RegexpContains, RegexpMatches + } = Unsupported, + supports_similar_to: bool = false, } } -// FIXME: Set dialect default values as disabled/unsupported when possible, -// otherwise set a traditional one. So developer can predict the values -// without needing to specify all arguments. -rapidquery_macros::pydialect_constructor!( - placeholder_character: char = '?', - - #[enum_(Simple, Numbered)] - placeholder_syntax: PlaceholderSyntax = PlaceholderSyntax::default(), - - #[bit(SCALAR, ANY)] - like_escape: LikeEscape = LikeEscape::SCALAR.bits(), - - #[enum_(OrExpansion, ArrayConstructor, PatternList)] - like_any: LikeAny = LikeAny::default(), - - #[enum_(Standard, UseLikeLower, UseLike)] - ilike_expression: ILikeExpression = ILikeExpression::default(), - - #[enum_(Unsupported, Standard, Tilde, RegexpLike, RegexpContains, RegexpMatches)] - rlike_expression: RLikeExpression = RLikeExpression::default(), - - supports_similar_to: bool = false, - - #[enum_(Unsupported, InsideParens, Postfix)] - null_treatment: NullTreatment = NullTreatment::default(), +#[rapidquery_macros::pyclass("rapidquery.dialect.BitwiseConfig", from_py_object, eq, hash)] +#[rapidquery_macros::define_options] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BitwiseConfig { + /// Which bitwise operators a dialect can render. + /// + /// A missing flag rejects that operator. AND, OR, NOT, and the shifts share + /// one spelling ([`BitwiseSyntax`]). XOR uses [`BitwiseXorSpelling`]. + #[define(bits { AND, OR, NOT, SHL, SHR })] + pub operations: BitwiseOperations, + + /// Spelling shared by `&`, `|`, `~`, `<<`, and `>>`. + /// + /// - Infix: `& | ~ << >>`. + /// - BitFunction: `BITAND` / `BITOR` / `BITNOT` / `BITSHIFTLEFT` / `BITSHIFTRIGHT`. + /// - ShiftFunction: `BITAND` / `BITOR` / `BITNOT` / `SHIFTLEFT` / `SHIFTRIGHT`. + #[define(enum { #[default] Infix, BitFunction, ShiftFunction })] + pub syntax: BitwiseSyntax, + + /// How `^` is spelled when XOR is supported. + /// + /// - Unsupported + /// - Caret: `left ^ right`. + /// - Hash: `left # right`. PostgreSQL `^` is exponent. + /// - Function: `xor(left, right)`. + /// - BitFunction: `BITXOR(left, right)`. + #[define(enum { #[default] Unsupported, Caret, Hash, Function, BitFunction })] + pub xor: BitwiseXorSpelling, +} - #[enum_(Unsupported, Filter, FilterWhere, InsideWhere)] - function_filter: FunctionFilter = FunctionFilter::default(), +rapidquery_macros::pyo3_constructor! { + BitwiseConfig => export_bitwise_config_options { + operations: bits BitwiseOperations { AND, OR, NOT, SHL, SHR } = empty, + syntax: enum BitwiseSyntax { Infix, BitFunction, ShiftFunction } = Infix, + xor: enum BitwiseXorSpelling { Unsupported, Caret, Hash, Function, BitFunction } = Unsupported, + } +} - #[bit(AND, OR, NOT, SHL, SHR)] - bitwise_operations: BitwiseOperations = BitwiseOperations::AND - .union(BitwiseOperations::OR) - .union(BitwiseOperations::NOT) - .union(BitwiseOperations::SHL) - .union(BitwiseOperations::SHR) - .bits(), +#[rapidquery_macros::pyclass("rapidquery.dialect.StringConfig", from_py_object, eq, hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct StringConfig { + /// String template e.g. `E'{quote}'` + /// + /// Template key specify escaping method: + /// - `quote`: Escape `'` → `''`. Other characters are unchanged. + /// - `hex`: Escape `\\`, `\'`, `\n`, `\r`, `\t`; other C0/DEL as `\xNN`. + /// - `mysql`: (MySQL-specific) Like `hex`, but also `\"` and `\Z` for 0x1A; tab and other controls pass through. + /// - `unicode`: Other C0/DEL as `\uNNNN`. No `\"`. + /// - `hex_zero`: Other C0/DEL as `\xNN`. + /// - `unicode_quote`: `unicode` plus `\"`. + /// + /// Prefer using `StringConfig::write_string_literal` instead of directly using this template. + pub template: template::Template, + + /// How binary operation string concatenation is spelled e.g. `{left} || {right}` + /// + /// Template keys are `left` and `right`. + pub concat: template::Template, +} - #[enum_(Infix, BitFunction, ShiftFunction)] - bitwise_call_syntax: BitwiseCallSyntax = BitwiseCallSyntax::default(), +rapidquery_macros::pyo3_constructor! { + StringConfig => export_string_config_options { + template: template(quote?, hex?, mysql?, unicode?, hex_zero?, unicode_quote?; 1) = "'{quote}'", + concat: template(left, right; 2) = "{left} || {right}", + } +} - #[enum_(Unsupported, Caret, Hash, Function, BitFunction)] - bitwise_xor_spelling: BitwiseXorSpelling = BitwiseXorSpelling::default(), +#[rapidquery_macros::pyclass("rapidquery.dialect.CastConfig", from_py_object, eq, hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CastConfig { + /// How `TRY_CAST` is spelled e.g. `TRY_CAST({expr} AS {type})`. + /// + /// Template keys are `expr` and `type`. + pub try_cast: Option, + + /// Whether the dialect supports the `expr::type` cast syntax. + pub supports_double_colon: bool, +} - #[enum_(DoubleQuote, Backtick, SquareBracket)] - identifier_format: IdentifierFormat = IdentifierFormat::default(), +rapidquery_macros::pyo3_constructor! { + CastConfig => export_cast_config_options { + // NOTE: the old default was `"'{quote}'"` (copy-paste from StringConfig). + try_cast: Option = "TRY_CAST({expr} AS {type})", + supports_double_colon: bool = false, + } +} - #[enum_(Double, Backslash)] - identifier_escape: IdentifierEscape = IdentifierEscape::default(), +#[rapidquery_macros::pyclass("rapidquery.dialect.TupleConfig", from_py_object, eq, hash)] +#[rapidquery_macros::define_options] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TupleConfig { + /// How a tuple / row value is spelled. + /// + /// Which arities are legal is [`TupleArity`], not another syntax variant. + /// + /// - Unsupported: Is not supported. + /// - Row: `ROW(...)`. + /// - Struct: `struct(...)`. + /// - Parentheses: `(a, b, ...)`. + #[define(enum { #[default] Unsupported, Row, Struct, Parentheses })] + pub syntax: TupleSyntax, + + /// Arities below 2 that [`TupleSyntax`] accepts. + /// + /// Arity 2 and above is legal whenever the syntax is not [`TupleSyntax::Unsupported`]. + /// Empty means a tuple needs at least two elements. + /// + /// - EMPTY: Arity 0. + /// - ONE: Arity 1. + #[define(bits { EMPTY, ONE })] + pub arity: TupleArity, +} - #[enum_(SingleQuote, PrefixBigE, PrefixBigN)] - string_format: StringFormat = StringFormat::default(), +rapidquery_macros::pyo3_constructor! { + TupleConfig => export_tuple_config_options { + syntax: enum TupleSyntax { Unsupported, Row, Struct, Parentheses } = Unsupported, + arity: bits TupleArity { EMPTY, ONE } = empty, + } +} - #[enum_(Quote, BackslashHex, BackslashMysql, BackslashUnicode, BackslashHexZero, BackslashUnicodeQuote)] - string_escape: StringEscape = StringEscape::default(), +#[rapidquery_macros::pyclass("rapidquery.dialect.TemporalConfig", from_py_object, eq, hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TemporalConfig { + /// How a date literal is spelled e.g. `DATE '{}'` + pub date: template::Template, - #[enum_(Pipes, Plus, ConcatFunction)] - string_concat: StringConcat = StringConcat::default(), + /// How a time literal is spelled e.g. `TIME '{}'` + pub time: template::Template, - #[enum_(Unsupported, TryCast, TryCastWord, SafeCast)] - cast_try_cast: TryCast = TryCast::default(), + /// How a *naive* datetime/timestamp literal is spelled e.g. `TIMESTAMP '{}'` + pub datetime_naive: template::Template, - cast_double_colon: bool = false, + /// How a *aware* datetime/timestamp literal is spelled e.g. `TIMESTAMPTZ '{}'` + pub datetime_aware: template::Template, +} - #[enum_(Unsupported, Row, Struct, Parentheses)] - tuple_syntax: TupleSyntax = TupleSyntax::default(), +rapidquery_macros::pyo3_constructor! { + TemporalConfig => export_temporal_config_options { + date: template(_; 1) = "{}", + time: template(_; 1) = "{}", + datetime_naive: template(_; 1) = "{}", + datetime_aware: template(_; 1) = "{}", + } +} - #[bit(EMPTY, ONE)] - tuple_arity: TupleArity = TupleArity::EMPTY.union(TupleArity::ONE).bits(), +#[rapidquery_macros::pyclass("rapidquery.dialect.LiteralConfig", from_py_object, eq, hash)] +#[rapidquery_macros::define_options] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LiteralConfig { + /// How a boolean literal is spelled. + /// + /// - Keyword: `TRUE` / `FALSE` + /// - Integer: `1` / `0` + #[define(enum { #[default] Keyword, Integer })] + pub boolean: BoolFormat, + + /// The way dialect accepts literal bit values e.g. `B'{}'` + /// + /// Prefer using `StringConfig::write_bytes` instead of directly using this template. + pub bit: Option, + + /// The way dialect accepts literal bytes values e.g. `X'{hex}'` + /// + /// Template keys are: + /// - `hex`: bytes as hex (e.g. `DEADBEEF`) + /// - `slashx`: each byte as `\xNN` + /// + /// Prefer using `StringConfig::write_bytes` instead of directly using this template. + pub bytes: template::Template, + + /// How an inet literal is spelled e.g. `'{}'::inet` + pub inet: Option, + + /// How a MAC address literal is spelled e.g. `'{}'::macaddr` + pub macaddr: Option, + + /// How a UUID literal is spelled e.g. `'{}'::uuid` + pub uuid: Option, + + /// How a JSON literal is spelled e.g. `JSON {}` (value is escaped and includes quotes itself). + pub json: template::Template, + + /// The way dialect accepts array values e.g. `ARRAY[{}]` + pub array: Option, +} - #[enum_(Quoted, Keyword)] - date_format: DateFormat = DateFormat::default(), +rapidquery_macros::pyo3_constructor! { + LiteralConfig => export_literal_config_options { + boolean: enum BoolFormat { Keyword, Integer } = Keyword, + bit: Option = None, + bytes: template(hex?, slashx?; 1) = "X'{hex}'", + inet: Option = None, + macaddr: Option = None, + uuid: Option = None, + json: template(_; 1) = "{}", + array: Option = None, + } +} - #[enum_(Quoted, Keyword)] - time_format: TimeFormat = TimeFormat::default(), +#[rapidquery_macros::pyclass("rapidquery.dialect.OperatorConfig", from_py_object, eq, hash)] +#[rapidquery_macros::define_options] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct OperatorConfig { + /// How binary operation mod (`%`) is spelled. + /// + /// - Percent: `left % right`. + /// - Function: `MOD(left, right)`. + /// - Infix: `left MOD right`. + #[define(enum { #[default] Percent, Function, Infix })] + pub modulo: ModuloSpelling, + + /// How `IS [NOT] DISTINCT FROM` is spelled. + /// + /// - Unsupported: Is not supported. + /// - Standard: `left IS [NOT] DISTINCT FROM right`. + /// - NullSafeEqual: `<=>` is null-safe equal (`IS NOT DISTINCT FROM`). `IS DISTINCT FROM` is `NOT (left <=> right)`. + #[define(enum { #[default] Unsupported, Standard, NullSafeEqual })] + pub is_distinct_from: IsDistinctFromSpelling, + + /// How `TRIM` is spelled. + /// + /// - SqlFrom: `TRIM([LEADING|TRAILING] [chars FROM] expr)`. + /// - Function: `TRIM` / `LTRIM` / `RTRIM` `(expr[, chars])`. + /// - SpacesOnly: `TRIM` / `LTRIM` / `RTRIM` `(expr)`. A character set raises. + #[define(enum { #[default] SqlFrom, Function, SpacesOnly })] + pub trim: TrimSpelling, + + /// How `COLLATE` is spelled. + /// + /// - Unsupported: Is not supported. + /// - InfixIdent: `expr COLLATE ident`. + /// - FunctionString: `COLLATE(expr, 'body')`. The identifier body is a string literal. + #[define(enum { #[default] Unsupported, InfixIdent, FunctionString })] + pub collate: CollateSpelling, +} - #[enum_(Quoted, Timestamptz, WithTimezone)] - timestamp_format: TimestampFormat = TimestampFormat::default(), +rapidquery_macros::pyo3_constructor! { + OperatorConfig => export_operator_config_options { + modulo: enum ModuloSpelling { Percent, Function, Infix } = Percent, + is_distinct_from: enum IsDistinctFromSpelling { Unsupported, Standard, NullSafeEqual } = Unsupported, + trim: enum TrimSpelling { SqlFrom, Function, SpacesOnly } = SqlFrom, + collate: enum CollateSpelling { Unsupported, InfixIdent, FunctionString } = Unsupported, + } +} - #[enum_(Keyword, Integer)] - bool_format: BoolFormat = BoolFormat::default(), +#[rapidquery_macros::pyclass("rapidquery.dialect.SchemaConfig", from_py_object, eq, hash)] +#[rapidquery_macros::define_options] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SchemaConfig { + /// Supported independent schema features, or is it even supported? + /// + /// - SUPPORTED: Means dialect support schema. + /// - IF_NOT_EXISTS: Is dialect support `IF NOT EXISTS` clause? + /// - EMBEDDED_DDL: Is dialect support embedded DDL statements in `CREATE SCHEMA`? + /// - IS_DATABASE: Schema has same meaning as database in this dialect. + /// - DROP_CASCADE: Supports `CASCADE` keyword in `DROP SCHEMA`. + /// - DROP_RESTRICT: Supports `RESTRICT` keyword in `DROP SCHEMA`. + #[define(bits { SUPPORTED, IF_NOT_EXISTS, EMBEDDED_DDL, IS_DATABASE, DROP_CASCADE, DROP_RESTRICT })] + pub features: SchemaFeatures, + + /// `AUTHORIZATION` clause style. How the schema owner is specified. + /// + /// - Unsupported: Means dialect does not support `AUTHORIZATION` for schema. + /// - Optional: Means `AUTHORIZATION` clause is supported and it's optional. + #[define(enum { #[default] Unsupported, Optional })] + pub authorization: SchemaAuthorizationStyle, + + /// How schema location is expressed e.g. `LOCATION {}` (value is escaped and includes quotes itself). + pub location: Option, + + /// How schema location is expressed e.g. `COMMENT {}` (value is escaped and includes quotes itself). + pub comment: Option, +} - #[enum_(Unsupported, PrefixBigB, PrefixSmallB, PostfixBigB)] - bit_format: BitFormat = BitFormat::default(), +rapidquery_macros::pyo3_constructor! { + SchemaConfig => export_schema_config_options { + features: bits SchemaFeatures { + SUPPORTED, IF_NOT_EXISTS, EMBEDDED_DDL, IS_DATABASE, DROP_CASCADE, DROP_RESTRICT + } = empty, + authorization: enum SchemaAuthorizationStyle { Unsupported, Optional } = Unsupported, + location: Option = None, + comment: Option = None, + } +} - #[enum_(PrefixBigX, PrefixZeroX, SlashXBytea, PrefixBigBSlashX, HexToRaw, Unhex, ToVarbyte, PostfixXbv, ToBinary, SlashXBlob)] - bytes_format: BytesFormat = BytesFormat::default(), +#[rapidquery_macros::pyclass("rapidquery.dialect.AggregateConfig", from_py_object, eq, hash)] +#[rapidquery_macros::define_options] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AggregateConfig { + /// The way dialect supports `{IGNORE|RESPECT} NULLS` expressions in functions. + /// + /// - Unsupported: Is not supported. + /// - InsideParens: e.g. `FIRST_VALUE(x IGNORE NULLS)`. + /// - Postfix: e.g. `FIRST_VALUE(x) IGNORE NULLS`. + #[define(enum { #[default] Unsupported, InsideParens, Postfix })] + pub null_treatment: NullTreatment, + + /// Function `FILTER` syntax. + /// + /// - Unsupported: Is not supported. + /// - Filter: e.g. `FIRST_VALUE(x) FILTER (id > 10)`. + /// - FilterWhere: e.g. `FIRST_VALUE(x) FILTER (WHERE id > 10)`. + /// - InsideWhere: e.g. `FIRST_VALUE(x WHERE id > 10)`. + #[define(enum { #[default] Unsupported, Filter, FilterWhere, InsideWhere })] + pub function_filter: FunctionFilter, +} - #[enum_(Unsupported, Quoted, Cast)] - inet_format: InetFormat = InetFormat::default(), +rapidquery_macros::pyo3_constructor! { + AggregateConfig => export_aggregate_config_options { + null_treatment: enum NullTreatment { Unsupported, InsideParens, Postfix } = Unsupported, + function_filter: enum FunctionFilter { Unsupported, Filter, FilterWhere, InsideWhere } = Unsupported, + } +} - #[enum_(Unsupported, Quoted, Cast)] - macaddr_format: MacAddrFormat = MacAddrFormat::default(), +/// SQL dialect: quoting, literals, and which syntax is legal. +#[rapidquery_macros::pyclass("rapidquery.dialect.Dialect", eq, hash)] +#[rapidquery_macros::define_options] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PyDialect { + /// Positional parameter template e.g. `'?{}'` + pub placeholder: template::Template, - #[enum_(Quoted, UuidKeyword, CastUuid)] - uuid_format: UuidFormat = UuidFormat::default(), + /// `LIKE` / `ILIKE` / regex configuration. + pub like: LikeConfig, - #[enum_(Quoted, JsonKeyword, ParseJson)] - json_format: JsonFormat = JsonFormat::default(), + /// Bitwise operator support and spelling. + pub bitwise: BitwiseConfig, - #[enum_(Unsupported, Constructor, List, Function)] - array_format: ArrayFormat = ArrayFormat::default(), + /// Identifier template e.g. `"{double}"` + /// + /// Template key specify escaping method: + /// - `double`: Escape identifier delimiters with doubling. + /// - `backslash`: Escape identifier delimiters with backslash. + pub identifier: template::Template, - #[enum_(Percent, Function, Infix)] - modulo_spelling: ModuloSpelling = ModuloSpelling::default(), + /// String literal format, escaping, and concatenation. + pub string: StringConfig, - #[enum_(Unsupported, Standard, NullSafeEqual)] - is_distinct_from_spelling: IsDistinctFromSpelling = IsDistinctFromSpelling::default(), + /// Cast syntax (`TRY_CAST`, `::`, etc.). + pub cast: CastConfig, - #[enum_(SqlFrom, Function, SpacesOnly)] - trim_spelling: TrimSpelling = TrimSpelling::default(), + /// Tuple / row-value syntax and arity support. + pub tuple: TupleConfig, - #[enum_(Unsupported, InfixIdent, FunctionString)] - collate_spelling: CollateSpelling = CollateSpelling::default(), + /// Date, time, and timestamp literal spelling. + pub temporal: TemporalConfig, - #[bit(UNSUPPORTED, IF_NOT_EXISTS, EMBEDDED_DDL, IS_DATABASE, DROP_CASCADE, DROP_RESTRICT)] - schema_features: SchemaFeatures = SchemaFeatures::empty().bits(), + /// Other literal formats (bool, bit, bytes, UUID, JSON, …). + pub literal: LiteralConfig, - #[enum_(Unsupported, Optional)] - schema_authorization_style: SchemaAuthorizationStyle = SchemaAuthorizationStyle::default(), + /// Miscellaneous operator spellings. + pub operator: OperatorConfig, - #[enum_(Unsupported, LocationKeyword, ManagedLocationKeyword, ExternalVolume)] - schema_location_style: SchemaLocationStyle = SchemaLocationStyle::default(), -); + /// Aggregate syntax + pub aggregate: AggregateConfig, -#[pyo3::pymethods] -impl PyDialect { - /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). - fn write_boolean(&self, value: bool) -> pyo3::PyResult { - let mut buf = String::new(); - self.0.write_boolean(&mut buf, value)?; - Ok(buf) - } + /// Schema / DDL feature flags and clause styles. + pub schema: SchemaConfig, +} - /// Escape `ident` and wrap it in this dialect’s identifier delimiters. - fn write_identifier(&self, value: &str) -> pyo3::PyResult { - let mut buf = String::new(); - let _ = self.0.write_identifier(&mut buf, value); - Ok(buf) - } +// NOTE: using `crate::typeref::check_exact` has better performance than PyO3 default `FromPyObject`. +impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyDialect { + type Error = pyo3::PyErr; - /// Escape `value` and wrap it as a complete string literal. - fn write_string_literal(&self, value: &str) -> pyo3::PyResult { - let mut buf = String::new(); - self.0.write_string_literal(&mut buf, value)?; - Ok(buf) - } + fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { + unsafe { + // SAFETY: `obj` is a borrowed `&Bound`, so `as_ptr()` is a + // live `PyObject*`. + if crate::typeref::check_exact(obj.as_ptr(), crate::typeref::DIALECT_TYPE) { + let cast = obj.cast_unchecked::(); + return Ok(cast.get().clone()); + } - /// Write a bit-string ('0'/'1' characters) as a native literal. - fn write_bits(&self, value: &str) -> pyo3::PyResult { - if value.chars().any(|c| c != '0' && c != '1') { - return rapidquery_macros::pyerr!(Err(PyValueError), "invalid input: {}", value); + rapidquery_macros::pyerr!( + Err(PyTypeError), + "expected Dialect, got {}", + utils::get_type_name(obj.py(), obj.as_ptr()) + ) } - - let mut buf = String::new(); - self.0.write_bits(&mut buf, value)?; - Ok(buf) } +} - /// Write bytes as a native blob/bytea/varbyte literal. - fn write_bytes(&self, value: &[u8]) -> pyo3::PyResult { - let mut buf = String::new(); - self.0.write_bytes(&mut buf, value)?; - Ok(buf) +rapidquery_macros::pyo3_constructor! { + PyDialect => export_pydialect_options { + placeholder: template(_; 1) = "?{}", + like: class LikeConfig = LikeConfig::default(), + bitwise: class BitwiseConfig = BitwiseConfig::default(), + identifier: template(_; 1) = "\"{}\"", + string: class StringConfig = StringConfig::default(), + cast: class CastConfig = CastConfig::default(), + tuple: class TupleConfig = TupleConfig::default(), + temporal: class TemporalConfig = TemporalConfig::default(), + literal: class LiteralConfig = LiteralConfig::default(), + operator: class OperatorConfig = OperatorConfig::default(), + aggregate: class AggregateConfig = AggregateConfig::default(), + schema: class SchemaConfig = SchemaConfig::default(), } } + +#[cold] +pub(super) fn export_options(module: &::pyo3::Bound<'_, ::pyo3::types::PyModule>) -> pyo3::PyResult<()> { + export_aggregate_config_options(module)?; + export_bitwise_config_options(module)?; + export_cast_config_options(module)?; + export_like_config_options(module)?; + export_literal_config_options(module)?; + export_operator_config_options(module)?; + export_pydialect_options(module)?; + export_schema_config_options(module)?; + export_string_config_options(module)?; + export_temporal_config_options(module)?; + export_tuple_config_options(module)?; + + Ok(()) +} diff --git a/src/dialect/escape.rs b/src/dialect/escape.rs new file mode 100644 index 0000000..443eeb0 --- /dev/null +++ b/src/dialect/escape.rs @@ -0,0 +1,283 @@ +use crate::internal::template; + +use super::base; +use super::errors; + +impl base::LiteralConfig { + /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). + #[inline] + pub fn write_boolean(&self, writer: &mut String, value: bool) { + match (self.boolean, value) { + (base::BoolFormat::Integer, false) => writer.push('0'), + (base::BoolFormat::Integer, true) => writer.push('1'), + (base::BoolFormat::Keyword, false) => writer.push_str("FALSE"), + (base::BoolFormat::Keyword, true) => writer.push_str("TRUE"), + } + } + + /// Write a bit-string (`'0'`/`'1'` characters) as a native literal. + /// + /// Native formats embed `bits` as-is. Callers must pass only `'0'` and `'1'`; + /// that is checked with `debug_assert!`. + #[inline] + pub fn write_bits(&self, writer: &mut String, bits: &str) -> Result<(), errors::FormatError> { + debug_assert!( + bits.chars().all(|c| c == '0' || c == '1'), + "write_bits: invalid input: {bits}" + ); + + if let Some(template) = &self.bit { + template.write_to(writer, |_, out| out.push_str(bits)); + Ok(()) + } else { + Err(errors::FormatError::Unsupported("BitSerializer")) + } + } + + /// Write `bytes` as a native blob/bytea/varbyte literal. + #[inline] + pub fn write_bytes(&self, writer: &mut String, bytes: &[u8]) { + self.bytes.write_to( + writer, + // hex: bytes as hex (e.g. DEADBEEF) + // slashx: each byte as \xNN + |key, out| match key { + "hex" => bytes_to_hex(out, bytes), + "slashx" => write_bytes_slash_x(out, bytes), + _ => unreachable!("invalid key - write_bytes"), + }, + ); + } +} + +impl base::StringConfig { + /// Escape `raw` and wrap it as a complete string literal. + #[inline] + pub fn write_string_literal(&self, writer: &mut String, raw: &str) -> Result<(), errors::FormatError> { + if raw.contains('\0') { + return Err(errors::FormatError::NulInString); + } + + self.template.write_to( + writer, + // quote: Escape ' → ''. Other characters are unchanged. + // hex: Escape \\, \', \n, \r, \t; other C0/DEL as \xNN. + // mysql: (MySQL-specific) Like hex, but also \" and \Z for 0x1A; tab and other controls pass through. + // unicode: Other C0/DEL as \uNNNN. No \". + // hex_zero: Other C0/DEL as \xNN. + // unicode_quote: unicode plus \". + |key, out| match key { + "quote" => template::replace_to(raw, '\'', "''", out), + "hex" => write_backslash_hex(out, raw), + "mysql" => write_backslash_mysql(out, raw), + "unicode" => write_backslash_unicode(out, raw, false), + "hex_zero" => write_backslash_hex_zero(out, raw), + "unicode_quote" => write_backslash_unicode(out, raw, true), + _ => unreachable!("invalid key - write_string_literal"), + }, + ); + Ok(()) + } +} + +impl base::PyDialect { + /// Escape `ident` and wrap it in this dialect's identifier delimiters. + #[inline] + pub fn write_identifier(&self, writer: &mut String, ident: &str) { + self.identifier.write_to( + writer, + // double: Escape identifier delimiters with doubling. + // backslash: Escape identifier delimiters with backslash + |key, out| match key { + "double" => template::replace_to(ident, '"', "\"\"", out), + "backslash" => write_identifier_backtick_escape(out, ident), + _ => unreachable!("invalid key - write_identifier"), + }, + ) + } +} + +const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF"; + +/// Write `byte` as two uppercase hex digits. +#[inline] +fn write_hex2(writer: &mut String, byte: u8) { + let buf = [HEX_DIGITS[(byte >> 4) as usize], HEX_DIGITS[(byte & 0x0F) as usize]]; + // SAFETY: HEX_DIGITS is ASCII, so `buf` is valid UTF-8. + writer.push_str(unsafe { std::str::from_utf8_unchecked(&buf) }) +} + +/// Write `bytes` as a contiguous uppercase hex run (`DEADBEEF`). +/// +/// 64 raw bytes fill a 128-byte stack buffer and are written once. +#[inline] +fn bytes_to_hex(writer: &mut String, bytes: &[u8]) { + let mut buf = [0u8; 128]; + for chunk in bytes.chunks(64) { + for (i, &byte) in chunk.iter().enumerate() { + let o = i * 2; + buf[o] = HEX_DIGITS[(byte >> 4) as usize]; + buf[o + 1] = HEX_DIGITS[(byte & 0x0F) as usize]; + } + let n = chunk.len() * 2; + // SAFETY: `buf[..n]` is filled only from `HEX_DIGITS` (ASCII). + writer.push_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) }); + } +} + +/// Write each byte as `\xNN` (uppercase hex). +/// +/// 32 raw bytes fill a 128-byte stack buffer and are written once. +#[inline] +fn write_bytes_slash_x(writer: &mut String, bytes: &[u8]) { + let mut buf = [0u8; 128]; + for chunk in bytes.chunks(32) { + for (i, &byte) in chunk.iter().enumerate() { + let o = i * 4; + buf[o] = b'\\'; + buf[o + 1] = b'x'; + buf[o + 2] = HEX_DIGITS[(byte >> 4) as usize]; + buf[o + 3] = HEX_DIGITS[(byte & 0x0F) as usize]; + } + let n = chunk.len() * 4; + // SAFETY: `buf[..n]` is ASCII (`\`, `x`, and hex digits). + writer.push_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) }); + } +} + +/// Write `\xNN` for a single byte (uppercase hex). +#[inline] +fn write_byte_hex_escape(writer: &mut String, byte: u8) { + writer.push_str("\\x"); + write_hex2(writer, byte) +} + +/// Write `\uNNNN` for a codepoint that fits in 16 bits. +#[inline] +fn write_unicode4_escape(writer: &mut String, code: u32) { + debug_assert!(code <= 0xFFFF); + writer.push_str("\\u"); + write_hex2(writer, (code >> 8) as u8); + write_hex2(writer, code as u8) +} + +/// Use the string escape table, plus `\``. +/// +/// `'` is left as itself. Only known escapes are emitted. +fn write_identifier_backtick_escape(writer: &mut String, ident: &str) { + write_span_escapes(writer, ident, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'`' => SpanEscape::Literal("\\`"), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + b'\t' => SpanEscape::Literal("\\t"), + b if b < 32 || b == 127 => SpanEscape::HexByte, + _ => SpanEscape::Keep, + }) +} + +/// Write `bytes[from..to]` as a string. +/// +/// # Safety invariant +/// `bytes` is the raw view of a `&str`. Callers split only on ASCII bytes, +/// which are char boundaries, or at `bytes.len()`. +#[inline] +fn write_byte_span(writer: &mut String, bytes: &[u8], from: usize, to: usize) { + if from == to { + return; + } + // SAFETY: see invariant above. + let chunk = unsafe { bytes.get_unchecked(from..to) }; + writer.push_str(unsafe { std::str::from_utf8_unchecked(chunk) }) +} + +enum SpanEscape { + Keep, + Literal(&'static str), + HexByte, + Unicode4, +} + +/// Copy each plain span with one `write_str`. Special bytes are ASCII. +#[cfg_attr(feature = "nightly", optimize(speed))] +fn write_span_escapes(writer: &mut String, string: &str, mut classify: impl FnMut(u8) -> SpanEscape) { + let bytes = string.as_bytes(); + let mut start = 0; + let mut i = 0; + while i < bytes.len() { + match classify(bytes[i]) { + SpanEscape::Keep => i += 1, + SpanEscape::Literal(repl) => { + write_byte_span(writer, bytes, start, i); + writer.push_str(repl); + i += 1; + start = i; + } + SpanEscape::HexByte => { + write_byte_span(writer, bytes, start, i); + write_byte_hex_escape(writer, bytes[i]); + i += 1; + start = i; + } + SpanEscape::Unicode4 => { + write_byte_span(writer, bytes, start, i); + write_unicode4_escape(writer, bytes[i] as u32); + i += 1; + start = i; + } + } + } + write_byte_span(writer, bytes, start, bytes.len()) +} + +fn write_backslash_hex(writer: &mut String, string: &str) { + write_span_escapes(writer, string, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'\'' => SpanEscape::Literal("\\'"), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + b'\t' => SpanEscape::Literal("\\t"), + b if b < 32 || b == 127 => SpanEscape::HexByte, + _ => SpanEscape::Keep, + }) +} + +fn write_backslash_mysql(writer: &mut String, string: &str) { + write_span_escapes(writer, string, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'\'' => SpanEscape::Literal("\\'"), + b'"' => SpanEscape::Literal("\\\""), + 0 => SpanEscape::Literal("\\0"), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + 0x1a => SpanEscape::Literal("\\Z"), + _ => SpanEscape::Keep, + }) +} + +fn write_backslash_unicode(writer: &mut String, string: &str, escape_double_quote: bool) { + write_span_escapes(writer, string, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'\'' => SpanEscape::Literal("\\'"), + b'"' if escape_double_quote => SpanEscape::Literal("\\\""), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + b'\t' => SpanEscape::Literal("\\t"), + 0 => SpanEscape::Literal("\\0"), + b if b < 32 || b == 127 => SpanEscape::Unicode4, + _ => SpanEscape::Keep, + }) +} + +fn write_backslash_hex_zero(writer: &mut String, string: &str) { + write_span_escapes(writer, string, |b| match b { + b'\\' => SpanEscape::Literal("\\\\"), + b'\'' => SpanEscape::Literal("\\'"), + b'\n' => SpanEscape::Literal("\\n"), + b'\r' => SpanEscape::Literal("\\r"), + b'\t' => SpanEscape::Literal("\\t"), + 0 => SpanEscape::Literal("\\0"), + b if b < 32 || b == 127 => SpanEscape::HexByte, + _ => SpanEscape::Keep, + }) +} diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index acb6a81..7daf60b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1,86 +1,35 @@ -pub mod config; pub mod errors; mod base; -mod builtin; +mod escape; -#[cfg(test)] -mod tests; - -pub use base::PyDialect; -pub use builtin::BIGQUERY_DIALECT; -pub use builtin::DATABRICKS_DIALECT; -pub use builtin::DUCKDB_DIALECT; -pub use builtin::HIVE_DIALECT; -pub use builtin::MSSQL_DIALECT; -pub use builtin::MYSQL_DIALECT; -pub use builtin::ORACLE_DIALECT; -pub use builtin::POSTGRESQL_DIALECT; -pub use builtin::REDSHIFT_DIALECT; -pub use builtin::SNOWFLAKE_DIALECT; -pub use builtin::SPARK_DIALECT; -pub use builtin::SQLITE_DIALECT; -pub use builtin::TERADATA_DIALECT; +pub use base::*; #[pyo3::pymodule(name = "dialect", gil_used = false)] pub mod dialect_mod { #[pymodule_export] - use super::PyDialect; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const BigQuery: super::PyDialect = super::BIGQUERY_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Databricks: super::PyDialect = super::DATABRICKS_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const DuckDb: super::PyDialect = super::DUCKDB_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Hive: super::PyDialect = super::HIVE_DIALECT; - + use super::AggregateConfig; #[pymodule_export] - #[allow(non_upper_case_globals)] - const MsSql: super::PyDialect = super::MSSQL_DIALECT; - + use super::BitwiseConfig; #[pymodule_export] - #[allow(non_upper_case_globals)] - const MySql: super::PyDialect = super::MYSQL_DIALECT; - + use super::CastConfig; #[pymodule_export] - #[allow(non_upper_case_globals)] - const Oracle: super::PyDialect = super::ORACLE_DIALECT; - + use super::LikeConfig; #[pymodule_export] - #[allow(non_upper_case_globals)] - const PostgreSql: super::PyDialect = super::POSTGRESQL_DIALECT; - + use super::OperatorConfig; #[pymodule_export] - #[allow(non_upper_case_globals)] - const Redshift: super::PyDialect = super::REDSHIFT_DIALECT; - + use super::PyDialect; #[pymodule_export] - #[allow(non_upper_case_globals)] - const Snowflake: super::PyDialect = super::SNOWFLAKE_DIALECT; - + use super::SchemaConfig; #[pymodule_export] - #[allow(non_upper_case_globals)] - const Spark: super::PyDialect = super::SPARK_DIALECT; - + use super::StringConfig; #[pymodule_export] - #[allow(non_upper_case_globals)] - const Sqlite: super::PyDialect = super::SQLITE_DIALECT; - + use super::TemporalConfig; #[pymodule_export] - #[allow(non_upper_case_globals)] - const Teradata: super::PyDialect = super::TERADATA_DIALECT; + use super::TupleConfig; #[pymodule_init] - fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { - super::base::export_options(m) + fn init(module: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { + super::export_options(module) } } diff --git a/src/expr/enum_/func.rs b/src/expr/enum_/func.rs index 4f8718f..59d17af 100644 --- a/src/expr/enum_/func.rs +++ b/src/expr/enum_/func.rs @@ -1,7 +1,7 @@ use super::ExprInner; use super::ident::Ident; -use crate::dialect::config; +use crate::dialect; use crate::internal::repr; use crate::internal::sql::PrepareResult; use crate::internal::sql::SqlWriter; @@ -109,12 +109,14 @@ impl WriteSql for Function { write_separated(dialect, out, &self.args, ", ")?; - if self.null_treatment.is_some() && matches!(dialect.0.null_treatment, config::NullTreatment::Unsupported) { + if self.null_treatment.is_some() + && matches!(dialect.aggregate.null_treatment, dialect::NullTreatment::Unsupported) + { return Err( rapidquery_macros::pyerr!(PyRuntimeError, "null_treatment is not supported by this dialect",).into(), ); } - if self.filter.is_some() && matches!(dialect.0.function_filter, config::FunctionFilter::Unsupported) { + if self.filter.is_some() && matches!(dialect.aggregate.function_filter, dialect::FunctionFilter::Unsupported) { return Err( rapidquery_macros::pyerr!(PyRuntimeError, "function filter is not supported by this dialect",).into(), ); @@ -122,7 +124,7 @@ impl WriteSql for Function { // Inside parens IGNORE/RESPECT NULLS if let Some(x) = self.null_treatment - && matches!(dialect.0.null_treatment, config::NullTreatment::InsideParens) + && matches!(dialect.aggregate.null_treatment, dialect::NullTreatment::InsideParens) { out.push_char_if(args_available, ' '); out.push_str(x.as_sql()); @@ -130,7 +132,7 @@ impl WriteSql for Function { // Inside parens WHERE filter if let Some(x) = &self.filter - && matches!(dialect.0.function_filter, config::FunctionFilter::InsideWhere) + && matches!(dialect.aggregate.function_filter, dialect::FunctionFilter::InsideWhere) { out.push_char_if(args_available, ' '); out.push_str("WHERE "); @@ -141,14 +143,14 @@ impl WriteSql for Function { // FILTER, then postfix IGNORE/RESPECT NULLS (immediately before OVER). if let Some(x) = &self.filter { - match dialect.0.function_filter { - config::FunctionFilter::FilterWhere => { + match dialect.aggregate.function_filter { + dialect::FunctionFilter::FilterWhere => { out.push_char(' '); out.push_str("FILTER (WHERE "); x.write_sql(dialect, out)?; out.push_char(')'); } - config::FunctionFilter::Filter => { + dialect::FunctionFilter::Filter => { out.push_char(' '); out.push_str("FILTER ("); x.write_sql(dialect, out)?; @@ -159,7 +161,7 @@ impl WriteSql for Function { } if let Some(x) = self.null_treatment - && matches!(dialect.0.null_treatment, config::NullTreatment::Postfix) + && matches!(dialect.aggregate.null_treatment, dialect::NullTreatment::Postfix) { out.push_char(' '); out.push_str(x.as_sql()); diff --git a/src/expr/enum_/ident.rs b/src/expr/enum_/ident.rs index e486408..3453aee 100644 --- a/src/expr/enum_/ident.rs +++ b/src/expr/enum_/ident.rs @@ -74,7 +74,7 @@ impl WriteSql for IdentPart { fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { match self { Self::Quote(name) => { - dialect.0.write_identifier(out, name.as_str())?; + dialect.write_identifier(out.sql_mut(), name.as_str()); } Self::Unquote(name) => { out.push_str(name.as_str()); diff --git a/src/expr/enum_/like.rs b/src/expr/enum_/like.rs index 5c12014..2bacb02 100644 --- a/src/expr/enum_/like.rs +++ b/src/expr/enum_/like.rs @@ -1,22 +1,27 @@ //! `LIKE` / `ILIKE` / `LIKE ANY` / `SIMILAR TO` / `RLIKE` AST nodes. use super::ExprInner; -use crate::dialect::PyDialect; -use crate::dialect::config; +use crate::dialect; use crate::internal::repr; use crate::internal::sql::PrepareResult; use crate::internal::sql::SqlWriter; use crate::internal::sql::WriteSql; use crate::internal::sql::write_separated; -pub(super) fn write_escape(dialect: &PyDialect, out: &mut W, escape: Option) -> PrepareResult<()> { +pub(super) fn write_escape( + dialect: &dialect::PyDialect, + out: &mut W, + escape: Option, +) -> PrepareResult<()> { let Some(c) = escape else { return Ok(()); }; out.push_str(" ESCAPE "); let mut buf = [0u8; 4]; - dialect.0.write_string_literal(out, c.encode_utf8(&mut buf))?; + dialect + .string + .write_string_literal(out.sql_mut(), c.encode_utf8(&mut buf))?; Ok(()) } @@ -96,7 +101,7 @@ impl Like { } fn write_like_operand( - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, expr: &ExprInner, wrap_lower: bool, @@ -126,7 +131,7 @@ fn write_like_infix(out: &mut W, negated: bool, ilike: bool) { } fn write_like_pattern_list( - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, patterns: &[ExprInner], wrap_lower: bool, @@ -147,17 +152,17 @@ fn write_like_pattern_list( /// `ESCAPE` is legal on this form. `native_any` is `LIKE ANY` that is not an /// `OR` expansion. #[inline] -fn like_escape_allowed(syntax: config::LikeEscape, native_any: bool) -> bool { +fn like_escape_allowed(syntax: dialect::LikeEscape, native_any: bool) -> bool { if native_any { - syntax.contains(config::LikeEscape::ANY) + syntax.contains(dialect::LikeEscape::ANY) } else { - syntax.contains(config::LikeEscape::SCALAR) + syntax.contains(dialect::LikeEscape::SCALAR) } } #[inline] fn ensure_like_escape_is_supported( - syntax: config::LikeEscape, + syntax: dialect::LikeEscape, escape: Option, native_any: bool, ) -> PrepareResult<()> { @@ -178,7 +183,7 @@ fn ensure_like_escape_is_supported( /// `[NOT] (x LIKE|ILIKE ANY (ARRAY?[…]) [ESCAPE char])` fn write_like_any_native( - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, like: &Like, patterns: &[ExprInner], @@ -211,21 +216,25 @@ fn write_like_any_native( } impl WriteSql for Like { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { let (ilike, wrap_lower) = if self.case_sensitive { (false, false) } else { - match dialect.0.ilike_expression { - config::ILikeExpression::Standard => (true, false), - config::ILikeExpression::UseLike => (false, false), - config::ILikeExpression::UseLikeLower => (false, true), + match dialect.like.ilike { + dialect::ILikeStrategy::Unsupported => { + return Err( + rapidquery_macros::pyerr!(PyRuntimeError, "ILIKE is not supported by this dialect").into(), + ); + } + dialect::ILikeStrategy::Standard => (true, false), + dialect::ILikeStrategy::UseLikeLower => (false, true), } }; ensure_like_escape_is_supported( - dialect.0.like_escape, + dialect.like.escape, self.escape, - matches!(self.pattern, LikePattern::Any(_)) && !matches!(dialect.0.like_any, config::LikeAny::OrExpansion), + matches!(self.pattern, LikePattern::Any(_)) && !matches!(dialect.like.any, dialect::LikeAny::OrExpansion), )?; match &self.pattern { @@ -235,14 +244,14 @@ impl WriteSql for Like { write_like_operand(dialect, out, pattern, wrap_lower, true)?; write_escape(dialect, out, self.escape) } - LikePattern::Any(patterns) => match dialect.0.like_any { - config::LikeAny::ArrayConstructor => { + LikePattern::Any(patterns) => match dialect.like.any { + dialect::LikeAny::ArrayConstructor => { write_like_any_native(dialect, out, self, patterns, ilike, wrap_lower, true) } - config::LikeAny::PatternList => { + dialect::LikeAny::PatternList => { write_like_any_native(dialect, out, self, patterns, ilike, wrap_lower, false) } - config::LikeAny::OrExpansion => { + dialect::LikeAny::OrExpansion => { let sep = if self.negated { " AND " } else { " OR " }; out.push_char('('); for (i, pattern) in patterns.iter().enumerate() { @@ -303,8 +312,8 @@ impl SimilarTo { } impl WriteSql for SimilarTo { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { - if !dialect.0.supports_similar_to { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + if !dialect.like.supports_similar_to { return Err( rapidquery_macros::pyerr!(PyRuntimeError, "SIMILAR TO is not supported by this dialect",).into(), ); @@ -363,7 +372,7 @@ impl RLike { /// `NOT? name(expr, pattern)` — unquoted function rewrite. fn write_regex_call( - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, name: &str, negated: bool, @@ -383,12 +392,12 @@ fn write_regex_call( } impl WriteSql for RLike { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { - match dialect.0.rlike_expression { - config::RLikeExpression::Unsupported => { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + match dialect.like.regex { + dialect::RegexSpelling::Unsupported => { Err(rapidquery_macros::pyerr!(PyRuntimeError, "RLIKE is not supported by this dialect",).into()) } - config::RLikeExpression::Standard => { + dialect::RegexSpelling::Standard => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { out.push_str(" NOT RLIKE "); @@ -397,18 +406,18 @@ impl WriteSql for RLike { } self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) } - config::RLikeExpression::Tilde => { + dialect::RegexSpelling::Tilde => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; out.push_str(if self.negated { " !~ " } else { " ~ " }); self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) } - config::RLikeExpression::RegexpLike => { + dialect::RegexSpelling::RegexpLike => { write_regex_call(dialect, out, "REGEXP_LIKE", self.negated, &self.expr, &self.pattern) } - config::RLikeExpression::RegexpContains => { + dialect::RegexSpelling::RegexpContains => { write_regex_call(dialect, out, "REGEXP_CONTAINS", self.negated, &self.expr, &self.pattern) } - config::RLikeExpression::RegexpMatches => { + dialect::RegexSpelling::RegexpMatches => { write_regex_call(dialect, out, "regexp_matches", self.negated, &self.expr, &self.pattern) } } diff --git a/src/expr/enum_/nodes.rs b/src/expr/enum_/nodes.rs index 5251ecd..2bbc5ee 100644 --- a/src/expr/enum_/nodes.rs +++ b/src/expr/enum_/nodes.rs @@ -6,8 +6,7 @@ use super::IsKind; use super::TrimWhere; use super::UnaryOp; use super::UnarySpacing; -use crate::dialect::PyDialect; -use crate::dialect::config; +use crate::dialect; use crate::internal::repr; use crate::internal::sql::PrepareResult; use crate::internal::sql::SqlWriter; @@ -56,28 +55,28 @@ impl Tuple { } impl WriteSql for Tuple { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { let arity = self.items.len(); - if dialect.0.tuple_syntax != config::TupleSyntax::Unsupported { - if arity == 0 && !dialect.0.tuple_arity.contains(config::TupleArity::EMPTY) { + if dialect.tuple.syntax != dialect::TupleSyntax::Unsupported { + if arity == 0 && !dialect.tuple.arity.contains(dialect::TupleArity::EMPTY) { return Err(unsupported("empty tuple is not supported by this dialect")); } - if arity == 1 && !dialect.0.tuple_arity.contains(config::TupleArity::ONE) { + if arity == 1 && !dialect.tuple.arity.contains(dialect::TupleArity::ONE) { return Err(unsupported("a tuple needs at least two elements on this dialect")); } } - match dialect.0.tuple_syntax { - config::TupleSyntax::Unsupported => { + match dialect.tuple.syntax { + dialect::TupleSyntax::Unsupported => { return Err(unsupported("tuple values are not supported by this dialect")); } - config::TupleSyntax::Row => { + dialect::TupleSyntax::Row => { out.push_str("ROW("); } - config::TupleSyntax::Struct => { + dialect::TupleSyntax::Struct => { out.push_str("struct("); } - config::TupleSyntax::Parentheses => { + dialect::TupleSyntax::Parentheses => { out.push_char('('); } } @@ -124,20 +123,20 @@ impl Unary { } impl WriteSql for Unary { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { if self.op == UnaryOp::BitNot { - if !dialect.0.bitwise_operations.contains(config::BitwiseOperations::NOT) { + if !dialect.bitwise.operations.contains(dialect::BitwiseOperations::NOT) { return Err(unsupported("bitwise NOT is not supported by this dialect")); } - match dialect.0.bitwise_call_syntax { - config::BitwiseCallSyntax::Infix => { + match dialect.bitwise.syntax { + dialect::BitwiseSyntax::Infix => { out.push_char('~'); return self .expr .write_operand(dialect, out, UnaryOp::BitNot.precedence(), true); } - config::BitwiseCallSyntax::BitFunction | config::BitwiseCallSyntax::ShiftFunction => { + dialect::BitwiseSyntax::BitFunction | dialect::BitwiseSyntax::ShiftFunction => { out.push_str("BITNOT("); self.expr.write_sql(dialect, out)?; out.push_char(')'); @@ -210,7 +209,7 @@ fn is_arithmetic_binary_op(op: &BinaryOp) -> bool { /// `name(left, right)` with an unquoted function name. #[inline] fn write_binary_call( - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, name: &str, left: &ExprInner, @@ -235,21 +234,21 @@ fn write_binary_call( #[allow(clippy::too_many_arguments)] fn write_bit_op( bin: &Binary, - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, - op: config::BitwiseOperations, + op: dialect::BitwiseOperations, infix: &str, bit_fn: &str, shift_fn: &str, missing: &'static str, ) -> PrepareResult<()> { - if !dialect.0.bitwise_operations.contains(op) { + if !dialect.bitwise.operations.contains(op) { return Err(unsupported(missing)); } - match dialect.0.bitwise_call_syntax { - config::BitwiseCallSyntax::Infix => write_infix(bin, dialect, out, infix, false), - config::BitwiseCallSyntax::BitFunction => write_binary_call(dialect, out, bit_fn, &bin.left, &bin.right), - config::BitwiseCallSyntax::ShiftFunction => write_binary_call(dialect, out, shift_fn, &bin.left, &bin.right), + match dialect.bitwise.syntax { + dialect::BitwiseSyntax::Infix => write_infix(bin, dialect, out, infix, false), + dialect::BitwiseSyntax::BitFunction => write_binary_call(dialect, out, bit_fn, &bin.left, &bin.right), + dialect::BitwiseSyntax::ShiftFunction => write_binary_call(dialect, out, shift_fn, &bin.left, &bin.right), } } @@ -262,7 +261,7 @@ fn write_bit_op( #[inline] fn write_infix( bin: &Binary, - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, spelling: &str, paren_arithmetic: bool, @@ -277,7 +276,7 @@ fn write_infix( #[inline] fn write_infix_side( - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, expr: &ExprInner, prec: u8, @@ -294,36 +293,43 @@ fn write_infix_side( } impl WriteSql for Binary { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { match &self.op { - BinaryOp::StringConcat => match dialect.0.string_concat { - config::StringConcat::ConcatFunction => { - write_binary_call(dialect, out, "CONCAT", &self.left, &self.right) - } - config::StringConcat::Pipes => write_infix(self, dialect, out, "||", true), - config::StringConcat::Plus => write_infix(self, dialect, out, "+", true), - }, - BinaryOp::BitXor => match dialect.0.bitwise_xor_spelling { - config::BitwiseXorSpelling::Unsupported => { + BinaryOp::StringConcat => { + _ = dialect.string.concat.write_to_result( + out, + // keys are `left` and `right` + |key, out| match key { + "left" => self.left.write_sql(dialect, out), + "right" => self.right.write_sql(dialect, out), + _ => unreachable!(), + }, + ); + Ok(()) + } + BinaryOp::BitXor => match dialect.bitwise.xor { + dialect::BitwiseXorSpelling::Unsupported => { Err(unsupported("bitwise XOR is not supported by this dialect")) } - config::BitwiseXorSpelling::Caret => write_infix(self, dialect, out, "^", false), - config::BitwiseXorSpelling::Hash => write_infix(self, dialect, out, "#", false), - config::BitwiseXorSpelling::Function => write_binary_call(dialect, out, "xor", &self.left, &self.right), - config::BitwiseXorSpelling::BitFunction => { + dialect::BitwiseXorSpelling::Caret => write_infix(self, dialect, out, "^", false), + dialect::BitwiseXorSpelling::Hash => write_infix(self, dialect, out, "#", false), + dialect::BitwiseXorSpelling::Function => { + write_binary_call(dialect, out, "xor", &self.left, &self.right) + } + dialect::BitwiseXorSpelling::BitFunction => { write_binary_call(dialect, out, "BITXOR", &self.left, &self.right) } }, - BinaryOp::Mod => match dialect.0.modulo_spelling { - config::ModuloSpelling::Percent => write_infix(self, dialect, out, "%", false), - config::ModuloSpelling::Function => write_binary_call(dialect, out, "MOD", &self.left, &self.right), - config::ModuloSpelling::Infix => write_infix(self, dialect, out, "MOD", false), + BinaryOp::Mod => match dialect.operator.modulo { + dialect::ModuloSpelling::Percent => write_infix(self, dialect, out, "%", false), + dialect::ModuloSpelling::Function => write_binary_call(dialect, out, "MOD", &self.left, &self.right), + dialect::ModuloSpelling::Infix => write_infix(self, dialect, out, "MOD", false), }, BinaryOp::BitAnd => write_bit_op( self, dialect, out, - config::BitwiseOperations::AND, + dialect::BitwiseOperations::AND, "&", "BITAND", "BITAND", @@ -333,7 +339,7 @@ impl WriteSql for Binary { self, dialect, out, - config::BitwiseOperations::OR, + dialect::BitwiseOperations::OR, "|", "BITOR", "BITOR", @@ -343,7 +349,7 @@ impl WriteSql for Binary { self, dialect, out, - config::BitwiseOperations::SHL, + dialect::BitwiseOperations::SHL, "<<", "BITSHIFTLEFT", "SHIFTLEFT", @@ -353,7 +359,7 @@ impl WriteSql for Binary { self, dialect, out, - config::BitwiseOperations::SHR, + dialect::BitwiseOperations::SHR, ">>", "BITSHIFTRIGHT", "SHIFTRIGHT", @@ -405,7 +411,7 @@ impl Is { } impl WriteSql for Is { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { out.push_str(" IS NOT "); @@ -458,12 +464,12 @@ impl IsDistinctFrom { } impl WriteSql for IsDistinctFrom { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { - match dialect.0.is_distinct_from_spelling { - config::IsDistinctFromSpelling::Unsupported => { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + match dialect.operator.is_distinct_from { + dialect::IsDistinctFromSpelling::Unsupported => { Err(unsupported("IS DISTINCT FROM is not supported by this dialect")) } - config::IsDistinctFromSpelling::Standard => { + dialect::IsDistinctFromSpelling::Standard => { self.left.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { out.push_str(" IS NOT DISTINCT FROM "); @@ -472,7 +478,7 @@ impl WriteSql for IsDistinctFrom { } self.right.write_operand(dialect, out, Self::PRECEDENCE, true) } - config::IsDistinctFromSpelling::NullSafeEqual => { + dialect::IsDistinctFromSpelling::NullSafeEqual => { // `<=>` is null-safe equal, i.e. `IS NOT DISTINCT FROM`. if !self.negated { out.push_str("NOT ("); @@ -535,10 +541,10 @@ impl InList { } impl WriteSql for InList { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { if self.list.is_empty() { // `x IN ()` is false and `x NOT IN ()` is true, including when `x` is NULL. - dialect.0.write_boolean(out, self.negated)?; + dialect.literal.write_boolean(out.sql_mut(), self.negated); return Ok(()); } @@ -605,7 +611,7 @@ impl Between { } impl WriteSql for Between { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { // SQL Server ranks `AND` above `IN`/`BETWEEN`/`OR`, and those four share // a level evaluated left to right. The parentheses keep the predicate // from being parsed as part of the left operand. @@ -670,7 +676,7 @@ impl Cast { Ok(fmt) } - fn write_call(&self, dialect: &PyDialect, out: &mut W, name: &str) -> PrepareResult<()> { + fn write_call(&self, dialect: &dialect::PyDialect, out: &mut W, name: &str) -> PrepareResult<()> { out.push_str(name); out.push_char('('); self.expr.write_sql(dialect, out)?; @@ -682,17 +688,26 @@ impl Cast { } impl WriteSql for Cast { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { match self.kind { CastKind::Cast => self.write_call(dialect, out, "CAST"), - CastKind::TryCast => match dialect.0.cast_try_cast { - config::TryCast::Unsupported => Err(unsupported("try cast is not supported by this dialect")), - config::TryCast::TryCast => self.write_call(dialect, out, "TRY_CAST"), - config::TryCast::TryCastWord => self.write_call(dialect, out, "TRYCAST"), - config::TryCast::SafeCast => self.write_call(dialect, out, "SAFE_CAST"), - }, + CastKind::TryCast => { + if let Some(tmpl) = &dialect.cast.try_cast { + tmpl.write_to_result( + out, + // keys are `expr` and `type` + |key, out| match key { + "expr" => self.expr.write_sql(dialect, out), + "type" => self.data_type_name.write_sql(dialect, out), + _ => unreachable!(), + }, + ) + } else { + Err(unsupported("try cast is not supported by this dialect")) + } + } CastKind::DoubleColon => { - if !dialect.0.cast_double_colon { + if !dialect.cast.supports_double_colon { return Err(unsupported("double-colon cast is not supported by this dialect")); } out.push_char('('); @@ -755,7 +770,7 @@ impl Trim { } #[inline] -fn write_trim_function(trim: &Trim, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { +fn write_trim_function(trim: &Trim, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { let name = match trim.where_ { TrimWhere::Both => "TRIM", TrimWhere::Leading => "LTRIM", @@ -773,9 +788,9 @@ fn write_trim_function(trim: &Trim, dialect: &PyDialect, out: &mut } impl WriteSql for Trim { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { - match dialect.0.trim_spelling { - config::TrimSpelling::SqlFrom => { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + match dialect.operator.trim { + dialect::TrimSpelling::SqlFrom => { out.push_str("TRIM("); match (&self.where_, &self.characters) { (TrimWhere::Both, None) => self.expr.write_sql(dialect, out)?, @@ -800,8 +815,8 @@ impl WriteSql for Trim { out.push_char(')'); Ok(()) } - config::TrimSpelling::Function => write_trim_function(self, dialect, out), - config::TrimSpelling::SpacesOnly => { + dialect::TrimSpelling::Function => write_trim_function(self, dialect, out), + dialect::TrimSpelling::SpacesOnly => { if self.characters.is_some() { return Err(unsupported("trim characters are not supported by this dialect")); } @@ -848,15 +863,15 @@ impl Collate { } impl WriteSql for Collate { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { - match dialect.0.collate_spelling { - config::CollateSpelling::Unsupported => Err(unsupported("COLLATE is not supported by this dialect")), - config::CollateSpelling::InfixIdent => { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + match dialect.operator.collate { + dialect::CollateSpelling::Unsupported => Err(unsupported("COLLATE is not supported by this dialect")), + dialect::CollateSpelling::InfixIdent => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; out.push_str(" COLLATE "); self.collation.write_sql(dialect, out) } - config::CollateSpelling::FunctionString => { + dialect::CollateSpelling::FunctionString => { let body = match self.collation.parts() { [super::IdentPart::Quote(body) | super::IdentPart::Unquote(body)] if !body.is_empty() => { body.as_str() @@ -868,7 +883,7 @@ impl WriteSql for Collate { out.push_str("COLLATE("); self.expr.write_sql(dialect, out)?; out.push_str(", "); - dialect.0.write_string_literal(out, body)?; + dialect.string.write_string_literal(out.sql_mut(), body)?; out.push_char(')'); Ok(()) } @@ -927,7 +942,7 @@ impl Case { } impl WriteSql for Case { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { out.push_str("CASE"); if let Some(operand) = &self.operand { out.push_char(' '); @@ -990,7 +1005,7 @@ impl Custom { impl WriteSql for Custom { #[cfg_attr(feature = "nightly", optimize(speed))] - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { let bytes = self.sql.as_bytes(); let mut i = 0; let mut start = 0; diff --git a/src/internal/repr.rs b/src/internal/repr.rs index b970ca9..a270eb2 100644 --- a/src/internal/repr.rs +++ b/src/internal/repr.rs @@ -1,5 +1,5 @@ +use std::fmt; use std::fmt::Write; -use std::fmt::{self}; /// A value rendered straight into a [`ReprFormatter`] buffer. /// diff --git a/src/internal/sql.rs b/src/internal/sql.rs index b872c81..6f99fd6 100644 --- a/src/internal/sql.rs +++ b/src/internal/sql.rs @@ -3,13 +3,13 @@ //! [`WriteSql`] walks a node into a [`SqlWriter`]. [`BufferedSql`] inlines values. //! [`ParameterizedSql`] binds them as placeholders. +// FIXME: revert change: move this file into root folder + use std::fmt::Write; use pyo3::types::PyAnyMethods; -use crate::dialect::PyDialect; -use crate::dialect::config::PlaceholderSyntax; -use crate::dialect::errors::FormatError; +use crate::dialect; use crate::internal::alias; use crate::serializers::SerializerType; @@ -51,25 +51,25 @@ impl From for PrepareError { } } -impl From for PrepareError { - fn from(err: FormatError) -> Self { +impl From for PrepareError { + fn from(err: dialect::errors::FormatError) -> Self { match err { - FormatError::Write => Self::Write, + dialect::errors::FormatError::Write => Self::Write, other => Self::Python(other.into()), } } } -impl From for pyo3::PyErr { - fn from(err: FormatError) -> Self { +impl From for pyo3::PyErr { + fn from(err: dialect::errors::FormatError) -> Self { match err { - FormatError::Write => { + dialect::errors::FormatError::Write => { rapidquery_macros::pyerr!(PyRuntimeError, "failed to write SQL") } - FormatError::NulInString => { + dialect::errors::FormatError::NulInString => { rapidquery_macros::pyerr!(PyValueError, "NUL is not allowed in a string literal") } - FormatError::Unsupported(name) => { + dialect::errors::FormatError::Unsupported(name) => { rapidquery_macros::pyerr!(PyNotImplementedError, "this dialect does not support {}", name) } } @@ -109,6 +109,8 @@ pub type PrepareParams = smallvec::SmallVec<[alias::PyObject; 1]>; /// [`BufferedSql`] inlines values. [`ParameterizedSql`] binds them. Other methods /// only append text. pub trait SqlWriter: std::fmt::Write { + fn sql_mut(&mut self) -> &mut String; + fn push_str(&mut self, s: &str) -> &mut Self; #[inline] @@ -128,7 +130,7 @@ pub trait SqlWriter: std::fmt::Write { /// unused. fn push_value( &mut self, - dialect: &PyDialect, + dialect: &dialect::PyDialect, serializer: &SerializerType, object: &alias::PyObject, ) -> PrepareResult<()>; @@ -181,6 +183,11 @@ impl<'py> BufferedSql<'py> { } impl<'py> SqlWriter for BufferedSql<'py> { + #[inline] + fn sql_mut(&mut self) -> &mut String { + &mut self.sql + } + #[inline] fn push_str(&mut self, s: &str) -> &mut Self { self.sql.push_str(s); @@ -195,7 +202,7 @@ impl<'py> SqlWriter for BufferedSql<'py> { fn push_value( &mut self, - dialect: &PyDialect, + dialect: &dialect::PyDialect, serializer: &SerializerType, object: &alias::PyObject, ) -> PrepareResult<()> { @@ -287,24 +294,23 @@ impl<'py> ParameterizedSql<'py> { impl ParameterizedSql<'_> { /// Register `param` and append its placeholder to the SQL in one step. #[inline] - pub fn push_param(&mut self, dialect: &PyDialect, param: alias::PyObject) -> &mut Self { - match dialect.0.placeholder_syntax { - PlaceholderSyntax::Simple => { - self.sql.push(dialect.0.placeholder_character); - } - PlaceholderSyntax::Numbered => { - self.sql.push(dialect.0.placeholder_character); - let _ = write!(self.sql, "{}", self.next_id); - self.next_id += 1; - } - } + pub fn push_param(&mut self, dialect: &dialect::PyDialect, param: alias::PyObject) -> &mut Self { + dialect + .placeholder + .write_to(&mut self.sql, |_, out| write!(out, "{}", self.next_id).expect("")); + self.next_id += 1; self.params.push(param); self } } impl SqlWriter for ParameterizedSql<'_> { + #[inline] + fn sql_mut(&mut self) -> &mut String { + &mut self.sql + } + #[inline] fn push_str(&mut self, s: &str) -> &mut Self { self.sql.push_str(s); @@ -320,7 +326,7 @@ impl SqlWriter for ParameterizedSql<'_> { #[inline] fn push_value( &mut self, - dialect: &PyDialect, + dialect: &dialect::PyDialect, _serializer: &SerializerType, object: &alias::PyObject, ) -> PrepareResult<()> { @@ -345,15 +351,15 @@ impl std::fmt::Write for ParameterizedSql<'_> { /// Render `self` into `out`. The writer chooses literal vs placeholder for values. pub trait WriteSql { - // NOTE FOR ME: - // Why `PyDialect` instead of Rust `Dialect`? - // because we accept custom serializers and custom serializer functions - // need to get dialect objects. - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()>; + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()>; } /// Render `stmt` as raw SQL (values inlined as literals). -pub fn write_sql<'py, T: WriteSql>(py: pyo3::Python<'py>, stmt: &T, dialect: &PyDialect) -> PrepareResult { +pub fn write_sql<'py, T: WriteSql>( + py: pyo3::Python<'py>, + stmt: &T, + dialect: &dialect::PyDialect, +) -> PrepareResult { let mut buf = BufferedSql::with_capacity(py, 256); stmt.write_sql(dialect, &mut buf)?; Ok(buf.into_string()) @@ -363,7 +369,7 @@ pub fn write_sql<'py, T: WriteSql>(py: pyo3::Python<'py>, stmt: &T, dialect: &Py pub fn prepare<'py, T: WriteSql>( py: pyo3::Python<'py>, stmt: &T, - dialect: &PyDialect, + dialect: &dialect::PyDialect, ) -> PrepareResult<(String, PrepareParams)> { let mut context = ParameterizedSql::with_capacity(py, 256); stmt.write_sql(dialect, &mut context)?; @@ -376,7 +382,7 @@ pub fn prepare<'py, T: WriteSql>( /// Use `","` / `", "` for lists and `" AND "` / `" OR "` for predicates. #[cfg_attr(feature = "nightly", optimize(speed))] pub fn write_separated<'a, T, W>( - dialect: &PyDialect, + dialect: &dialect::PyDialect, out: &mut W, items: impl IntoIterator, separator: &str, diff --git a/src/internal/template.rs b/src/internal/template.rs index bfc67dd..04b968a 100644 --- a/src/internal/template.rs +++ b/src/internal/template.rs @@ -54,10 +54,9 @@ impl ReplacePattern for &str { /// Matches are non-overlapping and searched left to right, same as [`str::replace`]. /// Empty `&str` needles are ignored (no infinite loop, input is copied as-is). #[inline] -pub fn replace_to(haystack: &str, from: P, to: &str, out: &mut W) -> std::fmt::Result +pub fn replace_to

(haystack: &str, from: P, to: &str, out: &mut String) where P: ReplacePattern, - W: std::fmt::Write, { // Single ASCII byte → single ASCII byte: still go through the generic loop. // `str::find(char)` already uses memchr, which beats a hand-rolled byte scan @@ -68,8 +67,8 @@ where while let Some((rel, len)) = from.find(&haystack[search_at..]) { let start = search_at + rel; - out.write_str(&haystack[last_end..start])?; - out.write_str(to)?; + out.push_str(&haystack[last_end..start]); + out.push_str(to); last_end = start + len; search_at = last_end; @@ -79,7 +78,7 @@ where } } - out.write_str(&haystack[last_end..]) + out.push_str(&haystack[last_end..]) } #[derive(Debug, Clone, Copy, derive_more::Display)] @@ -88,8 +87,10 @@ pub enum TemplateParseError { TemplateIsTooLong, #[display("one of required keys not found: {_0}")] RequiredKeyNotFound(&'static str), - #[display("placeholders are missing (at least {_0} placeholder is required, {_1} found)")] + #[display("placeholders are missing (minimum {_0}, {_1} found)")] MissingPlaceholder(u8, usize), + #[display("too many placeholders (maximum is {_0} , {_1} found)")] + TooManyPlaceholder(u8, usize), } impl std::error::Error for TemplateParseError {} @@ -105,10 +106,15 @@ struct TemplateMatches { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Template { tmpl: arcstr::ArcStr, - matches: smallvec::SmallVec<[TemplateMatches; 1]>, + matches: Vec, } impl Template { + #[inline(always)] + pub fn as_str(&self) -> &str { + &self.tmpl + } + /// Parses a template string, locating every `{key}` whose name appears in `keys`. /// /// # Errors @@ -117,14 +123,20 @@ impl Template { /// - Fewer than `minimum` placeholders found. /// - A required key is missing. #[cold] - pub fn parse(tmpl: &str, keys: &[(&'static str, bool)], minimum: u8) -> Result { + pub fn parse( + tmpl: &str, + keys: &[(&'static str, bool)], + minimum: u8, + maximum: u8, + ) -> Result { debug_assert!(keys.len() <= 16, "too many keys"); + debug_assert!(minimum <= maximum, "minimum is greater than maximum"); if tmpl.len() > 256 { return Err(TemplateParseError::TemplateIsTooLong); } - let mut matches: smallvec::SmallVec<[TemplateMatches; 1]> = smallvec::SmallVec::with_capacity(minimum as usize); + let mut matches: Vec = Vec::with_capacity(minimum as usize); let bytes = tmpl.as_bytes(); let mut required_mask = 0usize; let mut i = 0; @@ -161,6 +173,10 @@ impl Template { return Err(TemplateParseError::MissingPlaceholder(minimum, matches.len())); } + if matches.len() > maximum as usize { + return Err(TemplateParseError::TooManyPlaceholder(minimum, matches.len())); + } + for (idx, (key, required)) in keys.iter().enumerate() { if *required && (required_mask & (1 << idx)) == 0 { return Err(TemplateParseError::RequiredKeyNotFound(key)); @@ -177,19 +193,22 @@ impl Template { /// /// `fill` is called for each placeholder with its key name and the writer. /// It is responsible for writing the replacement (or nothing). + /// + /// # Panics + /// We're accepting `out` as `std::fmt::Write`, but it should not return `std::fmt::Error`. #[inline] #[cfg_attr(feature = "nightly", optimize(speed))] - pub fn write_to(&self, out: &mut W, mut fill: F) -> std::fmt::Result + pub fn write_to_result(&self, out: &mut W, mut fill: F) -> Result<(), E> where W: std::fmt::Write, - F: FnMut(&'static str, &mut W) -> std::fmt::Result, + F: FnMut(&'static str, &mut W) -> Result<(), E>, { let s = self.tmpl.as_str(); let mut last: u16 = 0; for m in self.matches.iter() { if last < m.start { - out.write_str(&s[last as usize..m.start as usize])?; + out.write_str(&s[last as usize..m.start as usize]).expect(""); } fill(m.key, out)?; @@ -197,21 +216,51 @@ impl Template { } if (last as usize) < s.len() { - out.write_str(&s[last as usize..])?; + out.write_str(&s[last as usize..]).expect(""); } Ok(()) } + + // Writes the expanded template to `out`. + /// + /// `fill` is called for each placeholder with its key name and the writer. + /// It is responsible for writing the replacement (or nothing). + /// + /// # Panics + /// We're accepting `out` as `std::fmt::Write`, but it should not return `std::fmt::Error`. + #[inline] + #[cfg_attr(feature = "nightly", optimize(speed))] + pub fn write_to(&self, out: &mut W, mut fill: F) + where + W: std::fmt::Write, + F: FnMut(&'static str, &mut W), + { + let s = self.tmpl.as_str(); + let mut last: u16 = 0; + + for m in self.matches.iter() { + if last < m.start { + out.write_str(&s[last as usize..m.start as usize]).expect(""); + } + + fill(m.key, out); + last = m.end; + } + + if (last as usize) < s.len() { + out.write_str(&s[last as usize..]).expect(""); + } + } } #[cfg(test)] mod tests { use super::*; - use std::fmt::Write; fn replace(haystack: &str, from: impl ReplacePattern, to: &str) -> String { let mut out = String::with_capacity(haystack.len()); - replace_to(haystack, from, to, &mut out).unwrap(); + replace_to(haystack, from, to, &mut out); out } @@ -281,17 +330,14 @@ mod tests { #[test] fn single_required_key() { - let t = Template::parse("{hex}::BLOB", &[("hex", true), ("raw", false)], 1).unwrap(); + let t = Template::parse("{hex}::BLOB", &[("hex", true), ("raw", false)], 1, 1).unwrap(); let mut out = String::new(); t.write_to(&mut out, |key, w| { if key == "hex" { - w.write_str("0xDEADBEEF") - } else { - Ok(()) + w.push_str("0xDEADBEEF") } - }) - .unwrap(); + }); assert_eq!(out, "0xDEADBEEF::BLOB"); } @@ -302,89 +348,96 @@ mod tests { "regexp_matches({expr}, {pattern})", &[("expr", true), ("pattern", true), ("flags", false)], 2, + 2, ) .unwrap(); let mut out = String::new(); t.write_to(&mut out, |key, w| match key { - "expr" => w.write_str("col"), - "pattern" => w.write_str("'^foo.*'"), - _ => Ok(()), - }) - .unwrap(); + "expr" => w.push_str("col"), + "pattern" => w.push_str("'^foo.*'"), + _ => {} + }); assert_eq!(out, "regexp_matches(col, '^foo.*')"); } #[test] fn optional_key_omitted() { - let t = Template::parse("func({a}, {b})", &[("a", true), ("b", false)], 1).unwrap(); + let t = Template::parse("func({a}, {b})", &[("a", true), ("b", false)], 1, 2).unwrap(); let mut out = String::new(); - t.write_to(&mut out, |key, w| if key == "a" { w.write_str("1") } else { Ok(()) }) - .unwrap(); + t.write_to_result(&mut out, |key, w| { + if key == "a" { + w.push('1'); + Ok::<(), ()>(()) + } else { + Ok(()) + } + }) + .unwrap(); assert_eq!(out, "func(1, )"); } #[test] fn required_key_missing_at_parse_fails() { - let err = Template::parse("no placeholders", &[("missing", true)], 0); + let err = Template::parse("no placeholders", &[("missing", true)], 0, 1); assert!(matches!(err, Err(TemplateParseError::RequiredKeyNotFound("missing")))); } #[test] fn minimum_placeholders_not_met() { - let err = Template::parse("only {one}", &[("one", false)], 2); + let err = Template::parse("only {one}", &[("one", false)], 2, 3); assert!(matches!(err, Err(TemplateParseError::MissingPlaceholder(2, 1)))); } #[test] fn unknown_placeholders_are_left_literal() { - let t = Template::parse("hello {unknown} world", &[("known", false)], 0).unwrap(); + let t = Template::parse("hello {unknown} world", &[("known", false)], 0, 3).unwrap(); let mut out = String::new(); - t.write_to(&mut out, |_, _| Ok(())).unwrap(); + t.write_to(&mut out, |_, _| {}); assert_eq!(out, "hello {unknown} world"); } #[test] fn repeated_keys() { - let t = Template::parse("{x} + {x}", &[("x", true)], 2).unwrap(); + let t = Template::parse("{x} + {x}", &[("x", true)], 2, 3).unwrap(); let mut out = String::new(); - t.write_to(&mut out, |_, w| w.write_str("1")).unwrap(); + t.write_to(&mut out, |_, w| w.push('1')); assert_eq!(out, "1 + 1"); } #[test] fn empty_template() { - let t = Template::parse("", &[], 0).unwrap(); + let t = Template::parse("", &[], 0, 3).unwrap(); let mut out = String::new(); - t.write_to(&mut out, |_, _| Ok(())).unwrap(); + t.write_to(&mut out, |_, _| {}); assert_eq!(out, ""); } #[test] fn empty_key() { - let t = Template::parse(":param_{}", &[("", true)], 1).unwrap(); + let t = Template::parse(":param_{}", &[("", true)], 1, 3).unwrap(); let mut out = String::new(); - t.write_to(&mut out, |_, w| w.write_char('1')).unwrap(); + t.write_to(&mut out, |_, w| w.push('1')); assert_eq!(out, ":param_1"); } #[test] fn no_placeholders() { - let t = Template::parse("plain text", &[("unused", false)], 0).unwrap(); + let t = Template::parse("plain text", &[("unused", false)], 0, 3).unwrap(); let mut out = String::new(); - t.write_to(&mut out, |_, _| Ok(())).unwrap(); + t.write_to(&mut out, |_, _| {}); assert_eq!(out, "plain text"); } @@ -392,7 +445,7 @@ mod tests { #[test] fn template_too_long() { let long = "a".repeat(257); - let err = Template::parse(&long, &[], 0); + let err = Template::parse(&long, &[], 0, 1); assert!(matches!(err, Err(TemplateParseError::TemplateIsTooLong))); } } diff --git a/src/serializers/functions.rs b/src/serializers/functions.rs index 2d3c136..a7f1861 100644 --- a/src/serializers/functions.rs +++ b/src/serializers/functions.rs @@ -1,9 +1,7 @@ use pyo3::types::PyAnyMethods; use pyo3::types::PyTzInfoAccess; -use crate::dialect::PyDialect; -use crate::dialect::config; -use crate::dialect::errors::FormatError; +use crate::dialect; use crate::internal::alias; use crate::internal::utils; use std::fmt::Write; @@ -24,34 +22,6 @@ pub(super) unsafe fn pyobject_string(py: pyo3::Python<'_>, ptr: *mut pyo3::ffi:: out } -#[inline(always)] -fn write_quoted(dialect: &config::DialectConfig, out: &mut String, raw: &str) -> pyo3::PyResult<()> { - dialect.write_string_literal(out, raw).map_err(Into::into) -} - -#[inline] -fn write_with_keyword( - dialect: &config::DialectConfig, - out: &mut String, - keyword: Option<&str>, - raw: &str, -) -> pyo3::PyResult<()> { - if let Some(keyword) = keyword { - out.push_str(keyword); - out.push(' '); - } - write_quoted(dialect, out, raw) -} - -#[inline(always)] -fn timestamp_keyword(format: config::TimestampFormat, aware: bool) -> Option<&'static str> { - match format { - config::TimestampFormat::Quoted => None, - config::TimestampFormat::Timestamptz => Some(if aware { "TIMESTAMPTZ" } else { "TIMESTAMP" }), - config::TimestampFormat::WithTimezone => Some(if aware { "TIMESTAMP WITH TIMEZONE" } else { "TIMESTAMP" }), - } -} - #[inline] pub(super) unsafe fn write_integer( py: pyo3::Python<'_>, @@ -123,12 +93,12 @@ pub(super) unsafe fn write_float( pub(super) unsafe fn write_bool( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::LiteralConfig, out: &mut String, ) -> pyo3::PyResult<()> { if pyo3::ffi::PyBool_Check(ptr) == 1 { let value = pyo3::ffi::Py_True() == ptr; - dialect.write_boolean(out, value)?; + cfg.write_boolean(out, value); Ok(()) } else { rapidquery_macros::pyerr!( @@ -143,11 +113,11 @@ pub(super) unsafe fn write_bool( pub(super) unsafe fn write_str( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::StringConfig, out: &mut String, ) -> pyo3::PyResult<()> { if pyo3::ffi::PyUnicode_Check(ptr) == 1 { - return dialect + return cfg .write_string_literal(out, utils::borrow_py_str_buffer(py, ptr)?) .map_err(Into::into); } @@ -163,9 +133,13 @@ pub(super) unsafe fn write_str( pub(super) unsafe fn write_uuid( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::LiteralConfig, out: &mut String, ) -> pyo3::PyResult<()> { + let Some(tmpl) = &cfg.uuid else { + return Err(dialect::errors::FormatError::Unsupported("UuidSerializer").into()); + }; + let text = if crate::typeref::check_exact(ptr, crate::typeref::STD_UUID_TYPE) { pyobject_string(py, ptr)? } else if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { @@ -182,25 +156,19 @@ pub(super) unsafe fn write_uuid( ); }; - match dialect.uuid_format { - config::UuidFormat::Quoted => write_quoted(dialect, out, &text), - config::UuidFormat::UuidKeyword => { - out.push_str("UUID "); - write_quoted(dialect, out, &text) - } - config::UuidFormat::CastUuid => { - write_quoted(dialect, out, &text)?; - out.push_str("::uuid"); - Ok(()) - } - } + tmpl.write_to( + out, + // no special keys + |_, out| out.push_str(text.as_str()), + ); + Ok(()) } #[inline] pub(super) unsafe fn write_bytes( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::LiteralConfig, out: &mut String, ) -> pyo3::PyResult<()> { if pyo3::ffi::PyBytes_CheckExact(ptr) == 1 { @@ -208,9 +176,8 @@ pub(super) unsafe fn write_bytes( let len = pyo3::ffi::PyBytes_Size(ptr) as usize; debug_assert!(!buf.is_null()); - dialect - .write_bytes(out, std::slice::from_raw_parts(buf, len)) - .map_err(Into::into) + cfg.write_bytes(out, std::slice::from_raw_parts(buf, len)); + Ok(()) } else { rapidquery_macros::pyerr!( Err(PyTypeError), @@ -223,9 +190,13 @@ pub(super) unsafe fn write_bytes( pub(super) unsafe fn write_bit( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::LiteralConfig, out: &mut String, ) -> pyo3::PyResult<()> { + if cfg.bit.is_none() { + return Err(dialect::errors::FormatError::Unsupported("BitSerializer").into()); + } + if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { let raw = utils::borrow_py_str_buffer(py, ptr)?; @@ -237,7 +208,7 @@ pub(super) unsafe fn write_bit( ); } - dialect.write_bits(out, raw).map_err(Into::into) + cfg.write_bits(out, raw).map_err(Into::into) } else { rapidquery_macros::pyerr!( Err(PyTypeError), @@ -251,44 +222,38 @@ pub(super) unsafe fn write_bit( pub(super) unsafe fn write_json( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + literal_cfg: &dialect::LiteralConfig, + string_cfg: &dialect::StringConfig, out: &mut String, ) -> pyo3::PyResult<()> { - crate::internal::modules::json_dumps(unsafe { pyo3::Bound::from_borrowed_ptr(py, ptr) }, |text| match dialect - .json_format - { - config::JsonFormat::Quoted => write_quoted(dialect, out, text), - config::JsonFormat::JsonKeyword => { - out.push_str("JSON "); - write_quoted(dialect, out, text) - } - config::JsonFormat::ParseJson => { - out.push_str("PARSE_JSON("); - write_quoted(dialect, out, text)?; - out.push(')'); + crate::internal::modules::json_dumps( + unsafe { pyo3::Bound::from_borrowed_ptr(py, ptr) }, + // write result into `out` + |text| { + literal_cfg.json.write_to_result( + out, + // no special keys + |_, out| string_cfg.write_string_literal(out, text), + )?; Ok(()) - } - }) + }, + ) } #[inline] pub(super) unsafe fn write_date( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::TemporalConfig, out: &mut String, ) -> pyo3::PyResult<()> { if crate::typeref::check_exact(ptr, crate::typeref::STD_DATE_TYPE) { let val: pyo3::Bound<'_, pyo3::types::PyDate> = pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; - let date: chrono::NaiveDate = val.extract()?; - let mut body = String::new(); - let _ = date.format("%Y-%m-%d").write_to(&mut body); - let keyword = match dialect.date_format { - config::DateFormat::Quoted => None, - config::DateFormat::Keyword => Some("DATE"), - }; - write_with_keyword(dialect, out, keyword, &body) + + cfg.date + .write_to(out, |_, out| date.format("%Y-%m-%d").write_to(out).expect("")); + Ok(()) } else { rapidquery_macros::pyerr!( Err(PyTypeError), @@ -301,20 +266,16 @@ pub(super) unsafe fn write_date( pub(super) unsafe fn write_time( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::TemporalConfig, out: &mut String, ) -> pyo3::PyResult<()> { if crate::typeref::check_exact(ptr, crate::typeref::STD_TIME_TYPE) { let val: pyo3::Bound<'_, pyo3::types::PyTime> = pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; - let time: chrono::NaiveTime = val.extract()?; - let mut body = String::new(); - let _ = time.format("%H:%M:%S%.f").write_to(&mut body); - let keyword = match dialect.time_format { - config::TimeFormat::Quoted => None, - config::TimeFormat::Keyword => Some("TIME"), - }; - write_with_keyword(dialect, out, keyword, &body) + + cfg.date + .write_to(out, |_, out| time.format("%H:%M:%S%.f").write_to(out).expect("")); + Ok(()) } else { rapidquery_macros::pyerr!( Err(PyTypeError), @@ -328,7 +289,7 @@ pub(super) unsafe fn write_time( pub(super) unsafe fn write_datetime( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::TemporalConfig, out: &mut String, ) -> pyo3::PyResult<()> { use chrono::TimeZone; @@ -338,15 +299,19 @@ pub(super) unsafe fn write_datetime( if val.get_tzinfo().is_none() { let datetime: chrono::NaiveDateTime = val.extract()?; - let mut body = String::new(); - let _ = datetime.format("%Y-%m-%d %H:%M:%S%.f").write_to(&mut body); - return write_with_keyword(dialect, out, timestamp_keyword(dialect.timestamp_format, false), &body); + + cfg.datetime_naive.write_to(out, |_, out| { + datetime.format("%Y-%m-%d %H:%M:%S%.f").write_to(out).expect("") + }); + return Ok(()); } let datetime: chrono::DateTime = val.extract()?; - let mut body = String::new(); - let _ = datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(&mut body); - return write_with_keyword(dialect, out, timestamp_keyword(dialect.timestamp_format, true), &body); + + cfg.datetime_aware.write_to(out, |_, out| { + datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(out).expect("") + }); + return Ok(()); } if pyo3::ffi::PyLong_CheckExact(ptr) == 1 { @@ -358,9 +323,10 @@ pub(super) unsafe fn write_datetime( return match chrono::Utc.timestamp_opt(num, 0) { chrono::offset::LocalResult::Single(datetime) => { - let mut body = String::new(); - let _ = datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(&mut body); - write_with_keyword(dialect, out, timestamp_keyword(dialect.timestamp_format, true), &body) + cfg.datetime_aware.write_to(out, |_, out| { + datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(out).expect("") + }); + return Ok(()); } _ => rapidquery_macros::pyerr!( Err(PyOverflowError), @@ -374,9 +340,10 @@ pub(super) unsafe fn write_datetime( let num = pyo3::ffi::PyFloat_AsDouble(ptr); return match chrono::Utc.timestamp_opt(num.trunc() as i64, (num.fract() * 1_000_000_000.0) as u32) { chrono::offset::LocalResult::Single(datetime) => { - let mut body = String::new(); - let _ = datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(&mut body); - write_with_keyword(dialect, out, timestamp_keyword(dialect.timestamp_format, true), &body) + cfg.datetime_aware.write_to(out, |_, out| { + datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(out).expect("") + }); + return Ok(()); } _ => rapidquery_macros::pyerr!( Err(PyOverflowError), @@ -396,54 +363,54 @@ pub(super) unsafe fn write_datetime( pub(super) unsafe fn write_inet( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::LiteralConfig, out: &mut String, ) -> pyo3::PyResult<()> { + let Some(tmpl) = &cfg.inet else { + return Err(dialect::errors::FormatError::Unsupported("InetSerializer").into()); + }; + let text = pyobject_string(py, ptr)?; let net = ipnetwork::IpNetwork::from_str(&text).map_err(|e| rapidquery_macros::pyerr!(PyValueError, e.to_string()))?; + let body = net.to_string(); - match dialect.inet_format { - config::InetFormat::Unsupported => Err(FormatError::Unsupported("InetSerializer").into()), - config::InetFormat::Quoted => write_quoted(dialect, out, &body), - config::InetFormat::Cast => { - write_quoted(dialect, out, &body)?; - out.push_str("::inet"); - Ok(()) - } - } + let _ = tmpl.write_to_result(out, |_, out| out.write_str(&body)); + Ok(()) } pub(super) unsafe fn write_macaddr( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &config::DialectConfig, + cfg: &dialect::LiteralConfig, out: &mut String, ) -> pyo3::PyResult<()> { + let Some(tmpl) = &cfg.inet else { + return Err(dialect::errors::FormatError::Unsupported("InetSerializer").into()); + }; + let text = pyobject_string(py, ptr)?; let addr = mac_address::MacAddress::from_str(&text).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + let body = addr.to_string(); - match dialect.macaddr_format { - config::MacAddrFormat::Unsupported => Err(FormatError::Unsupported("MacAddressSerializer").into()), - config::MacAddrFormat::Quoted => write_quoted(dialect, out, &body), - config::MacAddrFormat::Cast => { - write_quoted(dialect, out, &body)?; - out.push_str("::macaddr"); - Ok(()) - } - } + let _ = tmpl.write_to_result(out, |_, out| out.write_str(&body)); + Ok(()) } pub(super) unsafe fn write_array( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &PyDialect, + dialect: &dialect::PyDialect, element: &super::inner::SerializerType, out: &mut String, ) -> pyo3::PyResult<()> { + let Some(tmpl) = &dialect.literal.array else { + return Err(dialect::errors::FormatError::Unsupported("ArraySerializer").into()); + }; + let (len, getter): ( isize, unsafe fn(*mut pyo3::ffi::PyObject, isize) -> *mut pyo3::ffi::PyObject, @@ -459,43 +426,30 @@ pub(super) unsafe fn write_array( ); }; - let close = match dialect.0.array_format { - config::ArrayFormat::Unsupported => { - return Err(FormatError::Unsupported("ArraySerializer").into()); - } - config::ArrayFormat::Constructor => { - out.push_str("ARRAY["); - ']' - } - config::ArrayFormat::List => { - out.push('['); - ']' - } - config::ArrayFormat::Function => { - out.push_str("array("); - ')' - } - }; - - for i in 0..len { - if i > 0 { - out.push_str(", "); - } - let item = getter(ptr, i); - if item.is_null() { - return Err(pyo3::PyErr::fetch(py)); - } - let borrowed = alias::BoundObject::from_borrowed_ptr(py, item); - element.serialize(dialect, &borrowed, out)?; - } - out.push(close); - Ok(()) + tmpl.write_to_result( + out, + // no special keys + |_, out| { + for i in 0..len { + if i > 0 { + out.push_str(", "); + } + let item = getter(ptr, i); + if item.is_null() { + return Err(pyo3::PyErr::fetch(py)); + } + let borrowed = alias::BoundObject::from_borrowed_ptr(py, item); + element.serialize(dialect, &borrowed, out)?; + } + Ok(()) + }, + ) } pub(super) unsafe fn write_custom( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - dialect: &PyDialect, + dialect: &dialect::PyDialect, function: &pyo3::Py, object: &alias::BoundObject<'_>, out: &mut String, diff --git a/src/serializers/inner.rs b/src/serializers/inner.rs index 85cd397..cdd9c5d 100644 --- a/src/serializers/inner.rs +++ b/src/serializers/inner.rs @@ -170,17 +170,17 @@ impl SerializerType { match self { Self::Integer => super::functions::write_integer(py, ptr, out), Self::Float => super::functions::write_float(py, ptr, out), - Self::Boolean => super::functions::write_bool(py, ptr, &dialect.0, out), - Self::String => super::functions::write_str(py, ptr, &dialect.0, out), - Self::Uuid => super::functions::write_uuid(py, ptr, &dialect.0, out), - Self::Bytes => super::functions::write_bytes(py, ptr, &dialect.0, out), - Self::Bit => super::functions::write_bit(py, ptr, &dialect.0, out), - Self::Json => super::functions::write_json(py, ptr, &dialect.0, out), - Self::Date => super::functions::write_date(py, ptr, &dialect.0, out), - Self::Time => super::functions::write_time(py, ptr, &dialect.0, out), - Self::Datetime => super::functions::write_datetime(py, ptr, &dialect.0, out), - Self::Inet => super::functions::write_inet(py, ptr, &dialect.0, out), - Self::MacAddress => super::functions::write_macaddr(py, ptr, &dialect.0, out), + Self::Boolean => super::functions::write_bool(py, ptr, &dialect.literal, out), + Self::String => super::functions::write_str(py, ptr, &dialect.string, out), + Self::Uuid => super::functions::write_uuid(py, ptr, &dialect.literal, out), + Self::Bytes => super::functions::write_bytes(py, ptr, &dialect.literal, out), + Self::Bit => super::functions::write_bit(py, ptr, &dialect.literal, out), + Self::Json => super::functions::write_json(py, ptr, &dialect.literal, &dialect.string, out), + Self::Date => super::functions::write_date(py, ptr, &dialect.temporal, out), + Self::Time => super::functions::write_time(py, ptr, &dialect.temporal, out), + Self::Datetime => super::functions::write_datetime(py, ptr, &dialect.temporal, out), + Self::Inet => super::functions::write_inet(py, ptr, &dialect.literal, out), + Self::MacAddress => super::functions::write_macaddr(py, ptr, &dialect.literal, out), Self::Array(element) => super::functions::write_array(py, ptr, dialect, element, out), Self::Unspecified => { let text = super::functions::pyobject_string(py, ptr)?; From 2e17e9ebfc89cb6eba8eaf9f966a2e07ea2e3074 Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Fri, 23 Oct 2026 18:45:52 +0330 Subject: [PATCH 04/11] Move sql to root folder --- dialect/tests.rs | 10 +++++----- src/data_type/mod.rs | 2 +- src/ddl/schema.rs | 2 +- src/expr/base.rs | 2 +- src/expr/enum_/func.rs | 8 ++++---- src/expr/enum_/ident.rs | 8 ++++---- src/expr/enum_/like.rs | 8 ++++---- src/expr/enum_/mod.rs | 6 +++--- src/expr/enum_/nodes.rs | 14 +++++++------- src/expr/enum_/value.rs | 6 +++--- src/internal/mod.rs | 1 - src/lib.rs | 1 + src/{internal => }/sql.rs | 0 13 files changed, 34 insertions(+), 34 deletions(-) rename src/{internal => }/sql.rs (100%) diff --git a/dialect/tests.rs b/dialect/tests.rs index 5b02875..091a55c 100644 --- a/dialect/tests.rs +++ b/dialect/tests.rs @@ -810,17 +810,17 @@ fn binary(op: crate::expr::enum_::BinaryOp) -> crate::expr::enum_::Binary { fn render( py: pyo3::Python<'_>, flags: config::DialectConfig, - expr: &impl crate::internal::sql::WriteSql, -) -> Result { + expr: &impl crate::sql::WriteSql, +) -> Result { let dialect = PyDialect(flags); - let mut buf = crate::internal::sql::BufferedSql::new(py); + let mut buf = crate::sql::BufferedSql::new(py); expr.write_sql(&dialect, &mut buf)?; Ok(buf.into_string()) } -fn error_text(err: crate::internal::sql::PrepareError) -> String { +fn error_text(err: crate::sql::PrepareError) -> String { match err { - crate::internal::sql::PrepareError::Python(err) => err.to_string(), + crate::sql::PrepareError::Python(err) => err.to_string(), other => other.to_string(), } } diff --git a/src/data_type/mod.rs b/src/data_type/mod.rs index 9d6b5fb..f882ddd 100644 --- a/src/data_type/mod.rs +++ b/src/data_type/mod.rs @@ -1,4 +1,4 @@ -use crate::internal::sql; +use crate::sql; #[cfg(test)] mod tests; diff --git a/src/ddl/schema.rs b/src/ddl/schema.rs index 7da02c3..aaee438 100644 --- a/src/ddl/schema.rs +++ b/src/ddl/schema.rs @@ -3,7 +3,7 @@ use crate::expr::enum_::Ident; use crate::expr::extractors::IdentExtractor; use crate::internal::alias; use crate::internal::repr; -use crate::internal::sql; +use crate::sql; /// Represent a `CREATE SCHEMA` statement. #[rapidquery_macros::pyclass("rapidquery.ddl.CreateSchema", extends=super::PyDataDefinitionLanguage)] diff --git a/src/expr/base.rs b/src/expr/base.rs index c827e5d..b811bde 100644 --- a/src/expr/base.rs +++ b/src/expr/base.rs @@ -4,8 +4,8 @@ //! empty frozen subclasses. Shared methods live on the base. use crate::internal::alias; -use crate::internal::sql; use crate::internal::utils; +use crate::sql; use super::enum_; diff --git a/src/expr/enum_/func.rs b/src/expr/enum_/func.rs index 59d17af..c573be8 100644 --- a/src/expr/enum_/func.rs +++ b/src/expr/enum_/func.rs @@ -3,10 +3,10 @@ use super::ident::Ident; use crate::dialect; use crate::internal::repr; -use crate::internal::sql::PrepareResult; -use crate::internal::sql::SqlWriter; -use crate::internal::sql::WriteSql; -use crate::internal::sql::write_separated; +use crate::sql::PrepareResult; +use crate::sql::SqlWriter; +use crate::sql::WriteSql; +use crate::sql::write_separated; /// How `NULL`s are treated in some window/aggregate functions. /// diff --git a/src/expr/enum_/ident.rs b/src/expr/enum_/ident.rs index 3453aee..d31b45d 100644 --- a/src/expr/enum_/ident.rs +++ b/src/expr/enum_/ident.rs @@ -2,10 +2,10 @@ use crate::dialect::PyDialect; use crate::internal::repr; -use crate::internal::sql::PrepareResult; -use crate::internal::sql::SqlWriter; -use crate::internal::sql::WriteSql; -use crate::internal::sql::write_separated; +use crate::sql::PrepareResult; +use crate::sql::SqlWriter; +use crate::sql::WriteSql; +use crate::sql::write_separated; /// One segment of a dotted identifier. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/src/expr/enum_/like.rs b/src/expr/enum_/like.rs index 2bacb02..a9d4128 100644 --- a/src/expr/enum_/like.rs +++ b/src/expr/enum_/like.rs @@ -3,10 +3,10 @@ use super::ExprInner; use crate::dialect; use crate::internal::repr; -use crate::internal::sql::PrepareResult; -use crate::internal::sql::SqlWriter; -use crate::internal::sql::WriteSql; -use crate::internal::sql::write_separated; +use crate::sql::PrepareResult; +use crate::sql::SqlWriter; +use crate::sql::WriteSql; +use crate::sql::write_separated; pub(super) fn write_escape( dialect: &dialect::PyDialect, diff --git a/src/expr/enum_/mod.rs b/src/expr/enum_/mod.rs index b422133..5b632d8 100644 --- a/src/expr/enum_/mod.rs +++ b/src/expr/enum_/mod.rs @@ -14,9 +14,9 @@ pub use value::*; use crate::dialect::PyDialect; use crate::internal::repr::ReprFormatter; -use crate::internal::sql::PrepareResult; -use crate::internal::sql::SqlWriter; -use crate::internal::sql::WriteSql; +use crate::sql::PrepareResult; +use crate::sql::SqlWriter; +use crate::sql::WriteSql; /// SQL expression tree. /// diff --git a/src/expr/enum_/nodes.rs b/src/expr/enum_/nodes.rs index 2bbc5ee..f762b65 100644 --- a/src/expr/enum_/nodes.rs +++ b/src/expr/enum_/nodes.rs @@ -8,13 +8,13 @@ use super::UnaryOp; use super::UnarySpacing; use crate::dialect; use crate::internal::repr; -use crate::internal::sql::PrepareResult; -use crate::internal::sql::SqlWriter; -use crate::internal::sql::WriteSql; -use crate::internal::sql::write_separated; +use crate::sql::PrepareResult; +use crate::sql::SqlWriter; +use crate::sql::WriteSql; +use crate::sql::write_separated; #[inline] -fn unsupported(message: &'static str) -> crate::internal::sql::PrepareError { +fn unsupported(message: &'static str) -> crate::sql::PrepareError { rapidquery_macros::pyerr!(PyRuntimeError, message).into() } @@ -1023,7 +1023,7 @@ impl WriteSql for Custom { if i + 1 < bytes.len() && bytes[i + 1] == b'}' { out.push_str(&self.sql[start..i]); if used >= self.args.len() { - return Err(crate::internal::sql::PrepareError::CustomPlaceholders { + return Err(crate::sql::PrepareError::CustomPlaceholders { expected: used + 1, got: self.args.len(), }); @@ -1046,7 +1046,7 @@ impl WriteSql for Custom { out.push_str(&self.sql[start..]); if used != self.args.len() { - return Err(crate::internal::sql::PrepareError::CustomPlaceholders { + return Err(crate::sql::PrepareError::CustomPlaceholders { expected: used, got: self.args.len(), }); diff --git a/src/expr/enum_/value.rs b/src/expr/enum_/value.rs index a6b99f6..ff9e149 100644 --- a/src/expr/enum_/value.rs +++ b/src/expr/enum_/value.rs @@ -6,11 +6,11 @@ use pyo3::types::PyStringMethods; use crate::dialect::PyDialect; use crate::internal::alias; use crate::internal::repr; -use crate::internal::sql::PrepareResult; -use crate::internal::sql::SqlWriter; -use crate::internal::sql::WriteSql; use crate::serializers::SerializerType; use crate::serializers::serializer_repr; +use crate::sql::PrepareResult; +use crate::sql::SqlWriter; +use crate::sql::WriteSql; /// Python object plus the serializer that turns it into a SQL literal. /// diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 14d0c82..3174e1c 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -1,7 +1,6 @@ pub mod alias; pub mod modules; pub mod repr; -pub mod sql; pub mod submodule_ext; pub mod template; pub mod utils; diff --git a/src/lib.rs b/src/lib.rs index 4ca20f0..0091769 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod dialect; pub mod expr; pub mod internal; pub mod serializers; +pub mod sql; pub mod typeref; pub mod dcl; diff --git a/src/internal/sql.rs b/src/sql.rs similarity index 100% rename from src/internal/sql.rs rename to src/sql.rs From 60414536d3139c953c327b3de0c3472fe8d9f07f Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Sat, 24 Oct 2026 12:23:22 +0330 Subject: [PATCH 05/11] Refactor dialect; Faster int/float serializing; Use internal trait instead of std::fmt::Write - We supported templates in some dialect fields, to improve customizing - We used `itoa` and `zmij` crates for converting floats and integers to &str - FastWrite helps us to control writing without being worry about write methods and unwraping useless `Result`s (Part of Python stubs and tests needs refactor) --- Cargo.lock | 14 +++ Cargo.toml | 2 + src/data_type/mod.rs | 8 +- src/ddl/schema.rs | 62 ++++------ src/dialect/base.rs | 6 +- src/dialect/errors.rs | 19 +-- src/dialect/escape.rs | 71 ++++++----- src/expr/base.rs | 2 +- src/expr/enum_/func.rs | 47 +++---- src/expr/enum_/ident.rs | 11 +- src/expr/enum_/like.rs | 105 ++++++++-------- src/expr/enum_/mod.rs | 9 +- src/expr/enum_/nodes.rs | 207 ++++++++++++++++--------------- src/expr/enum_/value.rs | 5 +- src/internal/mod.rs | 1 + src/internal/template.rs | 34 +++-- src/internal/writer.rs | 55 ++++++++ src/serializers/functions.rs | 96 +++++++------- src/serializers/inner.rs | 9 +- src/sql.rs | 234 +++++++---------------------------- 20 files changed, 459 insertions(+), 538 deletions(-) create mode 100644 src/internal/writer.rs diff --git a/Cargo.lock b/Cargo.lock index 2de4f57..3fee583 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,6 +203,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "js-sys" version = "0.3.106" @@ -404,6 +410,7 @@ dependencies = [ "derive_more", "hashbrown", "ipnetwork", + "itoa", "mac_address", "once_cell", "parking_lot", @@ -414,6 +421,7 @@ dependencies = [ "smallvec", "sqlparser", "uuid", + "zmij", ] [[package]] @@ -658,3 +666,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 5e360ab..1dac6e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ uuid = { version = "1.26.1", default-features = false } sea-query = { version = "1.0.2", default-features = false } # ONLY for inspiring the source sqlparser = { version = "0.62.0", default-features = false } +zmij = { version = "1.0.23", default-features = false } +itoa = { version = "1.0.18", default-features = false } [build-dependencies] pyo3-build-config = "0.30.1" diff --git a/src/data_type/mod.rs b/src/data_type/mod.rs index f882ddd..b6e1f78 100644 --- a/src/data_type/mod.rs +++ b/src/data_type/mod.rs @@ -187,12 +187,8 @@ impl std::fmt::Display for DataTypeName { } impl sql::WriteSql for DataTypeName { - fn write_sql( - &self, - _dialect: &crate::dialect::PyDialect, - out: &mut W, - ) -> sql::PrepareResult<()> { - out.push_str(self.as_str()); + fn write_sql(&self, _dialect: &crate::dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + out.write_str(self.as_str()); Ok(()) } } diff --git a/src/ddl/schema.rs b/src/ddl/schema.rs index aaee438..e0c5230 100644 --- a/src/ddl/schema.rs +++ b/src/ddl/schema.rs @@ -24,26 +24,26 @@ pub struct PyCreateSchema { } impl sql::WriteSql for PyCreateSchema { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> sql::PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { if !dialect.schema.features.contains(dialect::SchemaFeatures::SUPPORTED) { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "CREATE SCHEMA statement is not supported by this dialect" - ) - .into()); + )); } // CREATE {SCHEMA | DATABASE} if dialect.schema.features.contains(dialect::SchemaFeatures::IS_DATABASE) { - out.push_str("CREATE DATABASE "); + out.write_str("CREATE DATABASE "); } else { - out.push_str("CREATE SCHEMA "); + out.write_str("CREATE SCHEMA "); } // ... [IF NOT EXISTS] - if self.if_not_exists && dialect.schema.features.contains(dialect::SchemaFeatures::IF_NOT_EXISTS) { - out.push_str("IF NOT EXISTS "); - } + out.write_str_if( + self.if_not_exists && dialect.schema.features.contains(dialect::SchemaFeatures::IF_NOT_EXISTS), + "IF NOT EXISTS ", + ); // ... [] [AUTHORIZATION ] match dialect.schema.authorization { @@ -52,8 +52,7 @@ impl sql::WriteSql for PyCreateSchema { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "authorization is not supported in CREATE SCHEMA by this dialect" - ) - .into()); + )); } self.name.write_sql(dialect, out)?; @@ -62,8 +61,8 @@ impl sql::WriteSql for PyCreateSchema { self.name.write_sql(dialect, out)?; if let Some(x) = &self.authorization { - out.push_str(" AUTHORIZATION "); - dialect.write_identifier(out.sql_mut(), x); + out.write_str(" AUTHORIZATION "); + dialect.write_identifier(out, x); } } } @@ -74,15 +73,10 @@ impl sql::WriteSql for PyCreateSchema { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "location is not supported in CREATE SCHEMA by this dialect" - ) - .into()); + )); }; - tmpl.write_to_result( - out, - // no special key - |_, out| dialect.string.write_string_literal(out.sql_mut(), x.as_str()), - )?; + tmpl.write_to_result(out, |_, out| dialect.string.write_string_literal(out, x.as_str()))?; } Ok(()) @@ -130,7 +124,7 @@ impl PyCreateSchema { /// Render this expression as raw SQL (values inlined as literals). fn to_sql(&self, py: pyo3::Python, dialect: crate::dialect::PyDialect) -> pyo3::PyResult { - sql::write_sql(py, self, &dialect).map_err(Into::into) + sql::write_sql(py, self, &dialect) } fn __repr__(&self) -> String { @@ -171,26 +165,26 @@ pub struct PyDropSchema { } impl sql::WriteSql for PyDropSchema { - fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> sql::PrepareResult<()> { + fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { if !dialect.schema.features.contains(dialect::SchemaFeatures::SUPPORTED) { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "DROP SCHEMA statement is not supported by this dialect" - ) - .into()); + )); } // DROP {SCHEMA | DATABASE} if dialect.schema.features.contains(dialect::SchemaFeatures::IS_DATABASE) { - out.push_str("DROP DATABASE "); + out.write_str("DROP DATABASE "); } else { - out.push_str("DROP SCHEMA "); + out.write_str("DROP SCHEMA "); } // ... [IF EXISTS] - if self.if_exists && dialect.schema.features.contains(dialect::SchemaFeatures::IF_NOT_EXISTS) { - out.push_str("IF EXISTS "); - } + out.write_str_if( + self.if_exists && dialect.schema.features.contains(dialect::SchemaFeatures::IF_NOT_EXISTS), + "IF EXISTS ", + ); // ... self.name.write_sql(dialect, out)?; @@ -203,22 +197,20 @@ impl sql::WriteSql for PyDropSchema { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "This dialect does not support CASCADE keyword in DROP SCHEMA statement" - ) - .into()); + )); } - out.push_str(" CASCADE"); + out.write_str(" CASCADE"); } Some(DropSchemaBehavior::Restrict) => { if !dialect.schema.features.contains(dialect::SchemaFeatures::DROP_RESTRICT) { return Err(rapidquery_macros::pyerr!( PyRuntimeError, "This dialect does not support RESTRICT keyword in DROP SCHEMA statement" - ) - .into()); + )); } - out.push_str(" RESTRICT"); + out.write_str(" RESTRICT"); } } @@ -268,7 +260,7 @@ impl PyDropSchema { /// Render this expression as raw SQL (values inlined as literals). fn to_sql(&self, py: pyo3::Python, dialect: crate::dialect::PyDialect) -> pyo3::PyResult { - sql::write_sql(py, self, &dialect).map_err(Into::into) + sql::write_sql(py, self, &dialect) } fn __repr__(&self) -> String { diff --git a/src/dialect/base.rs b/src/dialect/base.rs index 01e948b..1f72226 100644 --- a/src/dialect/base.rs +++ b/src/dialect/base.rs @@ -327,9 +327,6 @@ pub struct SchemaConfig { /// How schema location is expressed e.g. `LOCATION {}` (value is escaped and includes quotes itself). pub location: Option, - - /// How schema location is expressed e.g. `COMMENT {}` (value is escaped and includes quotes itself). - pub comment: Option, } rapidquery_macros::pyo3_constructor! { @@ -339,7 +336,6 @@ rapidquery_macros::pyo3_constructor! { } = empty, authorization: enum SchemaAuthorizationStyle { Unsupported, Optional } = Unsupported, location: Option = None, - comment: Option = None, } } @@ -445,7 +441,7 @@ rapidquery_macros::pyo3_constructor! { placeholder: template(_; 1) = "?{}", like: class LikeConfig = LikeConfig::default(), bitwise: class BitwiseConfig = BitwiseConfig::default(), - identifier: template(_; 1) = "\"{}\"", + identifier: template(double?, backslash?; 1) = "\"{double}\"", string: class StringConfig = StringConfig::default(), cast: class CastConfig = CastConfig::default(), tuple: class TupleConfig = TupleConfig::default(), diff --git a/src/dialect/errors.rs b/src/dialect/errors.rs index aab5729..34972ba 100644 --- a/src/dialect/errors.rs +++ b/src/dialect/errors.rs @@ -1,27 +1,32 @@ use std::fmt; +// FIXME: Implement a unique error enum for project + /// Formatting / serializing errors #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FormatError { - /// Equals to [`fmt::Write`] - Write, /// Means a `NUL` character has found in string during escaping/serializing a string value. NulInString, /// Means something is not supported by a dialect Unsupported(&'static str), } -impl From for FormatError { - #[inline] - fn from(_: fmt::Error) -> Self { - Self::Write +impl From for pyo3::PyErr { + fn from(value: FormatError) -> Self { + match value { + FormatError::NulInString => { + pyo3::exceptions::PyValueError::new_err("NUL is not allowed in a string literal") + } + FormatError::Unsupported(name) => { + pyo3::exceptions::PyRuntimeError::new_err(format!("this dialect does not support {name}")) + } + } } } impl fmt::Display for FormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Write => f.write_str("failed to write SQL to memory"), Self::NulInString => f.write_str("NUL is not allowed in a string literal"), Self::Unsupported(name) => write!(f, "this dialect does not support {name}"), } diff --git a/src/dialect/escape.rs b/src/dialect/escape.rs index 443eeb0..a8e8b23 100644 --- a/src/dialect/escape.rs +++ b/src/dialect/escape.rs @@ -1,4 +1,5 @@ use crate::internal::template; +use crate::internal::writer; use super::base; use super::errors; @@ -6,12 +7,20 @@ use super::errors; impl base::LiteralConfig { /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). #[inline] - pub fn write_boolean(&self, writer: &mut String, value: bool) { + pub fn write_boolean(&self, writer: &mut W, value: bool) { match (self.boolean, value) { - (base::BoolFormat::Integer, false) => writer.push('0'), - (base::BoolFormat::Integer, true) => writer.push('1'), - (base::BoolFormat::Keyword, false) => writer.push_str("FALSE"), - (base::BoolFormat::Keyword, true) => writer.push_str("TRUE"), + (base::BoolFormat::Integer, false) => { + writer.write('0'); + } + (base::BoolFormat::Integer, true) => { + writer.write('1'); + } + (base::BoolFormat::Keyword, false) => { + writer.write_str("FALSE"); + } + (base::BoolFormat::Keyword, true) => { + writer.write_str("TRUE"); + } } } @@ -20,14 +29,14 @@ impl base::LiteralConfig { /// Native formats embed `bits` as-is. Callers must pass only `'0'` and `'1'`; /// that is checked with `debug_assert!`. #[inline] - pub fn write_bits(&self, writer: &mut String, bits: &str) -> Result<(), errors::FormatError> { + pub fn write_bits(&self, writer: &mut W, bits: &str) -> Result<(), errors::FormatError> { debug_assert!( bits.chars().all(|c| c == '0' || c == '1'), "write_bits: invalid input: {bits}" ); if let Some(template) = &self.bit { - template.write_to(writer, |_, out| out.push_str(bits)); + template.write_to(writer, |_, out| out.write_str(bits)); Ok(()) } else { Err(errors::FormatError::Unsupported("BitSerializer")) @@ -36,7 +45,7 @@ impl base::LiteralConfig { /// Write `bytes` as a native blob/bytea/varbyte literal. #[inline] - pub fn write_bytes(&self, writer: &mut String, bytes: &[u8]) { + pub fn write_bytes(&self, writer: &mut W, bytes: &[u8]) { self.bytes.write_to( writer, // hex: bytes as hex (e.g. DEADBEEF) @@ -53,7 +62,11 @@ impl base::LiteralConfig { impl base::StringConfig { /// Escape `raw` and wrap it as a complete string literal. #[inline] - pub fn write_string_literal(&self, writer: &mut String, raw: &str) -> Result<(), errors::FormatError> { + pub fn write_string_literal( + &self, + writer: &mut W, + raw: &str, + ) -> Result<(), errors::FormatError> { if raw.contains('\0') { return Err(errors::FormatError::NulInString); } @@ -83,7 +96,7 @@ impl base::StringConfig { impl base::PyDialect { /// Escape `ident` and wrap it in this dialect's identifier delimiters. #[inline] - pub fn write_identifier(&self, writer: &mut String, ident: &str) { + pub fn write_identifier(&self, writer: &mut W, ident: &str) { self.identifier.write_to( writer, // double: Escape identifier delimiters with doubling. @@ -101,17 +114,17 @@ const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF"; /// Write `byte` as two uppercase hex digits. #[inline] -fn write_hex2(writer: &mut String, byte: u8) { +fn write_hex2(writer: &mut W, byte: u8) { let buf = [HEX_DIGITS[(byte >> 4) as usize], HEX_DIGITS[(byte & 0x0F) as usize]]; // SAFETY: HEX_DIGITS is ASCII, so `buf` is valid UTF-8. - writer.push_str(unsafe { std::str::from_utf8_unchecked(&buf) }) + writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf) }) } /// Write `bytes` as a contiguous uppercase hex run (`DEADBEEF`). /// /// 64 raw bytes fill a 128-byte stack buffer and are written once. #[inline] -fn bytes_to_hex(writer: &mut String, bytes: &[u8]) { +fn bytes_to_hex(writer: &mut W, bytes: &[u8]) { let mut buf = [0u8; 128]; for chunk in bytes.chunks(64) { for (i, &byte) in chunk.iter().enumerate() { @@ -121,7 +134,7 @@ fn bytes_to_hex(writer: &mut String, bytes: &[u8]) { } let n = chunk.len() * 2; // SAFETY: `buf[..n]` is filled only from `HEX_DIGITS` (ASCII). - writer.push_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) }); + writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) }); } } @@ -129,7 +142,7 @@ fn bytes_to_hex(writer: &mut String, bytes: &[u8]) { /// /// 32 raw bytes fill a 128-byte stack buffer and are written once. #[inline] -fn write_bytes_slash_x(writer: &mut String, bytes: &[u8]) { +fn write_bytes_slash_x(writer: &mut W, bytes: &[u8]) { let mut buf = [0u8; 128]; for chunk in bytes.chunks(32) { for (i, &byte) in chunk.iter().enumerate() { @@ -141,22 +154,22 @@ fn write_bytes_slash_x(writer: &mut String, bytes: &[u8]) { } let n = chunk.len() * 4; // SAFETY: `buf[..n]` is ASCII (`\`, `x`, and hex digits). - writer.push_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) }); + writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) }); } } /// Write `\xNN` for a single byte (uppercase hex). #[inline] -fn write_byte_hex_escape(writer: &mut String, byte: u8) { - writer.push_str("\\x"); +fn write_byte_hex_escape(writer: &mut W, byte: u8) { + writer.write_str("\\x"); write_hex2(writer, byte) } /// Write `\uNNNN` for a codepoint that fits in 16 bits. #[inline] -fn write_unicode4_escape(writer: &mut String, code: u32) { +fn write_unicode4_escape(writer: &mut W, code: u32) { debug_assert!(code <= 0xFFFF); - writer.push_str("\\u"); + writer.write_str("\\u"); write_hex2(writer, (code >> 8) as u8); write_hex2(writer, code as u8) } @@ -164,7 +177,7 @@ fn write_unicode4_escape(writer: &mut String, code: u32) { /// Use the string escape table, plus `\``. /// /// `'` is left as itself. Only known escapes are emitted. -fn write_identifier_backtick_escape(writer: &mut String, ident: &str) { +fn write_identifier_backtick_escape(writer: &mut W, ident: &str) { write_span_escapes(writer, ident, |b| match b { b'\\' => SpanEscape::Literal("\\\\"), b'`' => SpanEscape::Literal("\\`"), @@ -182,13 +195,13 @@ fn write_identifier_backtick_escape(writer: &mut String, ident: &str) { /// `bytes` is the raw view of a `&str`. Callers split only on ASCII bytes, /// which are char boundaries, or at `bytes.len()`. #[inline] -fn write_byte_span(writer: &mut String, bytes: &[u8], from: usize, to: usize) { +fn write_byte_span(writer: &mut W, bytes: &[u8], from: usize, to: usize) { if from == to { return; } // SAFETY: see invariant above. let chunk = unsafe { bytes.get_unchecked(from..to) }; - writer.push_str(unsafe { std::str::from_utf8_unchecked(chunk) }) + writer.write_str(unsafe { std::str::from_utf8_unchecked(chunk) }) } enum SpanEscape { @@ -200,7 +213,7 @@ enum SpanEscape { /// Copy each plain span with one `write_str`. Special bytes are ASCII. #[cfg_attr(feature = "nightly", optimize(speed))] -fn write_span_escapes(writer: &mut String, string: &str, mut classify: impl FnMut(u8) -> SpanEscape) { +fn write_span_escapes(writer: &mut W, string: &str, mut classify: impl FnMut(u8) -> SpanEscape) { let bytes = string.as_bytes(); let mut start = 0; let mut i = 0; @@ -209,7 +222,7 @@ fn write_span_escapes(writer: &mut String, string: &str, mut classify: impl FnMu SpanEscape::Keep => i += 1, SpanEscape::Literal(repl) => { write_byte_span(writer, bytes, start, i); - writer.push_str(repl); + writer.write_str(repl); i += 1; start = i; } @@ -230,7 +243,7 @@ fn write_span_escapes(writer: &mut String, string: &str, mut classify: impl FnMu write_byte_span(writer, bytes, start, bytes.len()) } -fn write_backslash_hex(writer: &mut String, string: &str) { +fn write_backslash_hex(writer: &mut W, string: &str) { write_span_escapes(writer, string, |b| match b { b'\\' => SpanEscape::Literal("\\\\"), b'\'' => SpanEscape::Literal("\\'"), @@ -242,7 +255,7 @@ fn write_backslash_hex(writer: &mut String, string: &str) { }) } -fn write_backslash_mysql(writer: &mut String, string: &str) { +fn write_backslash_mysql(writer: &mut W, string: &str) { write_span_escapes(writer, string, |b| match b { b'\\' => SpanEscape::Literal("\\\\"), b'\'' => SpanEscape::Literal("\\'"), @@ -255,7 +268,7 @@ fn write_backslash_mysql(writer: &mut String, string: &str) { }) } -fn write_backslash_unicode(writer: &mut String, string: &str, escape_double_quote: bool) { +fn write_backslash_unicode(writer: &mut W, string: &str, escape_double_quote: bool) { write_span_escapes(writer, string, |b| match b { b'\\' => SpanEscape::Literal("\\\\"), b'\'' => SpanEscape::Literal("\\'"), @@ -269,7 +282,7 @@ fn write_backslash_unicode(writer: &mut String, string: &str, escape_double_quot }) } -fn write_backslash_hex_zero(writer: &mut String, string: &str) { +fn write_backslash_hex_zero(writer: &mut W, string: &str) { write_span_escapes(writer, string, |b| match b { b'\\' => SpanEscape::Literal("\\\\"), b'\'' => SpanEscape::Literal("\\'"), diff --git a/src/expr/base.rs b/src/expr/base.rs index b811bde..801c003 100644 --- a/src/expr/base.rs +++ b/src/expr/base.rs @@ -589,7 +589,7 @@ impl PyExpr { /// Render this expression as raw SQL (values inlined as literals). fn to_sql(slf: pyo3::PyRef<'_, Self>, dialect: crate::dialect::PyDialect) -> pyo3::PyResult { - sql::write_sql(slf.py(), &slf.0, &dialect).map_err(Into::into) + sql::write_sql(slf.py(), &slf.0, &dialect) } fn __repr__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { diff --git a/src/expr/enum_/func.rs b/src/expr/enum_/func.rs index c573be8..dcf458d 100644 --- a/src/expr/enum_/func.rs +++ b/src/expr/enum_/func.rs @@ -3,7 +3,6 @@ use super::ident::Ident; use crate::dialect; use crate::internal::repr; -use crate::sql::PrepareResult; use crate::sql::SqlWriter; use crate::sql::WriteSql; use crate::sql::write_separated; @@ -97,64 +96,66 @@ impl Function { } impl WriteSql for Function { - fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { // "name"([DISTINCT] args [{IGNORE|RESPECT} NULLS] [WHERE expr]) // [FILTER ([WHERE] expr)] [{IGNORE|RESPECT} NULLS] self.name.write_sql(dialect, out)?; let args_available = !self.args.is_empty(); - out.push_char('('); - out.push_str_if(self.distinct && args_available, "DISTINCT "); + out.write('('); + out.write_str_if(self.distinct && args_available, "DISTINCT "); write_separated(dialect, out, &self.args, ", ")?; if self.null_treatment.is_some() && matches!(dialect.aggregate.null_treatment, dialect::NullTreatment::Unsupported) { - return Err( - rapidquery_macros::pyerr!(PyRuntimeError, "null_treatment is not supported by this dialect",).into(), - ); + return Err(rapidquery_macros::pyerr!( + PyRuntimeError, + "null_treatment is not supported by this dialect", + )); } if self.filter.is_some() && matches!(dialect.aggregate.function_filter, dialect::FunctionFilter::Unsupported) { - return Err( - rapidquery_macros::pyerr!(PyRuntimeError, "function filter is not supported by this dialect",).into(), - ); + return Err(rapidquery_macros::pyerr!( + PyRuntimeError, + "function filter is not supported by this dialect", + )); } // Inside parens IGNORE/RESPECT NULLS if let Some(x) = self.null_treatment && matches!(dialect.aggregate.null_treatment, dialect::NullTreatment::InsideParens) { - out.push_char_if(args_available, ' '); - out.push_str(x.as_sql()); + out.write_if(args_available, ' '); + out.write_str(x.as_sql()); } // Inside parens WHERE filter if let Some(x) = &self.filter && matches!(dialect.aggregate.function_filter, dialect::FunctionFilter::InsideWhere) { - out.push_char_if(args_available, ' '); - out.push_str("WHERE "); + out.write_if(args_available, ' '); + out.write_str("WHERE "); x.write_sql(dialect, out)?; } - out.push_char(')'); + out.write(')'); // FILTER, then postfix IGNORE/RESPECT NULLS (immediately before OVER). if let Some(x) = &self.filter { match dialect.aggregate.function_filter { dialect::FunctionFilter::FilterWhere => { - out.push_char(' '); - out.push_str("FILTER (WHERE "); + out.write(' '); + out.write_str("FILTER (WHERE "); x.write_sql(dialect, out)?; - out.push_char(')'); + out.write(')'); } dialect::FunctionFilter::Filter => { - out.push_char(' '); - out.push_str("FILTER ("); + out.write(' '); + out.write_str("FILTER ("); x.write_sql(dialect, out)?; - out.push_char(')'); + out.write(')'); } _ => {} } @@ -163,8 +164,8 @@ impl WriteSql for Function { if let Some(x) = self.null_treatment && matches!(dialect.aggregate.null_treatment, dialect::NullTreatment::Postfix) { - out.push_char(' '); - out.push_str(x.as_sql()); + out.write(' '); + out.write_str(x.as_sql()); } Ok(()) diff --git a/src/expr/enum_/ident.rs b/src/expr/enum_/ident.rs index d31b45d..586bb64 100644 --- a/src/expr/enum_/ident.rs +++ b/src/expr/enum_/ident.rs @@ -2,7 +2,6 @@ use crate::dialect::PyDialect; use crate::internal::repr; -use crate::sql::PrepareResult; use crate::sql::SqlWriter; use crate::sql::WriteSql; use crate::sql::write_separated; @@ -71,16 +70,16 @@ impl PartialEq<&str> for IdentPart { impl WriteSql for IdentPart { #[inline] - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> pyo3::PyResult<()> { match self { Self::Quote(name) => { - dialect.write_identifier(out.sql_mut(), name.as_str()); + dialect.write_identifier(out, name.as_str()); } Self::Unquote(name) => { - out.push_str(name.as_str()); + out.write_str(name.as_str()); } Self::Asterisk => { - out.push_char('*'); + out.write('*'); } } Ok(()) @@ -268,7 +267,7 @@ impl std::str::FromStr for Ident { impl WriteSql for Ident { #[inline] - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> pyo3::PyResult<()> { write_separated(dialect, out, &self.0, ".") } } diff --git a/src/expr/enum_/like.rs b/src/expr/enum_/like.rs index a9d4128..68b55f6 100644 --- a/src/expr/enum_/like.rs +++ b/src/expr/enum_/like.rs @@ -3,7 +3,6 @@ use super::ExprInner; use crate::dialect; use crate::internal::repr; -use crate::sql::PrepareResult; use crate::sql::SqlWriter; use crate::sql::WriteSql; use crate::sql::write_separated; @@ -12,16 +11,14 @@ pub(super) fn write_escape( dialect: &dialect::PyDialect, out: &mut W, escape: Option, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { let Some(c) = escape else { return Ok(()); }; - out.push_str(" ESCAPE "); + out.write_str(" ESCAPE "); let mut buf = [0u8; 4]; - dialect - .string - .write_string_literal(out.sql_mut(), c.encode_utf8(&mut buf))?; + dialect.string.write_string_literal(out, c.encode_utf8(&mut buf))?; Ok(()) } @@ -106,11 +103,11 @@ fn write_like_operand( expr: &ExprInner, wrap_lower: bool, paren_if_equal: bool, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { if wrap_lower { - out.push_str("LOWER("); + out.write_str("LOWER("); expr.write_sql(dialect, out)?; - out.push_char(')'); + out.write(')'); Ok(()) } else { expr.write_operand(dialect, out, Like::PRECEDENCE, paren_if_equal) @@ -118,16 +115,16 @@ fn write_like_operand( } fn write_like_keyword(out: &mut W, ilike: bool) { - out.push_str(if ilike { "ILIKE" } else { "LIKE" }); + out.write_str(if ilike { "ILIKE" } else { "LIKE" }); } fn write_like_infix(out: &mut W, negated: bool, ilike: bool) { - out.push_char(' '); + out.write(' '); if negated { - out.push_str("NOT "); + out.write_str("NOT "); } write_like_keyword(out, ilike); - out.push_char(' '); + out.write(' '); } fn write_like_pattern_list( @@ -135,11 +132,11 @@ fn write_like_pattern_list( out: &mut W, patterns: &[ExprInner], wrap_lower: bool, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { if wrap_lower { for (i, pattern) in patterns.iter().enumerate() { if i > 0 { - out.push_str(", "); + out.write_str(", "); } write_like_operand(dialect, out, pattern, true, false)?; } @@ -165,7 +162,7 @@ fn ensure_like_escape_is_supported( syntax: dialect::LikeEscape, escape: Option, native_any: bool, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { if escape.is_none() || like_escape_allowed(syntax, native_any) { return Ok(()); } @@ -177,8 +174,7 @@ fn ensure_like_escape_is_supported( } else { "LIKE ESCAPE is not supported by this dialect" } - ) - .into()) + )) } /// `[NOT] (x LIKE|ILIKE ANY (ARRAY?[…]) [ESCAPE char])` @@ -190,41 +186,42 @@ fn write_like_any_native( ilike: bool, wrap_lower: bool, array: bool, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { if like.negated { - out.push_str("NOT ("); + out.write_str("NOT ("); } write_like_operand(dialect, out, &like.expr, wrap_lower, false)?; - out.push_char(' '); + out.write(' '); write_like_keyword(out, ilike); if array { - out.push_str(" ANY (ARRAY["); + out.write_str(" ANY (ARRAY["); } else { - out.push_str(" ANY ("); + out.write_str(" ANY ("); } write_like_pattern_list(dialect, out, patterns, wrap_lower)?; if array { - out.push_str("])"); + out.write_str("])"); } else { - out.push_char(')'); + out.write(')'); } write_escape(dialect, out, like.escape)?; if like.negated { - out.push_char(')'); + out.write(')'); } Ok(()) } impl WriteSql for Like { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { let (ilike, wrap_lower) = if self.case_sensitive { (false, false) } else { match dialect.like.ilike { dialect::ILikeStrategy::Unsupported => { - return Err( - rapidquery_macros::pyerr!(PyRuntimeError, "ILIKE is not supported by this dialect").into(), - ); + return Err(rapidquery_macros::pyerr!( + PyRuntimeError, + "ILIKE is not supported by this dialect" + )); } dialect::ILikeStrategy::Standard => (true, false), dialect::ILikeStrategy::UseLikeLower => (false, true), @@ -253,17 +250,17 @@ impl WriteSql for Like { } dialect::LikeAny::OrExpansion => { let sep = if self.negated { " AND " } else { " OR " }; - out.push_char('('); + out.write('('); for (i, pattern) in patterns.iter().enumerate() { if i > 0 { - out.push_str(sep); + out.write_str(sep); } write_like_operand(dialect, out, &self.expr, wrap_lower, false)?; write_like_infix(out, self.negated, ilike); write_like_operand(dialect, out, pattern, wrap_lower, true)?; write_escape(dialect, out, self.escape)?; } - out.push_char(')'); + out.write(')'); Ok(()) } }, @@ -312,19 +309,20 @@ impl SimilarTo { } impl WriteSql for SimilarTo { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { if !dialect.like.supports_similar_to { - return Err( - rapidquery_macros::pyerr!(PyRuntimeError, "SIMILAR TO is not supported by this dialect",).into(), - ); + return Err(rapidquery_macros::pyerr!( + PyRuntimeError, + "SIMILAR TO is not supported by this dialect" + )); } self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; - out.push_char(' '); + out.write(' '); if self.negated { - out.push_str("NOT SIMILAR TO "); + out.write_str("NOT SIMILAR TO "); } else { - out.push_str("SIMILAR TO "); + out.write_str("SIMILAR TO "); } self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) } @@ -378,37 +376,38 @@ fn write_regex_call( negated: bool, expr: &ExprInner, pattern: &ExprInner, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { if negated { - out.push_str("NOT "); + out.write_str("NOT "); } - out.push_str(name); - out.push_char('('); + out.write_str(name); + out.write('('); expr.write_sql(dialect, out)?; - out.push_str(", "); + out.write_str(", "); pattern.write_sql(dialect, out)?; - out.push_char(')'); + out.write(')'); Ok(()) } impl WriteSql for RLike { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match dialect.like.regex { - dialect::RegexSpelling::Unsupported => { - Err(rapidquery_macros::pyerr!(PyRuntimeError, "RLIKE is not supported by this dialect",).into()) - } + dialect::RegexSpelling::Unsupported => Err(rapidquery_macros::pyerr!( + PyRuntimeError, + "RLIKE is not supported by this dialect", + )), dialect::RegexSpelling::Standard => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { - out.push_str(" NOT RLIKE "); + out.write_str(" NOT RLIKE "); } else { - out.push_str(" RLIKE "); + out.write_str(" RLIKE "); } self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) } dialect::RegexSpelling::Tilde => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; - out.push_str(if self.negated { " !~ " } else { " ~ " }); + out.write_str(if self.negated { " !~ " } else { " ~ " }); self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) } dialect::RegexSpelling::RegexpLike => { diff --git a/src/expr/enum_/mod.rs b/src/expr/enum_/mod.rs index 5b632d8..099246a 100644 --- a/src/expr/enum_/mod.rs +++ b/src/expr/enum_/mod.rs @@ -14,7 +14,6 @@ pub use value::*; use crate::dialect::PyDialect; use crate::internal::repr::ReprFormatter; -use crate::sql::PrepareResult; use crate::sql::SqlWriter; use crate::sql::WriteSql; @@ -128,15 +127,15 @@ impl ExprInner { out: &mut W, parent_prec: u8, paren_if_equal: bool, - ) -> PrepareResult<()> { + ) -> pyo3::PyResult<()> { let child_prec = self.precedence(); let paren = child_prec > parent_prec || (paren_if_equal && child_prec == parent_prec); if paren { - out.push_char('('); + out.write('('); } self.write_sql(dialect, out)?; if paren { - out.push_char(')'); + out.write(')'); } Ok(()) } @@ -168,7 +167,7 @@ impl ExprInner { impl WriteSql for ExprInner { #[cfg_attr(feature = "nightly", optimize(speed))] - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> pyo3::PyResult<()> { match self { Self::Ident(x) => x.write_sql(dialect, out), Self::Value(x) => x.write_sql(dialect, out), diff --git a/src/expr/enum_/nodes.rs b/src/expr/enum_/nodes.rs index f762b65..3aa8a8e 100644 --- a/src/expr/enum_/nodes.rs +++ b/src/expr/enum_/nodes.rs @@ -8,14 +8,13 @@ use super::UnaryOp; use super::UnarySpacing; use crate::dialect; use crate::internal::repr; -use crate::sql::PrepareResult; use crate::sql::SqlWriter; use crate::sql::WriteSql; use crate::sql::write_separated; #[inline] -fn unsupported(message: &'static str) -> crate::sql::PrepareError { - rapidquery_macros::pyerr!(PyRuntimeError, message).into() +fn unsupported(message: &'static str) -> pyo3::PyErr { + rapidquery_macros::pyerr!(PyRuntimeError, message) } /// Row value. The dialect chooses `ROW`, `struct`, or parentheses. @@ -55,7 +54,7 @@ impl Tuple { } impl WriteSql for Tuple { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { let arity = self.items.len(); if dialect.tuple.syntax != dialect::TupleSyntax::Unsupported { @@ -71,17 +70,17 @@ impl WriteSql for Tuple { return Err(unsupported("tuple values are not supported by this dialect")); } dialect::TupleSyntax::Row => { - out.push_str("ROW("); + out.write_str("ROW("); } dialect::TupleSyntax::Struct => { - out.push_str("struct("); + out.write_str("struct("); } dialect::TupleSyntax::Parentheses => { - out.push_char('('); + out.write('('); } } write_separated(dialect, out, &self.items, ", ")?; - out.push_char(')'); + out.write(')'); Ok(()) } } @@ -123,7 +122,7 @@ impl Unary { } impl WriteSql for Unary { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { if self.op == UnaryOp::BitNot { if !dialect.bitwise.operations.contains(dialect::BitwiseOperations::NOT) { return Err(unsupported("bitwise NOT is not supported by this dialect")); @@ -131,15 +130,15 @@ impl WriteSql for Unary { match dialect.bitwise.syntax { dialect::BitwiseSyntax::Infix => { - out.push_char('~'); + out.write('~'); return self .expr .write_operand(dialect, out, UnaryOp::BitNot.precedence(), true); } dialect::BitwiseSyntax::BitFunction | dialect::BitwiseSyntax::ShiftFunction => { - out.push_str("BITNOT("); + out.write_str("BITNOT("); self.expr.write_sql(dialect, out)?; - out.push_char(')'); + out.write(')'); return Ok(()); } } @@ -147,12 +146,12 @@ impl WriteSql for Unary { let prec = self.op.precedence(); match self.op.spacing() { UnarySpacing::SpacedPrefix => { - out.push_str(self.op.as_sql()); - out.push_char(' '); + out.write_str(self.op.as_sql()); + out.write(' '); self.expr.write_operand(dialect, out, prec, true) } UnarySpacing::GluedPrefix => { - out.push_str(self.op.as_sql()); + out.write_str(self.op.as_sql()); self.expr.write_operand(dialect, out, prec, true) } } @@ -214,13 +213,13 @@ fn write_binary_call( name: &str, left: &ExprInner, right: &ExprInner, -) -> PrepareResult<()> { - out.push_str(name); - out.push_char('('); +) -> pyo3::PyResult<()> { + out.write_str(name); + out.write('('); left.write_sql(dialect, out)?; - out.push_str(", "); + out.write_str(", "); right.write_sql(dialect, out)?; - out.push_char(')'); + out.write(')'); Ok(()) } @@ -241,7 +240,7 @@ fn write_bit_op( bit_fn: &str, shift_fn: &str, missing: &'static str, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { if !dialect.bitwise.operations.contains(op) { return Err(unsupported(missing)); } @@ -265,12 +264,12 @@ fn write_infix( out: &mut W, spelling: &str, paren_arithmetic: bool, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { let prec = bin.op.precedence(); write_infix_side(dialect, out, &bin.left, prec, false, paren_arithmetic)?; - out.push_char(' '); - out.push_str(spelling); - out.push_char(' '); + out.write(' '); + out.write_str(spelling); + out.write(' '); write_infix_side(dialect, out, &bin.right, prec, true, paren_arithmetic) } @@ -282,18 +281,18 @@ fn write_infix_side( prec: u8, paren_if_equal: bool, paren_arithmetic: bool, -) -> PrepareResult<()> { +) -> pyo3::PyResult<()> { if paren_arithmetic && matches!(expr, ExprInner::Binary(b) if is_arithmetic_binary_op(&b.op)) { - out.push_char('('); + out.write('('); expr.write_sql(dialect, out)?; - out.push_char(')'); + out.write(')'); return Ok(()); } expr.write_operand(dialect, out, prec, paren_if_equal) } impl WriteSql for Binary { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match &self.op { BinaryOp::StringConcat => { _ = dialect.string.concat.write_to_result( @@ -411,14 +410,14 @@ impl Is { } impl WriteSql for Is { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { - out.push_str(" IS NOT "); + out.write_str(" IS NOT "); } else { - out.push_str(" IS "); + out.write_str(" IS "); } - out.push_str(self.kind.as_sql()); + out.write_str(self.kind.as_sql()); Ok(()) } } @@ -464,7 +463,7 @@ impl IsDistinctFrom { } impl WriteSql for IsDistinctFrom { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match dialect.operator.is_distinct_from { dialect::IsDistinctFromSpelling::Unsupported => { Err(unsupported("IS DISTINCT FROM is not supported by this dialect")) @@ -472,22 +471,22 @@ impl WriteSql for IsDistinctFrom { dialect::IsDistinctFromSpelling::Standard => { self.left.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { - out.push_str(" IS NOT DISTINCT FROM "); + out.write_str(" IS NOT DISTINCT FROM "); } else { - out.push_str(" IS DISTINCT FROM "); + out.write_str(" IS DISTINCT FROM "); } self.right.write_operand(dialect, out, Self::PRECEDENCE, true) } dialect::IsDistinctFromSpelling::NullSafeEqual => { // `<=>` is null-safe equal, i.e. `IS NOT DISTINCT FROM`. if !self.negated { - out.push_str("NOT ("); + out.write_str("NOT ("); } self.left.write_operand(dialect, out, Self::PRECEDENCE, false)?; - out.push_str(" <=> "); + out.write_str(" <=> "); self.right.write_operand(dialect, out, Self::PRECEDENCE, true)?; if !self.negated { - out.push_char(')'); + out.write(')'); } Ok(()) } @@ -541,28 +540,28 @@ impl InList { } impl WriteSql for InList { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { if self.list.is_empty() { // `x IN ()` is false and `x NOT IN ()` is true, including when `x` is NULL. - dialect.literal.write_boolean(out.sql_mut(), self.negated); + dialect.literal.write_boolean(out, self.negated); return Ok(()); } // SQL Server ranks `AND` above `IN`/`BETWEEN`/`OR`, and those four share // a level evaluated left to right. The parentheses keep the predicate // from being parsed as part of the left operand. - out.push_char('('); + out.write('('); self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { - out.push_str(" NOT IN ("); + out.write_str(" NOT IN ("); } else { - out.push_str(" IN ("); + out.write_str(" IN ("); } write_separated(dialect, out, &self.list, ", ")?; - out.push_char(')'); + out.write(')'); - out.push_char(')'); + out.write(')'); Ok(()) } @@ -611,23 +610,23 @@ impl Between { } impl WriteSql for Between { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { // SQL Server ranks `AND` above `IN`/`BETWEEN`/`OR`, and those four share // a level evaluated left to right. The parentheses keep the predicate // from being parsed as part of the left operand. - out.push_char('('); + out.write('('); self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { - out.push_str(" NOT BETWEEN "); + out.write_str(" NOT BETWEEN "); } else { - out.push_str(" BETWEEN "); + out.write_str(" BETWEEN "); } self.low.write_operand(dialect, out, Self::PRECEDENCE, true)?; - out.push_str(" AND "); + out.write_str(" AND "); self.high.write_operand(dialect, out, Self::PRECEDENCE, true)?; - out.push_char(')'); + out.write(')'); Ok(()) } } @@ -676,19 +675,19 @@ impl Cast { Ok(fmt) } - fn write_call(&self, dialect: &dialect::PyDialect, out: &mut W, name: &str) -> PrepareResult<()> { - out.push_str(name); - out.push_char('('); + fn write_call(&self, dialect: &dialect::PyDialect, out: &mut W, name: &str) -> pyo3::PyResult<()> { + out.write_str(name); + out.write('('); self.expr.write_sql(dialect, out)?; - out.push_str(" AS "); + out.write_str(" AS "); self.data_type_name.write_sql(dialect, out)?; - out.push_char(')'); + out.write(')'); Ok(()) } } impl WriteSql for Cast { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match self.kind { CastKind::Cast => self.write_call(dialect, out, "CAST"), CastKind::TryCast => { @@ -710,9 +709,9 @@ impl WriteSql for Cast { if !dialect.cast.supports_double_colon { return Err(unsupported("double-colon cast is not supported by this dialect")); } - out.push_char('('); + out.write('('); self.expr.write_sql(dialect, out)?; - out.push_str(")::"); + out.write_str(")::"); self.data_type_name.write_sql(dialect, out)?; Ok(()) } @@ -770,49 +769,49 @@ impl Trim { } #[inline] -fn write_trim_function(trim: &Trim, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { +fn write_trim_function(trim: &Trim, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { let name = match trim.where_ { TrimWhere::Both => "TRIM", TrimWhere::Leading => "LTRIM", TrimWhere::Trailing => "RTRIM", }; - out.push_str(name); - out.push_char('('); + out.write_str(name); + out.write('('); trim.expr.write_sql(dialect, out)?; if let Some(characters) = &trim.characters { - out.push_str(", "); + out.write_str(", "); characters.write_sql(dialect, out)?; } - out.push_char(')'); + out.write(')'); Ok(()) } impl WriteSql for Trim { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match dialect.operator.trim { dialect::TrimSpelling::SqlFrom => { - out.push_str("TRIM("); + out.write_str("TRIM("); match (&self.where_, &self.characters) { (TrimWhere::Both, None) => self.expr.write_sql(dialect, out)?, (TrimWhere::Both, Some(characters)) => { characters.write_sql(dialect, out)?; - out.push_str(" FROM "); + out.write_str(" FROM "); self.expr.write_sql(dialect, out)?; } (side, None) => { - out.push_str(side.as_sql()); - out.push_str(" FROM "); + out.write_str(side.as_sql()); + out.write_str(" FROM "); self.expr.write_sql(dialect, out)?; } (side, Some(characters)) => { - out.push_str(side.as_sql()); - out.push_char(' '); + out.write_str(side.as_sql()); + out.write(' '); characters.write_sql(dialect, out)?; - out.push_str(" FROM "); + out.write_str(" FROM "); self.expr.write_sql(dialect, out)?; } } - out.push_char(')'); + out.write(')'); Ok(()) } dialect::TrimSpelling::Function => write_trim_function(self, dialect, out), @@ -863,12 +862,12 @@ impl Collate { } impl WriteSql for Collate { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match dialect.operator.collate { dialect::CollateSpelling::Unsupported => Err(unsupported("COLLATE is not supported by this dialect")), dialect::CollateSpelling::InfixIdent => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; - out.push_str(" COLLATE "); + out.write_str(" COLLATE "); self.collation.write_sql(dialect, out) } dialect::CollateSpelling::FunctionString => { @@ -880,11 +879,11 @@ impl WriteSql for Collate { return Err(unsupported("collation must be a single name")); } }; - out.push_str("COLLATE("); + out.write_str("COLLATE("); self.expr.write_sql(dialect, out)?; - out.push_str(", "); - dialect.string.write_string_literal(out.sql_mut(), body)?; - out.push_char(')'); + out.write_str(", "); + dialect.string.write_string_literal(out, body)?; + out.write(')'); Ok(()) } } @@ -942,23 +941,23 @@ impl Case { } impl WriteSql for Case { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { - out.push_str("CASE"); + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + out.write_str("CASE"); if let Some(operand) = &self.operand { - out.push_char(' '); + out.write(' '); operand.write_sql(dialect, out)?; } for (when, then) in &self.when { - out.push_str(" WHEN "); + out.write_str(" WHEN "); when.write_sql(dialect, out)?; - out.push_str(" THEN "); + out.write_str(" THEN "); then.write_sql(dialect, out)?; } if let Some(else_result) = &self.else_result { - out.push_str(" ELSE "); + out.write_str(" ELSE "); else_result.write_sql(dialect, out)?; } - out.push_str(" END"); + out.write_str(" END"); Ok(()) } } @@ -1005,7 +1004,7 @@ impl Custom { impl WriteSql for Custom { #[cfg_attr(feature = "nightly", optimize(speed))] - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { let bytes = self.sql.as_bytes(); let mut i = 0; let mut start = 0; @@ -1014,19 +1013,21 @@ impl WriteSql for Custom { while i < bytes.len() { if bytes[i] == b'{' { if i + 1 < bytes.len() && bytes[i + 1] == b'{' { - out.push_str(&self.sql[start..i]); - out.push_char('{'); + out.write_str(&self.sql[start..i]); + out.write('{'); i += 2; start = i; continue; } if i + 1 < bytes.len() && bytes[i + 1] == b'}' { - out.push_str(&self.sql[start..i]); + out.write_str(&self.sql[start..i]); if used >= self.args.len() { - return Err(crate::sql::PrepareError::CustomPlaceholders { - expected: used + 1, - got: self.args.len(), - }); + return rapidquery_macros::pyerr!( + Err(PyValueError), + "custom expression has {} placeholders but {} arguments", + used + 1, + self.args.len() + ); } self.args[used].write_sql(dialect, out)?; used += 1; @@ -1035,21 +1036,23 @@ impl WriteSql for Custom { continue; } } else if bytes[i] == b'}' && i + 1 < bytes.len() && bytes[i + 1] == b'}' { - out.push_str(&self.sql[start..i]); - out.push_char('}'); + out.write_str(&self.sql[start..i]); + out.write('}'); i += 2; start = i; continue; } i += 1; } - out.push_str(&self.sql[start..]); + out.write_str(&self.sql[start..]); if used != self.args.len() { - return Err(crate::sql::PrepareError::CustomPlaceholders { - expected: used, - got: self.args.len(), - }); + return rapidquery_macros::pyerr!( + Err(PyValueError), + "custom expression has {} placeholders but {} arguments", + used, + self.args.len() + ); } Ok(()) } diff --git a/src/expr/enum_/value.rs b/src/expr/enum_/value.rs index ff9e149..ca9baad 100644 --- a/src/expr/enum_/value.rs +++ b/src/expr/enum_/value.rs @@ -8,7 +8,6 @@ use crate::internal::alias; use crate::internal::repr; use crate::serializers::SerializerType; use crate::serializers::serializer_repr; -use crate::sql::PrepareResult; use crate::sql::SqlWriter; use crate::sql::WriteSql; @@ -56,8 +55,8 @@ impl Value { } impl WriteSql for Value { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> PrepareResult<()> { - out.push_value(dialect, &self.serializer, &self.object) + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> pyo3::PyResult<()> { + out.write_value(dialect, &self.serializer, &self.object) } } diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 3174e1c..22d48e9 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -4,3 +4,4 @@ pub mod repr; pub mod submodule_ext; pub mod template; pub mod utils; +pub mod writer; diff --git a/src/internal/template.rs b/src/internal/template.rs index 04b968a..7cdb42e 100644 --- a/src/internal/template.rs +++ b/src/internal/template.rs @@ -1,4 +1,6 @@ -//! Allocation-conscious string replacement that writes directly to a [`fmt::Write`]. +//! Allocation-conscious string replacement that writes directly to a [`writer::FastWrite`]. + +use crate::internal::writer; // FIXME: Use `std::str::pattern::Pattern` when it's stable /// A search needle that can be located inside a UTF-8 haystack. @@ -54,9 +56,10 @@ impl ReplacePattern for &str { /// Matches are non-overlapping and searched left to right, same as [`str::replace`]. /// Empty `&str` needles are ignored (no infinite loop, input is copied as-is). #[inline] -pub fn replace_to

(haystack: &str, from: P, to: &str, out: &mut String) +pub fn replace_to(haystack: &str, from: P, to: &str, out: &mut W) where P: ReplacePattern, + W: writer::FastWrite, { // Single ASCII byte → single ASCII byte: still go through the generic loop. // `str::find(char)` already uses memchr, which beats a hand-rolled byte scan @@ -67,8 +70,8 @@ where while let Some((rel, len)) = from.find(&haystack[search_at..]) { let start = search_at + rel; - out.push_str(&haystack[last_end..start]); - out.push_str(to); + out.write_str(&haystack[last_end..start]); + out.write_str(to); last_end = start + len; search_at = last_end; @@ -78,7 +81,7 @@ where } } - out.push_str(&haystack[last_end..]) + out.write_str(&haystack[last_end..]); } #[derive(Debug, Clone, Copy, derive_more::Display)] @@ -101,8 +104,7 @@ struct TemplateMatches { key: &'static str, } -/// A small, allocation-conscious template that expands `{key}` placeholders -/// by writing directly to a [`fmt::Write`]. +/// A small, allocation-conscious template that expands `{key}` placeholders; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Template { tmpl: arcstr::ArcStr, @@ -193,14 +195,11 @@ impl Template { /// /// `fill` is called for each placeholder with its key name and the writer. /// It is responsible for writing the replacement (or nothing). - /// - /// # Panics - /// We're accepting `out` as `std::fmt::Write`, but it should not return `std::fmt::Error`. #[inline] #[cfg_attr(feature = "nightly", optimize(speed))] pub fn write_to_result(&self, out: &mut W, mut fill: F) -> Result<(), E> where - W: std::fmt::Write, + W: writer::FastWrite, F: FnMut(&'static str, &mut W) -> Result<(), E>, { let s = self.tmpl.as_str(); @@ -208,7 +207,7 @@ impl Template { for m in self.matches.iter() { if last < m.start { - out.write_str(&s[last as usize..m.start as usize]).expect(""); + out.write_str(&s[last as usize..m.start as usize]); } fill(m.key, out)?; @@ -216,7 +215,7 @@ impl Template { } if (last as usize) < s.len() { - out.write_str(&s[last as usize..]).expect(""); + out.write_str(&s[last as usize..]); } Ok(()) @@ -226,14 +225,11 @@ impl Template { /// /// `fill` is called for each placeholder with its key name and the writer. /// It is responsible for writing the replacement (or nothing). - /// - /// # Panics - /// We're accepting `out` as `std::fmt::Write`, but it should not return `std::fmt::Error`. #[inline] #[cfg_attr(feature = "nightly", optimize(speed))] pub fn write_to(&self, out: &mut W, mut fill: F) where - W: std::fmt::Write, + W: writer::FastWrite, F: FnMut(&'static str, &mut W), { let s = self.tmpl.as_str(); @@ -241,7 +237,7 @@ impl Template { for m in self.matches.iter() { if last < m.start { - out.write_str(&s[last as usize..m.start as usize]).expect(""); + out.write_str(&s[last as usize..m.start as usize]); } fill(m.key, out); @@ -249,7 +245,7 @@ impl Template { } if (last as usize) < s.len() { - out.write_str(&s[last as usize..]).expect(""); + out.write_str(&s[last as usize..]); } } } diff --git a/src/internal/writer.rs b/src/internal/writer.rs new file mode 100644 index 0000000..bbfe4b5 --- /dev/null +++ b/src/internal/writer.rs @@ -0,0 +1,55 @@ +/// An internal little, chainable, and faster version of `std::fmt::Write` +pub trait FastWrite { + fn write(&mut self, val: char); + fn write_str(&mut self, val: &str); + + #[inline(always)] + fn write_int(&mut self, val: impl itoa::Integer) { + self.write_str(itoa::Buffer::new().format(val)) + } + + #[inline(always)] + fn write_float(&mut self, val: impl zmij::Float) { + self.write_str(zmij::Buffer::new().format(val)) + } + + #[inline(always)] + fn write_if(&mut self, cond: bool, val: char) { + if cond { + self.write(val) + } + } + + #[inline(always)] + fn write_str_if(&mut self, cond: bool, val: &str) { + if cond { + self.write_str(val) + } + } + + #[inline(always)] + fn write_int_if(&mut self, cond: bool, val: impl itoa::Integer) { + if cond { + self.write_int(val) + } + } + + #[inline(always)] + fn write_float_if(&mut self, cond: bool, val: impl zmij::Float) { + if cond { + self.write_float(val) + } + } +} + +impl FastWrite for String { + #[inline(always)] + fn write(&mut self, val: char) { + self.push(val); + } + + #[inline(always)] + fn write_str(&mut self, val: &str) { + self.push_str(val); + } +} diff --git a/src/serializers/functions.rs b/src/serializers/functions.rs index a7f1861..4dd94c0 100644 --- a/src/serializers/functions.rs +++ b/src/serializers/functions.rs @@ -4,7 +4,7 @@ use pyo3::types::PyTzInfoAccess; use crate::dialect; use crate::internal::alias; use crate::internal::utils; -use std::fmt::Write; +use crate::internal::writer; use std::str::FromStr; /// `str(obj)` copied into a Rust `String`. @@ -23,10 +23,10 @@ pub(super) unsafe fn pyobject_string(py: pyo3::Python<'_>, ptr: *mut pyo3::ffi:: } #[inline] -pub(super) unsafe fn write_integer( +pub(super) unsafe fn write_integer( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { if pyo3::ffi::PyLong_Check(ptr) != 1 { return rapidquery_macros::pyerr!( @@ -42,7 +42,7 @@ pub(super) unsafe fn write_integer( if overflow != 0 { // Fallback to `str(obj)` for very long integers. let text = pyobject_string(py, ptr)?; - out.push_str(&text); + out.write_str(&text); return Ok(()); } @@ -50,15 +50,15 @@ pub(super) unsafe fn write_integer( return Err(pyo3::PyErr::fetch(py)); } - let _ = write!(out, "{n}"); + out.write_int(n); Ok(()) } #[inline] -pub(super) unsafe fn write_float( +pub(super) unsafe fn write_float( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { if pyo3::ffi::PyFloat_CheckExact(ptr) == 1 { let v = pyo3::ffi::PyFloat_AsDouble(ptr); @@ -70,15 +70,14 @@ pub(super) unsafe fn write_float( ); } - // `{:?}` so `1.0` stays distinct from `1` - let _ = write!(out, "{v:?}"); + out.write_float(v); return Ok(()); } if pyo3::ffi::PyLong_CheckExact(ptr) == 1 || crate::typeref::check_exact(ptr, crate::typeref::STD_DECIMAL_TYPE) { // Fallback to `str(obj)` for int/decimal.Decimal let text = pyobject_string(py, ptr)?; - out.push_str(&text); + out.write_str(&text); return Ok(()); } @@ -90,11 +89,11 @@ pub(super) unsafe fn write_float( } #[inline] -pub(super) unsafe fn write_bool( +pub(super) unsafe fn write_bool( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { if pyo3::ffi::PyBool_Check(ptr) == 1 { let value = pyo3::ffi::Py_True() == ptr; @@ -110,11 +109,11 @@ pub(super) unsafe fn write_bool( } #[inline] -pub(super) unsafe fn write_str( +pub(super) unsafe fn write_str( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::StringConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { if pyo3::ffi::PyUnicode_Check(ptr) == 1 { return cfg @@ -130,11 +129,11 @@ pub(super) unsafe fn write_str( } #[inline] -pub(super) unsafe fn write_uuid( +pub(super) unsafe fn write_uuid( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { let Some(tmpl) = &cfg.uuid else { return Err(dialect::errors::FormatError::Unsupported("UuidSerializer").into()); @@ -159,17 +158,17 @@ pub(super) unsafe fn write_uuid( tmpl.write_to( out, // no special keys - |_, out| out.push_str(text.as_str()), + |_, out| out.write_str(text.as_str()), ); Ok(()) } #[inline] -pub(super) unsafe fn write_bytes( +pub(super) unsafe fn write_bytes( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { if pyo3::ffi::PyBytes_CheckExact(ptr) == 1 { let buf = pyo3::ffi::PyBytes_AsString(ptr) as *const u8; @@ -187,11 +186,11 @@ pub(super) unsafe fn write_bytes( } } -pub(super) unsafe fn write_bit( +pub(super) unsafe fn write_bit( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { if cfg.bit.is_none() { return Err(dialect::errors::FormatError::Unsupported("BitSerializer").into()); @@ -219,12 +218,12 @@ pub(super) unsafe fn write_bit( } #[inline] -pub(super) unsafe fn write_json( +pub(super) unsafe fn write_json( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, literal_cfg: &dialect::LiteralConfig, string_cfg: &dialect::StringConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { crate::internal::modules::json_dumps( unsafe { pyo3::Bound::from_borrowed_ptr(py, ptr) }, @@ -241,18 +240,18 @@ pub(super) unsafe fn write_json( } #[inline] -pub(super) unsafe fn write_date( +pub(super) unsafe fn write_date( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::TemporalConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { if crate::typeref::check_exact(ptr, crate::typeref::STD_DATE_TYPE) { let val: pyo3::Bound<'_, pyo3::types::PyDate> = pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; let date: chrono::NaiveDate = val.extract()?; cfg.date - .write_to(out, |_, out| date.format("%Y-%m-%d").write_to(out).expect("")); + .write_to(out, |_, out| out.write_str(&date.format("%Y-%m-%d").to_string())); Ok(()) } else { rapidquery_macros::pyerr!( @@ -263,18 +262,18 @@ pub(super) unsafe fn write_date( } } -pub(super) unsafe fn write_time( +pub(super) unsafe fn write_time( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::TemporalConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { if crate::typeref::check_exact(ptr, crate::typeref::STD_TIME_TYPE) { let val: pyo3::Bound<'_, pyo3::types::PyTime> = pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; let time: chrono::NaiveTime = val.extract()?; cfg.date - .write_to(out, |_, out| time.format("%H:%M:%S%.f").write_to(out).expect("")); + .write_to(out, |_, out| out.write_str(&time.format("%H:%M:%S%.f").to_string())); Ok(()) } else { rapidquery_macros::pyerr!( @@ -286,11 +285,11 @@ pub(super) unsafe fn write_time( } #[inline] -pub(super) unsafe fn write_datetime( +pub(super) unsafe fn write_datetime( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::TemporalConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { use chrono::TimeZone; @@ -301,7 +300,7 @@ pub(super) unsafe fn write_datetime( let datetime: chrono::NaiveDateTime = val.extract()?; cfg.datetime_naive.write_to(out, |_, out| { - datetime.format("%Y-%m-%d %H:%M:%S%.f").write_to(out).expect("") + out.write_str(&datetime.format("%Y-%m-%d %H:%M:%S%.f").to_string()) }); return Ok(()); } @@ -309,7 +308,7 @@ pub(super) unsafe fn write_datetime( let datetime: chrono::DateTime = val.extract()?; cfg.datetime_aware.write_to(out, |_, out| { - datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(out).expect("") + out.write_str(&datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").to_string()) }); return Ok(()); } @@ -324,7 +323,7 @@ pub(super) unsafe fn write_datetime( return match chrono::Utc.timestamp_opt(num, 0) { chrono::offset::LocalResult::Single(datetime) => { cfg.datetime_aware.write_to(out, |_, out| { - datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(out).expect("") + out.write_str(&datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").to_string()) }); return Ok(()); } @@ -341,7 +340,7 @@ pub(super) unsafe fn write_datetime( return match chrono::Utc.timestamp_opt(num.trunc() as i64, (num.fract() * 1_000_000_000.0) as u32) { chrono::offset::LocalResult::Single(datetime) => { cfg.datetime_aware.write_to(out, |_, out| { - datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").write_to(out).expect("") + out.write_str(&datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").to_string()) }); return Ok(()); } @@ -360,11 +359,11 @@ pub(super) unsafe fn write_datetime( ) } -pub(super) unsafe fn write_inet( +pub(super) unsafe fn write_inet( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { let Some(tmpl) = &cfg.inet else { return Err(dialect::errors::FormatError::Unsupported("InetSerializer").into()); @@ -376,15 +375,15 @@ pub(super) unsafe fn write_inet( let body = net.to_string(); - let _ = tmpl.write_to_result(out, |_, out| out.write_str(&body)); + tmpl.write_to(out, |_, out| out.write_str(&body)); Ok(()) } -pub(super) unsafe fn write_macaddr( +pub(super) unsafe fn write_macaddr( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { let Some(tmpl) = &cfg.inet else { return Err(dialect::errors::FormatError::Unsupported("InetSerializer").into()); @@ -396,16 +395,16 @@ pub(super) unsafe fn write_macaddr( let body = addr.to_string(); - let _ = tmpl.write_to_result(out, |_, out| out.write_str(&body)); + tmpl.write_to(out, |_, out| out.write_str(&body)); Ok(()) } -pub(super) unsafe fn write_array( +pub(super) unsafe fn write_array( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, dialect: &dialect::PyDialect, element: &super::inner::SerializerType, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { let Some(tmpl) = &dialect.literal.array else { return Err(dialect::errors::FormatError::Unsupported("ArraySerializer").into()); @@ -431,9 +430,8 @@ pub(super) unsafe fn write_array( // no special keys |_, out| { for i in 0..len { - if i > 0 { - out.push_str(", "); - } + out.write_str_if(i > 0, ", "); + let item = getter(ptr, i); if item.is_null() { return Err(pyo3::PyErr::fetch(py)); @@ -446,13 +444,13 @@ pub(super) unsafe fn write_array( ) } -pub(super) unsafe fn write_custom( +pub(super) unsafe fn write_custom( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, dialect: &dialect::PyDialect, function: &pyo3::Py, object: &alias::BoundObject<'_>, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { let _ = ptr; @@ -465,6 +463,6 @@ pub(super) unsafe fn write_custom( ); } - out.push_str(utils::borrow_py_str_buffer(py, output.as_ptr())?); + out.write_str(utils::borrow_py_str_buffer(py, output.as_ptr())?); Ok(()) } diff --git a/src/serializers/inner.rs b/src/serializers/inner.rs index cdd9c5d..518f79e 100644 --- a/src/serializers/inner.rs +++ b/src/serializers/inner.rs @@ -2,6 +2,7 @@ use crate::dialect::PyDialect; use crate::internal::alias; use crate::internal::modules; use crate::internal::utils; +use crate::internal::writer; /// A thin enum which specifies the serializer type. /// @@ -154,14 +155,12 @@ impl SerializerType { // * -> Unspecified Ok(Self::Unspecified) } -} -impl SerializerType { - pub fn serialize( + pub fn serialize( &self, dialect: &PyDialect, object: &alias::BoundObject<'_>, - out: &mut String, + out: &mut W, ) -> pyo3::PyResult<()> { let py = object.py(); let ptr = object.as_ptr(); @@ -184,7 +183,7 @@ impl SerializerType { Self::Array(element) => super::functions::write_array(py, ptr, dialect, element, out), Self::Unspecified => { let text = super::functions::pyobject_string(py, ptr)?; - out.push_str(&text); + out.write_str(&text); Ok(()) } Self::Custom(s) => super::functions::write_custom(py, ptr, dialect, s, object, out), diff --git a/src/sql.rs b/src/sql.rs index 6f99fd6..cdc8ff5 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,139 +1,29 @@ //! SQL text buffers. //! -//! [`WriteSql`] walks a node into a [`SqlWriter`]. [`BufferedSql`] inlines values. -//! [`ParameterizedSql`] binds them as placeholders. - -// FIXME: revert change: move this file into root folder - -use std::fmt::Write; - -use pyo3::types::PyAnyMethods; +//! [`WriteSql`] walks a node into a [`SqlWriter`]. [`Buffer`] inlines values. +//! [`Parameterized`] binds them as placeholders. use crate::dialect; use crate::internal::alias; +use crate::internal::writer::FastWrite; use crate::serializers::SerializerType; -/// Error produced while building SQL. -#[derive(Debug)] -pub enum PrepareError { - /// A [`std::fmt::Write`] implementation returned [`std::fmt::Error`]. - Write, - /// An error that originated in Python (e.g. from `.extract()`). - Python(pyo3::PyErr), - /// A `Custom` template had a different number of `{}` slots than args. - CustomPlaceholders { expected: usize, got: usize }, -} - -impl std::fmt::Display for PrepareError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Write => f.write_str("failed to write SQL"), - Self::Python(err) => write!(f, "{err}"), - Self::CustomPlaceholders { expected, got } => { - write!(f, "custom expression has {expected} placeholders but {got} arguments") - } - } - } -} - -impl std::error::Error for PrepareError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Write | Self::CustomPlaceholders { .. } => None, - Self::Python(err) => Some(err), - } - } -} - -impl From for PrepareError { - fn from(_: std::fmt::Error) -> Self { - Self::Write - } -} - -impl From for PrepareError { - fn from(err: dialect::errors::FormatError) -> Self { - match err { - dialect::errors::FormatError::Write => Self::Write, - other => Self::Python(other.into()), - } - } -} - -impl From for pyo3::PyErr { - fn from(err: dialect::errors::FormatError) -> Self { - match err { - dialect::errors::FormatError::Write => { - rapidquery_macros::pyerr!(PyRuntimeError, "failed to write SQL") - } - dialect::errors::FormatError::NulInString => { - rapidquery_macros::pyerr!(PyValueError, "NUL is not allowed in a string literal") - } - dialect::errors::FormatError::Unsupported(name) => { - rapidquery_macros::pyerr!(PyNotImplementedError, "this dialect does not support {}", name) - } - } - } -} - -impl From for PrepareError { - fn from(err: pyo3::PyErr) -> Self { - Self::Python(err) - } -} - -impl From for pyo3::PyErr { - fn from(err: PrepareError) -> Self { - match err { - PrepareError::Python(err) => err, - PrepareError::Write => { - rapidquery_macros::pyerr!(PyRuntimeError, "failed to write SQL") - } - PrepareError::CustomPlaceholders { expected, got } => { - rapidquery_macros::pyerr!( - PyValueError, - "custom expression has {} placeholders but {} arguments", - expected, - got - ) - } - } - } -} - -pub type PrepareResult = Result; -pub type PrepareParams = smallvec::SmallVec<[alias::PyObject; 1]>; +pub type PrepareParams = Vec; /// Destination for [`WriteSql`]. /// -/// [`BufferedSql`] inlines values. [`ParameterizedSql`] binds them. Other methods +/// [`Buffered`] inlines values. [`Parameterized`] binds them. Other methods /// only append text. -pub trait SqlWriter: std::fmt::Write { - fn sql_mut(&mut self) -> &mut String; - - fn push_str(&mut self, s: &str) -> &mut Self; - - #[inline] - fn push_str_if(&mut self, cond: bool, s: &str) -> &mut Self { - if cond { self.push_str(s) } else { self } - } - - fn push_char(&mut self, c: char) -> &mut Self; - - #[inline] - fn push_char_if(&mut self, cond: bool, c: char) -> &mut Self { - if cond { self.push_char(c) } else { self } - } - +pub trait SqlWriter: FastWrite { /// Raw (`SqlBuf`): `None` → `NULL`, else `serializer.serialize` into the /// SQL buffer. Prepared: bind `object` as a placeholder; `serializer` is /// unused. - fn push_value( + fn write_value( &mut self, dialect: &dialect::PyDialect, serializer: &SerializerType, object: &alias::PyObject, - ) -> PrepareResult<()>; + ) -> pyo3::PyResult<()>; } /// SQL buffer that inlines values. It does not keep a parameter list. @@ -143,16 +33,16 @@ pub trait SqlWriter: std::fmt::Write { /// /// ``` /// pyo3::Python::attach(|py| { -/// let buf = rapidquery::internal::sql::BufferedSql::new(py); +/// let buf = rapidquery::internal::sql::Buffered::new(py); /// assert_eq!(buf.as_str(), ""); /// }); /// ``` -pub struct BufferedSql<'py> { +pub struct Buffer<'py> { py: pyo3::Python<'py>, sql: String, } -impl<'py> BufferedSql<'py> { +impl<'py> Buffer<'py> { #[inline] pub fn new(py: pyo3::Python<'py>) -> Self { Self { py, sql: String::new() } @@ -171,41 +61,34 @@ impl<'py> BufferedSql<'py> { self.py } - #[inline] - pub fn as_str(&self) -> &str { - &self.sql - } - #[inline] pub fn into_string(self) -> String { self.sql } } -impl<'py> SqlWriter for BufferedSql<'py> { +impl<'py> FastWrite for Buffer<'py> { #[inline] - fn sql_mut(&mut self) -> &mut String { - &mut self.sql + fn write(&mut self, val: char) { + self.sql.write(val); } #[inline] - fn push_str(&mut self, s: &str) -> &mut Self { - self.sql.push_str(s); - self + fn write_str(&mut self, val: &str) { + self.sql.write_str(val); } +} +impl<'py> SqlWriter for Buffer<'py> { #[inline] - fn push_char(&mut self, c: char) -> &mut Self { - self.sql.push(c); - self - } - - fn push_value( + fn write_value( &mut self, dialect: &dialect::PyDialect, serializer: &SerializerType, object: &alias::PyObject, - ) -> PrepareResult<()> { + ) -> pyo3::PyResult<()> { + use pyo3::types::PyAnyMethods; + let bound = object.bind(self.py); if bound.is_none() { self.sql.push_str("NULL"); @@ -216,20 +99,6 @@ impl<'py> SqlWriter for BufferedSql<'py> { } } -impl<'py> std::fmt::Write for BufferedSql<'py> { - #[inline] - fn write_str(&mut self, s: &str) -> std::fmt::Result { - self.sql.push_str(s); - Ok(()) - } - - #[inline] - fn write_char(&mut self, c: char) -> std::fmt::Result { - self.sql.push(c); - Ok(()) - } -} - /// SQL text plus the Python objects bound for each placeholder. /// /// [`std::fmt::Write`] appends text. [`Self::push_param`] appends a placeholder @@ -237,19 +106,19 @@ impl<'py> std::fmt::Write for BufferedSql<'py> { /// /// ``` /// pyo3::Python::attach(|py| { -/// let sql = rapidquery::internal::sql::ParameterizedSql::new(py); +/// let sql = rapidquery::internal::sql::Parameterized::new(py); /// assert_eq!(sql.sql(), ""); /// assert!(sql.params().is_empty()); /// }); /// ``` -pub struct ParameterizedSql<'py> { +pub struct Parameterized<'py> { py: pyo3::Python<'py>, sql: String, params: PrepareParams, next_id: usize, } -impl<'py> ParameterizedSql<'py> { +impl<'py> Parameterized<'py> { #[inline] pub fn new(py: pyo3::Python<'py>) -> Self { Self { @@ -275,29 +144,19 @@ impl<'py> ParameterizedSql<'py> { self.py } - #[inline] - pub fn sql(&self) -> &str { - self.sql.as_str() - } - - #[inline] - pub fn params(&self) -> &PrepareParams { - &self.params - } - #[inline] pub fn into_parts(self) -> (String, PrepareParams) { (self.sql, self.params) } } -impl ParameterizedSql<'_> { +impl Parameterized<'_> { /// Register `param` and append its placeholder to the SQL in one step. #[inline] pub fn push_param(&mut self, dialect: &dialect::PyDialect, param: alias::PyObject) -> &mut Self { dialect .placeholder - .write_to(&mut self.sql, |_, out| write!(out, "{}", self.next_id).expect("")); + .write_to(&mut self.sql, |_, out| out.write_int(self.next_id)); self.next_id += 1; self.params.push(param); @@ -305,37 +164,32 @@ impl ParameterizedSql<'_> { } } -impl SqlWriter for ParameterizedSql<'_> { +impl<'py> FastWrite for Parameterized<'py> { #[inline] - fn sql_mut(&mut self) -> &mut String { - &mut self.sql + fn write(&mut self, val: char) { + self.sql.write(val); } #[inline] - fn push_str(&mut self, s: &str) -> &mut Self { - self.sql.push_str(s); - self - } - - #[inline] - fn push_char(&mut self, c: char) -> &mut Self { - self.sql.push(c); - self + fn write_str(&mut self, val: &str) { + self.sql.write_str(val); } +} +impl SqlWriter for Parameterized<'_> { #[inline] - fn push_value( + fn write_value( &mut self, dialect: &dialect::PyDialect, _serializer: &SerializerType, object: &alias::PyObject, - ) -> PrepareResult<()> { + ) -> pyo3::PyResult<()> { self.push_param(dialect, object.clone_ref(self.py)); Ok(()) } } -impl std::fmt::Write for ParameterizedSql<'_> { +impl std::fmt::Write for Parameterized<'_> { #[inline] fn write_str(&mut self, s: &str) -> std::fmt::Result { self.sql.push_str(s); @@ -351,7 +205,7 @@ impl std::fmt::Write for ParameterizedSql<'_> { /// Render `self` into `out`. The writer chooses literal vs placeholder for values. pub trait WriteSql { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> PrepareResult<()>; + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()>; } /// Render `stmt` as raw SQL (values inlined as literals). @@ -359,8 +213,8 @@ pub fn write_sql<'py, T: WriteSql>( py: pyo3::Python<'py>, stmt: &T, dialect: &dialect::PyDialect, -) -> PrepareResult { - let mut buf = BufferedSql::with_capacity(py, 256); +) -> pyo3::PyResult { + let mut buf = Buffer::with_capacity(py, 256); stmt.write_sql(dialect, &mut buf)?; Ok(buf.into_string()) } @@ -370,8 +224,8 @@ pub fn prepare<'py, T: WriteSql>( py: pyo3::Python<'py>, stmt: &T, dialect: &dialect::PyDialect, -) -> PrepareResult<(String, PrepareParams)> { - let mut context = ParameterizedSql::with_capacity(py, 256); +) -> pyo3::PyResult<(String, PrepareParams)> { + let mut context = Parameterized::with_capacity(py, 256); stmt.write_sql(dialect, &mut context)?; Ok(context.into_parts()) } @@ -386,7 +240,7 @@ pub fn write_separated<'a, T, W>( out: &mut W, items: impl IntoIterator, separator: &str, -) -> PrepareResult<()> +) -> pyo3::PyResult<()> where T: WriteSql + 'a, W: SqlWriter, @@ -394,7 +248,7 @@ where let mut first = true; for item in items { if !first { - out.push_str(separator); + out.write_str(separator); } first = false; item.write_sql(dialect, out)?; From 07b1a56d41673ce649a811bbf990db0ef681e2db Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Sat, 24 Oct 2026 13:03:02 +0330 Subject: [PATCH 06/11] Remove old dialect files --- dialect/base.rs | 198 ---------- dialect/builtin.rs | 605 ----------------------------- dialect/config.rs | 699 --------------------------------- dialect/errors.rs | 31 -- dialect/mod.rs | 86 ----- dialect/tests.rs | 937 --------------------------------------------- 6 files changed, 2556 deletions(-) delete mode 100644 dialect/base.rs delete mode 100644 dialect/builtin.rs delete mode 100644 dialect/config.rs delete mode 100644 dialect/errors.rs delete mode 100644 dialect/mod.rs delete mode 100644 dialect/tests.rs diff --git a/dialect/base.rs b/dialect/base.rs deleted file mode 100644 index 55e509f..0000000 --- a/dialect/base.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::internal::utils; - -use super::config::*; - -/// SQL dialect: quoting, literals, and which syntax is legal. -/// -/// Use a builtin such as `PostgreSql`, or build one from `OPT_*` flags. -#[rapidquery_macros::pyclass("rapidquery.dialect.Dialect", eq, hash)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct PyDialect(pub DialectConfig); - -impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyDialect { - type Error = pyo3::PyErr; - - fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { - unsafe { - // SAFETY: `obj` is a borrowed `&Bound`, so `as_ptr()` is a - // live `PyObject*`. - if crate::typeref::check_exact(obj.as_ptr(), crate::typeref::DIALECT_TYPE) { - let cast = obj.cast_unchecked::(); - return Ok(Self(cast.get().0)); - } - - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected Dialect, got {}", - utils::get_type_name(obj.py(), obj.as_ptr()) - ) - } - } -} - -// FIXME: Set dialect default values as disabled/unsupported when possible, -// otherwise set a traditional one. So developer can predict the values -// without needing to specify all arguments. -rapidquery_macros::pydialect_constructor!( - placeholder_character: char = '?', - - #[enum_(Simple, Numbered)] - placeholder_syntax: PlaceholderSyntax = PlaceholderSyntax::default(), - - #[bit(SCALAR, ANY)] - like_escape: LikeEscape = LikeEscape::SCALAR.bits(), - - #[enum_(OrExpansion, ArrayConstructor, PatternList)] - like_any: LikeAny = LikeAny::default(), - - #[enum_(Standard, UseLikeLower, UseLike)] - ilike_expression: ILikeExpression = ILikeExpression::default(), - - #[enum_(Unsupported, Standard, Tilde, RegexpLike, RegexpContains, RegexpMatches)] - rlike_expression: RLikeExpression = RLikeExpression::default(), - - supports_similar_to: bool = false, - - #[enum_(Unsupported, InsideParens, Postfix)] - null_treatment: NullTreatment = NullTreatment::default(), - - #[enum_(Unsupported, Filter, FilterWhere, InsideWhere)] - function_filter: FunctionFilter = FunctionFilter::default(), - - #[bit(AND, OR, NOT, SHL, SHR)] - bitwise_operations: BitwiseOperations = BitwiseOperations::AND - .union(BitwiseOperations::OR) - .union(BitwiseOperations::NOT) - .union(BitwiseOperations::SHL) - .union(BitwiseOperations::SHR) - .bits(), - - #[enum_(Infix, BitFunction, ShiftFunction)] - bitwise_call_syntax: BitwiseCallSyntax = BitwiseCallSyntax::default(), - - #[enum_(Unsupported, Caret, Hash, Function, BitFunction)] - bitwise_xor_spelling: BitwiseXorSpelling = BitwiseXorSpelling::default(), - - #[enum_(DoubleQuote, Backtick, SquareBracket)] - identifier_format: IdentifierFormat = IdentifierFormat::default(), - - #[enum_(Double, Backslash)] - identifier_escape: IdentifierEscape = IdentifierEscape::default(), - - #[enum_(SingleQuote, PrefixBigE, PrefixBigN)] - string_format: StringFormat = StringFormat::default(), - - #[enum_(Quote, BackslashHex, BackslashMysql, BackslashUnicode, BackslashHexZero, BackslashUnicodeQuote)] - string_escape: StringEscape = StringEscape::default(), - - #[enum_(Pipes, Plus, ConcatFunction)] - string_concat: StringConcat = StringConcat::default(), - - #[enum_(Unsupported, TryCast, TryCastWord, SafeCast)] - cast_try_cast: TryCast = TryCast::default(), - - cast_double_colon: bool = false, - - #[enum_(Unsupported, Row, Struct, Parentheses)] - tuple_syntax: TupleSyntax = TupleSyntax::default(), - - #[bit(EMPTY, ONE)] - tuple_arity: TupleArity = TupleArity::EMPTY.union(TupleArity::ONE).bits(), - - #[enum_(Quoted, Keyword)] - date_format: DateFormat = DateFormat::default(), - - #[enum_(Quoted, Keyword)] - time_format: TimeFormat = TimeFormat::default(), - - #[enum_(Quoted, Timestamptz, WithTimezone)] - timestamp_format: TimestampFormat = TimestampFormat::default(), - - #[enum_(Keyword, Integer)] - bool_format: BoolFormat = BoolFormat::default(), - - #[enum_(Unsupported, PrefixBigB, PrefixSmallB, PostfixBigB)] - bit_format: BitFormat = BitFormat::default(), - - #[enum_(PrefixBigX, PrefixZeroX, SlashXBytea, PrefixBigBSlashX, HexToRaw, Unhex, ToVarbyte, PostfixXbv, ToBinary, SlashXBlob)] - bytes_format: BytesFormat = BytesFormat::default(), - - #[enum_(Unsupported, Quoted, Cast)] - inet_format: InetFormat = InetFormat::default(), - - #[enum_(Unsupported, Quoted, Cast)] - macaddr_format: MacAddrFormat = MacAddrFormat::default(), - - #[enum_(Quoted, UuidKeyword, CastUuid)] - uuid_format: UuidFormat = UuidFormat::default(), - - #[enum_(Quoted, JsonKeyword, ParseJson)] - json_format: JsonFormat = JsonFormat::default(), - - #[enum_(Unsupported, Constructor, List, Function)] - array_format: ArrayFormat = ArrayFormat::default(), - - #[enum_(Percent, Function, Infix)] - modulo_spelling: ModuloSpelling = ModuloSpelling::default(), - - #[enum_(Unsupported, Standard, NullSafeEqual)] - is_distinct_from_spelling: IsDistinctFromSpelling = IsDistinctFromSpelling::default(), - - #[enum_(SqlFrom, Function, SpacesOnly)] - trim_spelling: TrimSpelling = TrimSpelling::default(), - - #[enum_(Unsupported, InfixIdent, FunctionString)] - collate_spelling: CollateSpelling = CollateSpelling::default(), - - #[bit(UNSUPPORTED, IF_NOT_EXISTS, EMBEDDED_DDL, IS_DATABASE, DROP_CASCADE, DROP_RESTRICT)] - schema_features: SchemaFeatures = SchemaFeatures::empty().bits(), - - #[enum_(Unsupported, Optional)] - schema_authorization_style: SchemaAuthorizationStyle = SchemaAuthorizationStyle::default(), - - #[enum_(Unsupported, LocationKeyword, ManagedLocationKeyword, ExternalVolume)] - schema_location_style: SchemaLocationStyle = SchemaLocationStyle::default(), -); - -#[pyo3::pymethods] -impl PyDialect { - /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). - fn write_boolean(&self, value: bool) -> pyo3::PyResult { - let mut buf = String::new(); - self.0.write_boolean(&mut buf, value)?; - Ok(buf) - } - - /// Escape `ident` and wrap it in this dialect’s identifier delimiters. - fn write_identifier(&self, value: &str) -> pyo3::PyResult { - let mut buf = String::new(); - let _ = self.0.write_identifier(&mut buf, value); - Ok(buf) - } - - /// Escape `value` and wrap it as a complete string literal. - fn write_string_literal(&self, value: &str) -> pyo3::PyResult { - let mut buf = String::new(); - self.0.write_string_literal(&mut buf, value)?; - Ok(buf) - } - - /// Write a bit-string ('0'/'1' characters) as a native literal. - fn write_bits(&self, value: &str) -> pyo3::PyResult { - if value.chars().any(|c| c != '0' && c != '1') { - return rapidquery_macros::pyerr!(Err(PyValueError), "invalid input: {}", value); - } - - let mut buf = String::new(); - self.0.write_bits(&mut buf, value)?; - Ok(buf) - } - - /// Write bytes as a native blob/bytea/varbyte literal. - fn write_bytes(&self, value: &[u8]) -> pyo3::PyResult { - let mut buf = String::new(); - self.0.write_bytes(&mut buf, value)?; - Ok(buf) - } -} diff --git a/dialect/builtin.rs b/dialect/builtin.rs deleted file mode 100644 index ee0f112..0000000 --- a/dialect/builtin.rs +++ /dev/null @@ -1,605 +0,0 @@ -use super::base; -use super::config; - -/// BigQuery SQL dialect. -pub const BIGQUERY_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '?', - placeholder_syntax: config::PlaceholderSyntax::Simple, - null_treatment: config::NullTreatment::InsideParens, - like_escape: config::LikeEscape::empty(), - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::UseLikeLower, - rlike_expression: config::RLikeExpression::RegexpContains, - string_concat: config::StringConcat::Pipes, - bitwise_xor_spelling: config::BitwiseXorSpelling::Caret, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Standard, - cast_try_cast: config::TryCast::SafeCast, - cast_double_colon: false, - trim_spelling: config::TrimSpelling::Function, - collate_spelling: config::CollateSpelling::FunctionString, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Unsupported, - tuple_arity: config::TupleArity::empty(), - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::Backtick, - identifier_escape: config::IdentifierEscape::Backslash, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::BackslashHex, - bytes_format: config::BytesFormat::PrefixBigBSlashX, - function_filter: config::FunctionFilter::InsideWhere, - array_format: config::ArrayFormat::Constructor, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Keyword, - timestamp_format: config::TimestampFormat::WithTimezone, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::JsonKeyword, - supports_similar_to: false, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS, - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); - -/// Databricks SQL dialect. -pub const DATABRICKS_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '?', - placeholder_syntax: config::PlaceholderSyntax::Simple, - null_treatment: config::NullTreatment::Postfix, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::PatternList, - ilike_expression: config::ILikeExpression::Standard, - rlike_expression: config::RLikeExpression::Standard, - string_concat: config::StringConcat::Pipes, - bitwise_xor_spelling: config::BitwiseXorSpelling::Caret, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Standard, - cast_try_cast: config::TryCast::TryCast, - cast_double_colon: true, - trim_spelling: config::TrimSpelling::SqlFrom, - collate_spelling: config::CollateSpelling::InfixIdent, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Struct, - tuple_arity: config::TupleArity::ONE, - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::Backtick, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::BackslashUnicodeQuote, - bytes_format: config::BytesFormat::PrefixBigX, - function_filter: config::FunctionFilter::FilterWhere, - array_format: config::ArrayFormat::Unsupported, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::WithTimezone, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: false, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS, - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::LocationKeyword, -}); - -/// DuckDB SQL dialect. -pub const DUCKDB_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '$', - placeholder_syntax: config::PlaceholderSyntax::Numbered, - null_treatment: config::NullTreatment::InsideParens, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::Standard, - rlike_expression: config::RLikeExpression::RegexpMatches, - string_concat: config::StringConcat::Pipes, - bitwise_xor_spelling: config::BitwiseXorSpelling::Function, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Standard, - cast_try_cast: config::TryCast::TryCast, - cast_double_colon: true, - trim_spelling: config::TrimSpelling::Function, - collate_spelling: config::CollateSpelling::InfixIdent, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Row, - tuple_arity: config::TupleArity::ONE.union(config::TupleArity::EMPTY), - bit_format: config::BitFormat::PrefixBigB, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::DoubleQuote, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::Quote, - bytes_format: config::BytesFormat::SlashXBlob, - function_filter: config::FunctionFilter::Filter, - array_format: config::ArrayFormat::List, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Keyword, - timestamp_format: config::TimestampFormat::Timestamptz, - inet_format: config::InetFormat::Cast, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::UuidKeyword, - json_format: config::JsonFormat::Quoted, - supports_similar_to: true, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS, - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); - -/// Hive SQL dialect. -pub const HIVE_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '?', - placeholder_syntax: config::PlaceholderSyntax::Simple, - null_treatment: config::NullTreatment::Unsupported, - like_escape: config::LikeEscape::empty(), - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::UseLikeLower, - rlike_expression: config::RLikeExpression::Standard, - string_concat: config::StringConcat::Pipes, - bitwise_xor_spelling: config::BitwiseXorSpelling::Caret, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::NullSafeEqual, - cast_try_cast: config::TryCast::Unsupported, - cast_double_colon: false, - trim_spelling: config::TrimSpelling::SpacesOnly, - collate_spelling: config::CollateSpelling::Unsupported, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Struct, - tuple_arity: config::TupleArity::ONE, - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::Backtick, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::BackslashUnicode, - bytes_format: config::BytesFormat::Unhex, - function_filter: config::FunctionFilter::Unsupported, - array_format: config::ArrayFormat::Function, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::WithTimezone, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: false, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS.union(config::SchemaFeatures::IS_DATABASE), - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::LocationKeyword, -}); - -/// Microsoft SQL Server dialect. -pub const MSSQL_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '?', - placeholder_syntax: config::PlaceholderSyntax::Simple, - null_treatment: config::NullTreatment::Postfix, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::UseLike, - rlike_expression: config::RLikeExpression::Unsupported, - string_concat: config::StringConcat::Plus, - bitwise_xor_spelling: config::BitwiseXorSpelling::Caret, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Standard, - cast_try_cast: config::TryCast::TryCast, - cast_double_colon: false, - trim_spelling: config::TrimSpelling::SqlFrom, - collate_spelling: config::CollateSpelling::InfixIdent, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Unsupported, - tuple_arity: config::TupleArity::empty(), - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Integer, - identifier_format: config::IdentifierFormat::SquareBracket, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::PrefixBigN, - string_escape: config::StringEscape::Quote, - bytes_format: config::BytesFormat::PrefixZeroX, - function_filter: config::FunctionFilter::Unsupported, - array_format: config::ArrayFormat::Unsupported, - date_format: config::DateFormat::Quoted, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::Quoted, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: false, - schema_features: config::SchemaFeatures::EMBEDDED_DDL, - schema_authorization_style: config::SchemaAuthorizationStyle::Optional, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); - -/// MySQL dialect. -pub const MYSQL_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '?', - placeholder_syntax: config::PlaceholderSyntax::Simple, - null_treatment: config::NullTreatment::Unsupported, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::UseLike, - rlike_expression: config::RLikeExpression::Standard, - string_concat: config::StringConcat::ConcatFunction, - bitwise_xor_spelling: config::BitwiseXorSpelling::Caret, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::NullSafeEqual, - cast_try_cast: config::TryCast::Unsupported, - cast_double_colon: false, - trim_spelling: config::TrimSpelling::SqlFrom, - collate_spelling: config::CollateSpelling::InfixIdent, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Row, - tuple_arity: config::TupleArity::ONE, - bit_format: config::BitFormat::PrefixSmallB, - bool_format: config::BoolFormat::Integer, - identifier_format: config::IdentifierFormat::Backtick, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::BackslashMysql, - bytes_format: config::BytesFormat::PrefixBigX, - function_filter: config::FunctionFilter::Unsupported, - array_format: config::ArrayFormat::Unsupported, - date_format: config::DateFormat::Quoted, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::Quoted, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: false, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS.union(config::SchemaFeatures::IS_DATABASE), - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); - -/// Oracle SQL dialect. -pub const ORACLE_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: ':', - placeholder_syntax: config::PlaceholderSyntax::Numbered, - null_treatment: config::NullTreatment::Postfix, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::UseLikeLower, - rlike_expression: config::RLikeExpression::RegexpLike, - string_concat: config::StringConcat::Pipes, - modulo_spelling: config::ModuloSpelling::Function, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Unsupported, - cast_try_cast: config::TryCast::Unsupported, - cast_double_colon: false, - trim_spelling: config::TrimSpelling::SqlFrom, - collate_spelling: config::CollateSpelling::InfixIdent, - bitwise_xor_spelling: config::BitwiseXorSpelling::Unsupported, - bitwise_operations: config::BitwiseOperations::AND, - bitwise_call_syntax: config::BitwiseCallSyntax::BitFunction, - tuple_syntax: config::TupleSyntax::Unsupported, - tuple_arity: config::TupleArity::empty(), - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Integer, - identifier_format: config::IdentifierFormat::DoubleQuote, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::Quote, - bytes_format: config::BytesFormat::HexToRaw, - function_filter: config::FunctionFilter::FilterWhere, - array_format: config::ArrayFormat::Unsupported, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::WithTimezone, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: false, - schema_features: config::SchemaFeatures::EMBEDDED_DDL, - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); - -/// PostgreSQL dialect. -pub const POSTGRESQL_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '$', - placeholder_syntax: config::PlaceholderSyntax::Numbered, - null_treatment: config::NullTreatment::Unsupported, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::ArrayConstructor, - ilike_expression: config::ILikeExpression::Standard, - rlike_expression: config::RLikeExpression::Tilde, - string_concat: config::StringConcat::Pipes, - bitwise_xor_spelling: config::BitwiseXorSpelling::Hash, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Standard, - cast_try_cast: config::TryCast::Unsupported, - trim_spelling: config::TrimSpelling::SqlFrom, - collate_spelling: config::CollateSpelling::InfixIdent, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Row, - tuple_arity: config::TupleArity::EMPTY.union(config::TupleArity::ONE), - bit_format: config::BitFormat::PrefixBigB, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::DoubleQuote, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::PrefixBigE, - string_escape: config::StringEscape::BackslashHex, - bytes_format: config::BytesFormat::SlashXBytea, - function_filter: config::FunctionFilter::FilterWhere, - array_format: config::ArrayFormat::Constructor, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Keyword, - timestamp_format: config::TimestampFormat::Timestamptz, - inet_format: config::InetFormat::Cast, - macaddr_format: config::MacAddrFormat::Cast, - uuid_format: config::UuidFormat::CastUuid, - json_format: config::JsonFormat::Quoted, - supports_similar_to: true, - cast_double_colon: true, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS.union(config::SchemaFeatures::EMBEDDED_DDL), - schema_authorization_style: config::SchemaAuthorizationStyle::Optional, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); - -/// Amazon Redshift dialect. -pub const REDSHIFT_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '$', - placeholder_syntax: config::PlaceholderSyntax::Numbered, - null_treatment: config::NullTreatment::Postfix, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::Standard, - rlike_expression: config::RLikeExpression::Tilde, - string_concat: config::StringConcat::Pipes, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Unsupported, - cast_try_cast: config::TryCast::TryCast, - trim_spelling: config::TrimSpelling::SqlFrom, - collate_spelling: config::CollateSpelling::FunctionString, - bitwise_xor_spelling: config::BitwiseXorSpelling::Unsupported, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Unsupported, - tuple_arity: config::TupleArity::empty(), - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::DoubleQuote, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::Quote, - bytes_format: config::BytesFormat::ToVarbyte, - function_filter: config::FunctionFilter::Unsupported, - array_format: config::ArrayFormat::Unsupported, - date_format: config::DateFormat::Quoted, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::Quoted, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: true, - cast_double_colon: true, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS.union(config::SchemaFeatures::EMBEDDED_DDL), - schema_authorization_style: config::SchemaAuthorizationStyle::Optional, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); - -/// Snowflake SQL dialect. -pub const SNOWFLAKE_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: ':', - placeholder_syntax: config::PlaceholderSyntax::Numbered, - null_treatment: config::NullTreatment::Postfix, - like_escape: config::LikeEscape::SCALAR.union(config::LikeEscape::ANY), - like_any: config::LikeAny::PatternList, - ilike_expression: config::ILikeExpression::Standard, - rlike_expression: config::RLikeExpression::Standard, - string_concat: config::StringConcat::Pipes, - bitwise_xor_spelling: config::BitwiseXorSpelling::BitFunction, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Standard, - cast_try_cast: config::TryCast::TryCast, - cast_double_colon: true, - trim_spelling: config::TrimSpelling::Function, - collate_spelling: config::CollateSpelling::FunctionString, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::BitFunction, - tuple_syntax: config::TupleSyntax::Unsupported, - tuple_arity: config::TupleArity::empty(), - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::DoubleQuote, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::BackslashHexZero, - bytes_format: config::BytesFormat::ToBinary, - function_filter: config::FunctionFilter::Unsupported, - array_format: config::ArrayFormat::Unsupported, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::Timestamptz, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::ParseJson, - supports_similar_to: false, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS, - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::ExternalVolume, -}); - -/// Apache Spark SQL dialect. -pub const SPARK_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '?', - placeholder_syntax: config::PlaceholderSyntax::Simple, - null_treatment: config::NullTreatment::Postfix, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::PatternList, - ilike_expression: config::ILikeExpression::Standard, - rlike_expression: config::RLikeExpression::Standard, - string_concat: config::StringConcat::Pipes, - bitwise_xor_spelling: config::BitwiseXorSpelling::Caret, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Standard, - cast_try_cast: config::TryCast::TryCast, - cast_double_colon: true, - trim_spelling: config::TrimSpelling::SqlFrom, - collate_spelling: config::CollateSpelling::Unsupported, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Struct, - tuple_arity: config::TupleArity::ONE, - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::Backtick, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::BackslashUnicodeQuote, - bytes_format: config::BytesFormat::PrefixBigX, - function_filter: config::FunctionFilter::FilterWhere, - array_format: config::ArrayFormat::Function, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::WithTimezone, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: false, - schema_features: config::SchemaFeatures::IF_NOT_EXISTS.union(config::SchemaFeatures::IS_DATABASE), - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::LocationKeyword, -}); - -/// SQLite dialect. -pub const SQLITE_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '?', - placeholder_syntax: config::PlaceholderSyntax::Numbered, - null_treatment: config::NullTreatment::Unsupported, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::UseLike, - rlike_expression: config::RLikeExpression::Unsupported, - string_concat: config::StringConcat::Pipes, - modulo_spelling: config::ModuloSpelling::Percent, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Standard, - cast_try_cast: config::TryCast::Unsupported, - cast_double_colon: false, - trim_spelling: config::TrimSpelling::Function, - collate_spelling: config::CollateSpelling::InfixIdent, - bitwise_xor_spelling: config::BitwiseXorSpelling::Unsupported, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - tuple_syntax: config::TupleSyntax::Parentheses, - tuple_arity: config::TupleArity::EMPTY, - bit_format: config::BitFormat::Unsupported, - bool_format: config::BoolFormat::Integer, - identifier_format: config::IdentifierFormat::DoubleQuote, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::Quote, - bytes_format: config::BytesFormat::PrefixBigX, - function_filter: config::FunctionFilter::FilterWhere, - array_format: config::ArrayFormat::Unsupported, - date_format: config::DateFormat::Quoted, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::Quoted, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: false, - schema_features: config::SchemaFeatures::UNSUPPORTED, - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); - -/// Teradata SQL dialect. -pub const TERADATA_DIALECT: base::PyDialect = base::PyDialect(config::DialectConfig { - placeholder_character: '?', - placeholder_syntax: config::PlaceholderSyntax::Simple, - null_treatment: config::NullTreatment::InsideParens, - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::OrExpansion, - ilike_expression: config::ILikeExpression::UseLikeLower, - rlike_expression: config::RLikeExpression::Unsupported, - string_concat: config::StringConcat::Pipes, - bitwise_xor_spelling: config::BitwiseXorSpelling::BitFunction, - modulo_spelling: config::ModuloSpelling::Infix, - is_distinct_from_spelling: config::IsDistinctFromSpelling::Unsupported, - cast_try_cast: config::TryCast::TryCastWord, - cast_double_colon: false, - trim_spelling: config::TrimSpelling::SqlFrom, - collate_spelling: config::CollateSpelling::Unsupported, - bitwise_operations: config::BitwiseOperations::AND - .union(config::BitwiseOperations::OR) - .union(config::BitwiseOperations::NOT) - .union(config::BitwiseOperations::SHL) - .union(config::BitwiseOperations::SHR), - bitwise_call_syntax: config::BitwiseCallSyntax::ShiftFunction, - tuple_syntax: config::TupleSyntax::Unsupported, - tuple_arity: config::TupleArity::empty(), - bit_format: config::BitFormat::PostfixBigB, - bool_format: config::BoolFormat::Keyword, - identifier_format: config::IdentifierFormat::DoubleQuote, - identifier_escape: config::IdentifierEscape::Double, - string_format: config::StringFormat::SingleQuote, - string_escape: config::StringEscape::Quote, - bytes_format: config::BytesFormat::PostfixXbv, - function_filter: config::FunctionFilter::Unsupported, - array_format: config::ArrayFormat::Unsupported, - date_format: config::DateFormat::Keyword, - time_format: config::TimeFormat::Quoted, - timestamp_format: config::TimestampFormat::WithTimezone, - inet_format: config::InetFormat::Unsupported, - macaddr_format: config::MacAddrFormat::Unsupported, - uuid_format: config::UuidFormat::Quoted, - json_format: config::JsonFormat::Quoted, - supports_similar_to: false, - schema_features: config::SchemaFeatures::UNSUPPORTED, - schema_authorization_style: config::SchemaAuthorizationStyle::Unsupported, - schema_location_style: config::SchemaLocationStyle::Unsupported, -}); diff --git a/dialect/config.rs b/dialect/config.rs deleted file mode 100644 index ca80cdc..0000000 --- a/dialect/config.rs +++ /dev/null @@ -1,699 +0,0 @@ -use crate::internal::template; - -use super::errors::FormatError; - -/// Packed options that decide how one dialect spells SQL. -/// -/// ``` -/// use rapidquery::dialect::config::BoolFormat; -/// use rapidquery::dialect::config::DialectConfig; -/// -/// let mut c = DialectConfig { bool_format: BoolFormat::Integer, ..Default::default() }; -/// -/// let mut out = String::new(); -/// c.write_boolean(&mut out, true).unwrap(); -/// assert_eq!(out, "1"); -/// ``` -#[rapidquery_macros::define_options] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] -pub struct DialectConfig { - pub placeholder_character: char, - - /// Positional parameter syntax - /// - /// - Simple: `'?'` - /// - Numbered: `'?1'` - #[enum_(#[default] Simple, Numbered)] - pub placeholder_syntax: PlaceholderSyntax, - - /// Where an inlined `LIKE` `ESCAPE` clause is legal. - /// - /// Empty writes no `ESCAPE` and rejects a set escape character. - /// `SCALAR | ANY` allows both a scalar pattern and native `LIKE ANY`. - /// - /// - SCALAR: Scalar `LIKE` and an `OR` expansion. - /// - ANY: Native `LIKE ANY (p, …) ESCAPE`. - #[bit(SCALAR = 1 << 0, ANY = 1 << 1)] - pub like_escape: LikeEscape, - - /// How `LIKE ANY` / `ILIKE ANY` is rendered. - /// - /// - OrExpansion: `(x LIKE p1 OR x LIKE p2)`. Useful when it isn't supported natively by dialect. - /// - ArrayConstructor: `LIKE ANY (ARRAY[p, …])`. - /// - PatternList: `LIKE ANY (p, …)`. - #[enum_(#[default] OrExpansion, ArrayConstructor, PatternList)] - pub like_any: LikeAny, - - /// Does dialect support `ILIKE` expression or we should fallback to another options? - /// - /// - Standard - /// - UseLikeLower: Use `LIKE` expression and `LOWER()` function - /// - UseLike: Fallback to `LIKE` expression - #[enum_(#[default] Standard, UseLikeLower, UseLike)] - pub ilike_expression: ILikeExpression, - - /// How regex `RLike` is spelled. - /// - /// - Unsupported: Is not supported. - /// - Standard: `expr [NOT] RLIKE pattern` - /// - Tilde: `expr ~ pattern` / `expr !~ pattern` - /// - RegexpLike: `REGEXP_LIKE(expr, pattern)` - /// - RegexpContains: `REGEXP_CONTAINS(expr, pattern)` - /// - RegexpMatches: `regexp_matches(expr, pattern)` - #[enum_(#[default] Unsupported, Standard, Tilde, RegexpLike, RegexpContains, RegexpMatches)] - pub rlike_expression: RLikeExpression, - - pub supports_similar_to: bool, - - /// The way dialect supports `{IGNORE|RESPECT} NULLS` expressions in `Function`s. - /// - /// - Unsupported: Is not supported - /// - InsideParens: e.g. `FIRST_VALUE(x IGNORE NULLS)` - /// - Postfix: e.g. `FIRST_VALUE(x) IGNORE NULLS` - #[enum_(#[default] Unsupported, InsideParens, Postfix)] - pub null_treatment: NullTreatment, - - /// Function `FILTER` syntax. - /// - /// - Unsupported: Is not supported. - /// - Filter: e.g. FIRST_VALUE(x) FILTER (id > 10) - /// - FilterWhere: e.g. FIRST_VALUE(x) FILTER (WHERE id > 10) - /// - InsideWhere: e.g. FIRST_VALUE(x WHERE id > 10) - #[enum_(Unsupported, Filter, #[default] FilterWhere, InsideWhere)] - pub function_filter: FunctionFilter, - - /// Which bitwise operators a dialect can render. - /// - /// A missing flag rejects that operator. AND, OR, NOT, and the shifts share - /// one spelling ([`BitCall`]). XOR uses [`XorSpelling`]. - #[bit(AND = 1 << 0, OR = 1 << 1, NOT = 1 << 2, SHL = 1 << 3, SHR = 1 << 4)] - pub bitwise_operations: BitwiseOperations, - - /// Spelling shared by `&`, `|`, `~`, `<<`, and `>>`. - /// - /// - Infix: `& | ~ << >>`. - /// - BitFunction: `BITAND` / `BITOR` / `BITNOT` / `BITSHIFTLEFT` / `BITSHIFTRIGHT`. - /// - ShiftFunction: `BITAND` / `BITOR` / `BITNOT` / `SHIFTLEFT` / `SHIFTRIGHT`. - #[enum_(#[default] Infix, BitFunction, ShiftFunction)] - pub bitwise_call_syntax: BitwiseCallSyntax, - - /// How `^` is spelled when [`BitOp::XOR`] is set. - /// - /// - Unsupported - /// - Caret: `left ^ right`. - /// - Hash: `left # right`. PostgreSQL `^` is exponent. - /// - Function: `xor(left, right)`. - /// - BitFunction: `BITXOR(left, right)`. - #[enum_(Unsupported, #[default] Caret, Hash, Function, BitFunction)] - pub bitwise_xor_spelling: BitwiseXorSpelling, - - /// Delimiters wrapped around a quoted identifier. - /// - /// - DoubleQuote: `"name"` - /// - Backtick: `` `name` `` - /// - SquareBracket: `[name]` - #[enum_(#[default] DoubleQuote, Backtick, SquareBracket)] - pub identifier_format: IdentifierFormat, - - /// How to escape identifiers. - /// - /// - Double: Escape identifier delimiters with doubling. - /// - Backslash: Escape identifier delimiters with backslash. - #[enum_(#[default] Double, Backslash)] - pub identifier_escape: IdentifierEscape, - - /// Delimiters wrapped around a literal string. - /// - /// - SingleQuote: `'string'` - /// - PrefixBigE: `E'string'` - /// - PrefixBigN: `N'string'` - #[enum_(#[default] SingleQuote, PrefixBigE, PrefixBigN)] - pub string_format: StringFormat, - - /// How to escape literal strings. - /// - /// - Quote: `'` → `''`. Other characters are unchanged. - /// - BackslashHex: `\\`, `\'`, `\n`, `\r`, `\t`; other C0/DEL as `\xNN`. - /// - BackslashMysql: Also `\"` and `\Z` for 0x1A; tab and other controls pass through. - /// - BackslashUnicode: Other C0/DEL as `\uNNNN`. No `\"`. - /// - BackslashHexZero: Other C0/DEL as `\xNN`. - /// - BackslashUnicodeQuote: [`Self::BackslashUnicode`] plus `\"`. - #[enum_(#[default] Quote, BackslashHex, BackslashMysql, BackslashUnicode, BackslashHexZero, BackslashUnicodeQuote)] - pub string_escape: StringEscape, - - /// How binary operation StringConcat (`||`) is spelled. - /// - /// - Pipes: `left || right`. - /// - Plus: `left + right`. SQL Server `||` is 2025+; classic concat is `+`. - /// - ConcatFunction: `CONCAT(left, right)`. MySQL `||` is logical OR unless `PIPES_AS_CONCAT`. - #[enum_(#[default] Pipes, Plus, ConcatFunction)] - pub string_concat: StringConcat, - - /// How `TRY_CAST` is spelled. - /// - /// - Unsupported: Is not supported - /// - TryCast: `TRY_CAST(expr AS type)`. - /// - TryCastWord: `TRYCAST(expr AS type)`. - /// - SafeCast: `SAFE_CAST(expr AS type)`. BigQuery's spelling of a null-on-failure cast. - #[enum_(Unsupported, #[default] TryCast, TryCastWord, SafeCast)] - pub cast_try_cast: TryCast, - - pub cast_double_colon: bool, - - /// How a tuple / row value is spelled. - /// - /// Which arities are legal is [`TupleArity`], not another syntax variant. - /// - /// - Unsupported: Is not supported. - /// - Row: `ROW(...)`. - /// - Struct: `struct(...)`. - /// - Parentheses: `(a, b, ...)`. - #[enum_(#[default] Unsupported, Row, Struct, Parentheses)] - pub tuple_syntax: TupleSyntax, - - /// Arities below 2 that [`TupleSyntax`] accepts. - /// - /// Arity 2 and above is legal whenever the syntax is not [`TupleSyntax::Unsupported`]. - /// Empty means a tuple needs at least two elements. - /// - /// - EMPTY: Arity 0. - /// - ONE: Arity 1. - #[bit(EMPTY = 1 << 0, ONE = 1 << 1)] - pub tuple_arity: TupleArity, - - /// How a date literal is spelled. - /// - /// - Quoted: Quoted string. - /// - Keyword: `DATE`, a space, then a quoted literal. - #[enum_(#[default] Quoted, Keyword)] - pub date_format: DateFormat, - - /// How a time literal is spelled. - /// - /// - Quoted: Quoted string. - /// - Keyword: `TIME`, a space, then a quoted literal. - #[enum_(#[default] Quoted, Keyword)] - pub time_format: TimeFormat, - - /// How a timestamp literal is spelled. - /// - /// Naive values use `TIMESTAMP` when a keyword is configured. Aware values - /// (`datetime` with a tzinfo, or a UTC epoch number) use the keyword this - /// variant names. - /// - /// - Quoted: Both naive and aware values are a quoted string. No keyword. - /// - Timestamptz: Naive: `TIMESTAMP '…'`. Aware: `TIMESTAMPTZ '…'`. - /// - WithTimezone: Naive: `TIMESTAMP '…'`. Aware: `TIMESTAMP WITH TIMEZONE '…'`. - #[enum_(Quoted, Timestamptz, #[default] WithTimezone)] - pub timestamp_format: TimestampFormat, - - /// How a boolean literal is spelled. - /// - /// - Keyword: `TRUE` / `FALSE` - /// - Integer: `1` / `0` - #[enum_(#[default] Keyword, Integer)] - pub bool_format: BoolFormat, - - /// The way dialect accepts literal bit values - /// - /// - Unsupported: Is not supported. - /// - PrefixBigB: e.g. `B'101'` - /// - PrefixSmallB: e.g. `b'101'` - /// - PostfixBigB: e.g. `'101'B` - #[enum_(Unsupported, #[default] PrefixBigB, PrefixSmallB, PostfixBigB)] - pub bit_format: BitFormat, - - /// The way dialect accepts literal bytes values. - /// - /// - PrefixBigX: `X'DEADBEEF'` - /// - PrefixZeroX: `0xDEADBEEF` - /// - SlashXBytea: `'\xDEADBEEF'::bytea` - /// - PrefixBigBSlashX: `B'\xNN…'` (each byte as `\xNN`) - /// - HexToRaw: `HEXTORAW('DEADBEEF')` - /// - Unhex: `UNHEX('DEADBEEF')` - /// - ToVarbyte: `TO_VARBYTE('DEADBEEF', 'hex')` - /// - PostfixXbv: `'DEADBEEF'XBV` - /// - ToBinary: `TO_BINARY('DEADBEEF', 'HEX')` - /// - SlashXBlob: `'\xHH…'::BLOB`; empty is `''::BLOB` - #[enum_(#[default] PrefixBigX, PrefixZeroX, SlashXBytea, PrefixBigBSlashX, HexToRaw, Unhex, ToVarbyte, PostfixXbv, ToBinary, SlashXBlob)] - pub bytes_format: BytesFormat, - - /// How an inet literal is spelled. - /// - /// - Unsupported: Is not supported. - /// - Quoted: `'192.168.1.1/32'` - /// - Cast: `'192.168.1.1/32'::inet` - #[enum_(#[default] Unsupported, Quoted, Cast)] - pub inet_format: InetFormat, - - /// How a MAC address literal is spelled. - /// - /// - Unsupported: Is not supported. - /// - Quoted: `'01:23:45:67:89:AB'` - /// - Cast: `'01:23:45:67:89:AB'::macaddr` - #[enum_(#[default] Unsupported, Quoted, Cast)] - pub macaddr_format: MacAddrFormat, - - /// How a UUID literal is spelled. - /// - /// - Quoted: Quoted hyphenated text. - /// - UuidKeyword: `UUID '…'` - /// - CastUuid: `'…'::uuid` - #[enum_(#[default] Quoted, UuidKeyword, CastUuid)] - pub uuid_format: UuidFormat, - - /// How a JSON literal is spelled. - /// - /// - Quoted: Quoted JSON text. - /// - JsonKeyword: `JSON '…'` - /// - ParseJson: `PARSE_JSON('…')` - #[enum_(#[default] Quoted, JsonKeyword, ParseJson)] - pub json_format: JsonFormat, - - /// The way dialect accepts array values. - /// - /// - Unsupported: Is not supported. - /// - Constructor: `ARRAY[…]` - /// - List: `[…]` - /// - Function: `array(…)` - #[enum_(#[default] Unsupported, Constructor, List, Function)] - pub array_format: ArrayFormat, - - /// How binary operation mod (`%`) is spelled. - /// - /// - Percent: `left % right`. - /// - Function: `MOD(left, right)`. - /// - Infix: `left MOD right`. - #[enum_(#[default] Percent, Function, Infix)] - pub modulo_spelling: ModuloSpelling, - - /// How `IS [NOT] DISTINCT FROM` is spelled. - /// - /// - Unsupported: Is not supported - /// - Standard: `left IS [NOT] DISTINCT FROM right`. - /// - NullSafeEqual: `<=>` is null-safe equal (`IS NOT DISTINCT FROM`). `IS DISTINCT FROM` is `NOT (left <=> right)`. - #[enum_(Unsupported, #[default] Standard, NullSafeEqual)] - pub is_distinct_from_spelling: IsDistinctFromSpelling, - - /// How `TRIM` is spelled. - /// - /// - SqlFrom: `TRIM([LEADING|TRAILING] [chars FROM] expr)`. - /// - Function: `TRIM` / `LTRIM` / `RTRIM` `(expr[, chars])`. - /// - SpacesOnly: `TRIM` / `LTRIM` / `RTRIM` `(expr)`. A character set raises. - #[enum_(#[default] SqlFrom, Function, SpacesOnly)] - pub trim_spelling: TrimSpelling, - - /// How `COLLATE` is spelled. - /// - /// - Unsupported: Is not supported - /// - InfixIdent: `expr COLLATE ident`. - /// - FunctionString: `COLLATE(expr, 'body')`. The identifier body is a string literal. - #[enum_(Unsupported, #[default] InfixIdent, FunctionString)] - pub collate_spelling: CollateSpelling, - - // --- DDL Schema --- - /// Supported independant schema features, or is it even supported? - /// - /// - UNSUPPORTED: Means dialect does not support schema. - /// - IF_NOT_EXISTS: Is dialect support `IF NOT EXISTS` clause? - /// - EMBEDDED_DDL: Is dialect support embedded DDL statements in `CREATE SCHEMA`? - /// - IS_DATABASE: Schema has same meaning as database in this dialect. - /// - DROP_CASCADE: Supports `CASCADE` keyword in `DROP SCHEMA`. - /// - DROP_RESTRICT: Supports `RESTRICT` keyword in `DROP SCHEMA`. - #[bit(UNSUPPORTED=1<<0, IF_NOT_EXISTS=1<<1, EMBEDDED_DDL=1<<2, IS_DATABASE=1<<3, DROP_CASCADE=1<<4, DROP_RESTRICT=1<<5)] - pub schema_features: SchemaFeatures, - - /// `AUTHORIZATION` clause style. How the schema owner is specified. - /// - /// - Unsupported: Means dialect does not support `AUTHORIZATION` for schema. - /// - Optional: Means `AUTHORIZATION` clause is supported and it's optional. - #[enum_(Unsupported, #[default] Optional)] - pub schema_authorization_style: SchemaAuthorizationStyle, - - /// How schema location is expressed. - /// - /// - Unsupported: Means dialect does not support location for schema at all. - /// - LocationKeyword: `LOCATION location` clause. - /// - ManagedLocationKeyword: `MANAGED LOCATION location` clause. - /// - ExternalVolume: `EXTERNAL_VOLUME = location` clause. - #[enum_(Unsupported, #[default] LocationKeyword, ManagedLocationKeyword, ExternalVolume)] - pub schema_location_style: SchemaLocationStyle, -} - -impl DialectConfig { - /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). - #[inline] - pub fn write_boolean(&self, writer: &mut String, value: bool) { - match (self.bool_format, value) { - (BoolFormat::Integer, false) => writer.push_("0")?, - (BoolFormat::Integer, true) => writer.push_("1")?, - (BoolFormat::Keyword, false) => writer.push_("FALSE")?, - (BoolFormat::Keyword, true) => writer.push_("TRUE")?, - } - - Ok(()) - } - - /// Write `string` with special characters neutralized, without wrapping it. - #[inline] - pub(super) fn escape_string(&self, writer: &mut W, string: &str) -> Result<(), FormatError> { - let result = match self.string_escape { - StringEscape::Quote => template::replace_to(string, '\'', "''", writer), - StringEscape::BackslashHex => write_backslash_hex(writer, string), - StringEscape::BackslashMysql => write_backslash_mysql(writer, string), - StringEscape::BackslashUnicode => write_backslash_unicode(writer, string, false), - StringEscape::BackslashHexZero => write_backslash_hex_zero(writer, string), - StringEscape::BackslashUnicodeQuote => write_backslash_unicode(writer, string, true), - }; - result.map_err(Into::into) - } - - /// Escape `ident` and wrap it in this dialect's identifier delimiters. - #[inline] - pub fn write_identifier(&self, writer: &mut W, ident: &str) -> std::fmt::Result { - let (start, end) = match self.identifier_format { - IdentifierFormat::DoubleQuote => ('"', '"'), - IdentifierFormat::Backtick => ('`', '`'), - IdentifierFormat::SquareBracket => ('[', ']'), - }; - writer.write_char(start)?; - - match self.identifier_escape { - IdentifierEscape::Double => match self.identifier_format { - IdentifierFormat::DoubleQuote => { - template::replace_to(ident, '"', "\"\"", writer)?; - } - IdentifierFormat::Backtick => { - template::replace_to(ident, '`', "``", writer)?; - } - IdentifierFormat::SquareBracket => { - template::replace_to(ident, ']', "]]", writer)?; - } - }, - IdentifierEscape::Backslash => match self.identifier_format { - IdentifierFormat::DoubleQuote => { - template::replace_to(ident, '"', "\\\"", writer)?; - } - IdentifierFormat::Backtick => { - write_identifier_backtick_escape(writer, ident)?; - } - IdentifierFormat::SquareBracket => { - template::replace_to(ident, ']', "\\]", writer)?; - } - }, - } - - writer.write_char(end) - } - - /// Escape `raw` and wrap it as a complete string literal. - #[inline] - pub fn write_string_literal(&self, writer: &mut W, raw: &str) -> Result<(), FormatError> { - if raw.contains('\0') { - return Err(FormatError::NulInString); - } - - match self.string_format { - StringFormat::PrefixBigE => writer.write_str("E'")?, - StringFormat::PrefixBigN => writer.write_str("N'")?, - StringFormat::SingleQuote => writer.write_char('\'')?, - } - self.escape_string(writer, raw)?; - writer.write_char('\'')?; - Ok(()) - } - - /// Write a bit-string (`'0'`/`'1'` characters) as a native literal. - /// - /// Native formats embed `bits` as-is. Callers must pass only `'0'` and `'1'`; - /// that is checked with `debug_assert!`. - #[inline] - pub fn write_bits(&self, writer: &mut W, bits: &str) -> Result<(), FormatError> { - debug_assert!( - bits.chars().all(|c| c == '0' || c == '1'), - "write_bits: invalid input: {bits}" - ); - match self.bit_format { - BitFormat::Unsupported => Err(FormatError::Unsupported("BitSerializer")), - BitFormat::PrefixBigB => write!(writer, "B'{bits}'").map_err(Into::into), - BitFormat::PrefixSmallB => write!(writer, "b'{bits}'").map_err(Into::into), - BitFormat::PostfixBigB => write!(writer, "'{bits}'B").map_err(Into::into), - } - } - - /// Write `bytes` as a native blob/bytea/varbyte literal. - #[inline] - pub fn write_bytes(&self, writer: &mut W, bytes: &[u8]) -> Result<(), FormatError> { - let result = match self.bytes_format { - BytesFormat::PrefixBigX => { - writer.write_str("X'")?; - bytes_to_hex(writer, bytes)?; - writer.write_char('\'') - } - BytesFormat::PrefixZeroX => { - writer.write_str("0x")?; - bytes_to_hex(writer, bytes) - } - BytesFormat::SlashXBytea => { - writer.write_str("'\\x")?; - bytes_to_hex(writer, bytes)?; - writer.write_str("'::bytea") - } - BytesFormat::PrefixBigBSlashX => { - writer.write_str("B'")?; - write_bytes_slash_x(writer, bytes)?; - writer.write_char('\'') - } - BytesFormat::HexToRaw => { - writer.write_str("HEXTORAW('")?; - bytes_to_hex(writer, bytes)?; - writer.write_str("')") - } - BytesFormat::Unhex => { - writer.write_str("UNHEX('")?; - bytes_to_hex(writer, bytes)?; - writer.write_str("')") - } - BytesFormat::ToVarbyte => { - writer.write_str("TO_VARBYTE('")?; - bytes_to_hex(writer, bytes)?; - writer.write_str("', 'hex')") - } - BytesFormat::PostfixXbv => { - writer.write_char('\'')?; - bytes_to_hex(writer, bytes)?; - writer.write_str("'XBV") - } - BytesFormat::ToBinary => { - writer.write_str("TO_BINARY('")?; - bytes_to_hex(writer, bytes)?; - writer.write_str("', 'HEX')") - } - BytesFormat::SlashXBlob => { - if bytes.is_empty() { - writer.write_str("''::BLOB") - } else { - writer.write_char('\'')?; - write_bytes_slash_x(writer, bytes)?; - writer.write_str("'::BLOB") - } - } - }; - result.map_err(Into::into) - } -} - -const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF"; - -/// Write `byte` as two uppercase hex digits. -#[inline] -fn write_hex2(writer: &mut W, byte: u8) -> std::fmt::Result { - let buf = [HEX_DIGITS[(byte >> 4) as usize], HEX_DIGITS[(byte & 0x0F) as usize]]; - // SAFETY: HEX_DIGITS is ASCII, so `buf` is valid UTF-8. - writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf) }) -} - -/// Write `bytes` as a contiguous uppercase hex run (`DEADBEEF`). -/// -/// 64 raw bytes fill a 128-byte stack buffer and are written once. -#[inline] -fn bytes_to_hex(writer: &mut W, bytes: &[u8]) -> std::fmt::Result { - let mut buf = [0u8; 128]; - for chunk in bytes.chunks(64) { - for (i, &byte) in chunk.iter().enumerate() { - let o = i * 2; - buf[o] = HEX_DIGITS[(byte >> 4) as usize]; - buf[o + 1] = HEX_DIGITS[(byte & 0x0F) as usize]; - } - let n = chunk.len() * 2; - // SAFETY: `buf[..n]` is filled only from `HEX_DIGITS` (ASCII). - writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) })?; - } - Ok(()) -} - -/// Write each byte as `\xNN` (uppercase hex). -/// -/// 32 raw bytes fill a 128-byte stack buffer and are written once. -#[inline] -fn write_bytes_slash_x(writer: &mut W, bytes: &[u8]) -> std::fmt::Result { - let mut buf = [0u8; 128]; - for chunk in bytes.chunks(32) { - for (i, &byte) in chunk.iter().enumerate() { - let o = i * 4; - buf[o] = b'\\'; - buf[o + 1] = b'x'; - buf[o + 2] = HEX_DIGITS[(byte >> 4) as usize]; - buf[o + 3] = HEX_DIGITS[(byte & 0x0F) as usize]; - } - let n = chunk.len() * 4; - // SAFETY: `buf[..n]` is ASCII (`\`, `x`, and hex digits). - writer.write_str(unsafe { std::str::from_utf8_unchecked(&buf[..n]) })?; - } - Ok(()) -} - -/// Write `\xNN` for a single byte (uppercase hex). -#[inline] -fn write_byte_hex_escape(writer: &mut W, byte: u8) -> std::fmt::Result { - writer.write_str("\\x")?; - write_hex2(writer, byte) -} - -/// Write `\uNNNN` for a codepoint that fits in 16 bits. -#[inline] -fn write_unicode4_escape(writer: &mut W, code: u32) -> std::fmt::Result { - debug_assert!(code <= 0xFFFF); - writer.write_str("\\u")?; - write_hex2(writer, (code >> 8) as u8)?; - write_hex2(writer, code as u8) -} - -/// Use the string escape table, plus `\``. -/// -/// `'` is left as itself. Only known escapes are emitted. -fn write_identifier_backtick_escape(writer: &mut W, ident: &str) -> std::fmt::Result { - write_span_escapes(writer, ident, |b| match b { - b'\\' => SpanEscape::Literal("\\\\"), - b'`' => SpanEscape::Literal("\\`"), - b'\n' => SpanEscape::Literal("\\n"), - b'\r' => SpanEscape::Literal("\\r"), - b'\t' => SpanEscape::Literal("\\t"), - b if b < 32 || b == 127 => SpanEscape::HexByte, - _ => SpanEscape::Keep, - }) -} - -/// Write `bytes[from..to]` as a string. -/// -/// # Safety invariant -/// `bytes` is the raw view of a `&str`. Callers split only on ASCII bytes, -/// which are char boundaries, or at `bytes.len()`. -#[inline] -fn write_byte_span(writer: &mut W, bytes: &[u8], from: usize, to: usize) -> std::fmt::Result { - if from == to { - return Ok(()); - } - // SAFETY: see invariant above. - let chunk = unsafe { bytes.get_unchecked(from..to) }; - writer.write_str(unsafe { std::str::from_utf8_unchecked(chunk) }) -} - -enum SpanEscape { - Keep, - Literal(&'static str), - HexByte, - Unicode4, -} - -/// Copy each plain span with one `write_str`. Special bytes are ASCII. -#[cfg_attr(feature = "nightly", optimize(speed))] -fn write_span_escapes( - writer: &mut W, - string: &str, - mut classify: impl FnMut(u8) -> SpanEscape, -) -> std::fmt::Result { - let bytes = string.as_bytes(); - let mut start = 0; - let mut i = 0; - while i < bytes.len() { - match classify(bytes[i]) { - SpanEscape::Keep => i += 1, - SpanEscape::Literal(repl) => { - write_byte_span(writer, bytes, start, i)?; - writer.write_str(repl)?; - i += 1; - start = i; - } - SpanEscape::HexByte => { - write_byte_span(writer, bytes, start, i)?; - write_byte_hex_escape(writer, bytes[i])?; - i += 1; - start = i; - } - SpanEscape::Unicode4 => { - write_byte_span(writer, bytes, start, i)?; - write_unicode4_escape(writer, bytes[i] as u32)?; - i += 1; - start = i; - } - } - } - write_byte_span(writer, bytes, start, bytes.len()) -} - -fn write_backslash_hex(writer: &mut W, string: &str) -> std::fmt::Result { - write_span_escapes(writer, string, |b| match b { - b'\\' => SpanEscape::Literal("\\\\"), - b'\'' => SpanEscape::Literal("\\'"), - b'\n' => SpanEscape::Literal("\\n"), - b'\r' => SpanEscape::Literal("\\r"), - b'\t' => SpanEscape::Literal("\\t"), - b if b < 32 || b == 127 => SpanEscape::HexByte, - _ => SpanEscape::Keep, - }) -} - -fn write_backslash_mysql(writer: &mut W, string: &str) -> std::fmt::Result { - write_span_escapes(writer, string, |b| match b { - b'\\' => SpanEscape::Literal("\\\\"), - b'\'' => SpanEscape::Literal("\\'"), - b'"' => SpanEscape::Literal("\\\""), - 0 => SpanEscape::Literal("\\0"), - b'\n' => SpanEscape::Literal("\\n"), - b'\r' => SpanEscape::Literal("\\r"), - 0x1a => SpanEscape::Literal("\\Z"), - _ => SpanEscape::Keep, - }) -} - -fn write_backslash_unicode( - writer: &mut W, - string: &str, - escape_double_quote: bool, -) -> std::fmt::Result { - write_span_escapes(writer, string, |b| match b { - b'\\' => SpanEscape::Literal("\\\\"), - b'\'' => SpanEscape::Literal("\\'"), - b'"' if escape_double_quote => SpanEscape::Literal("\\\""), - b'\n' => SpanEscape::Literal("\\n"), - b'\r' => SpanEscape::Literal("\\r"), - b'\t' => SpanEscape::Literal("\\t"), - 0 => SpanEscape::Literal("\\0"), - b if b < 32 || b == 127 => SpanEscape::Unicode4, - _ => SpanEscape::Keep, - }) -} - -fn write_backslash_hex_zero(writer: &mut W, string: &str) -> std::fmt::Result { - write_span_escapes(writer, string, |b| match b { - b'\\' => SpanEscape::Literal("\\\\"), - b'\'' => SpanEscape::Literal("\\'"), - b'\n' => SpanEscape::Literal("\\n"), - b'\r' => SpanEscape::Literal("\\r"), - b'\t' => SpanEscape::Literal("\\t"), - 0 => SpanEscape::Literal("\\0"), - b if b < 32 || b == 127 => SpanEscape::HexByte, - _ => SpanEscape::Keep, - }) -} diff --git a/dialect/errors.rs b/dialect/errors.rs deleted file mode 100644 index aab5729..0000000 --- a/dialect/errors.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::fmt; - -/// Formatting / serializing errors -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FormatError { - /// Equals to [`fmt::Write`] - Write, - /// Means a `NUL` character has found in string during escaping/serializing a string value. - NulInString, - /// Means something is not supported by a dialect - Unsupported(&'static str), -} - -impl From for FormatError { - #[inline] - fn from(_: fmt::Error) -> Self { - Self::Write - } -} - -impl fmt::Display for FormatError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Write => f.write_str("failed to write SQL to memory"), - Self::NulInString => f.write_str("NUL is not allowed in a string literal"), - Self::Unsupported(name) => write!(f, "this dialect does not support {name}"), - } - } -} - -impl std::error::Error for FormatError {} diff --git a/dialect/mod.rs b/dialect/mod.rs deleted file mode 100644 index acb6a81..0000000 --- a/dialect/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -pub mod config; -pub mod errors; - -mod base; -mod builtin; - -#[cfg(test)] -mod tests; - -pub use base::PyDialect; -pub use builtin::BIGQUERY_DIALECT; -pub use builtin::DATABRICKS_DIALECT; -pub use builtin::DUCKDB_DIALECT; -pub use builtin::HIVE_DIALECT; -pub use builtin::MSSQL_DIALECT; -pub use builtin::MYSQL_DIALECT; -pub use builtin::ORACLE_DIALECT; -pub use builtin::POSTGRESQL_DIALECT; -pub use builtin::REDSHIFT_DIALECT; -pub use builtin::SNOWFLAKE_DIALECT; -pub use builtin::SPARK_DIALECT; -pub use builtin::SQLITE_DIALECT; -pub use builtin::TERADATA_DIALECT; - -#[pyo3::pymodule(name = "dialect", gil_used = false)] -pub mod dialect_mod { - #[pymodule_export] - use super::PyDialect; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const BigQuery: super::PyDialect = super::BIGQUERY_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Databricks: super::PyDialect = super::DATABRICKS_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const DuckDb: super::PyDialect = super::DUCKDB_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Hive: super::PyDialect = super::HIVE_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const MsSql: super::PyDialect = super::MSSQL_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const MySql: super::PyDialect = super::MYSQL_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Oracle: super::PyDialect = super::ORACLE_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const PostgreSql: super::PyDialect = super::POSTGRESQL_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Redshift: super::PyDialect = super::REDSHIFT_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Snowflake: super::PyDialect = super::SNOWFLAKE_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Spark: super::PyDialect = super::SPARK_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Sqlite: super::PyDialect = super::SQLITE_DIALECT; - - #[pymodule_export] - #[allow(non_upper_case_globals)] - const Teradata: super::PyDialect = super::TERADATA_DIALECT; - - #[pymodule_init] - fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { - super::base::export_options(m) - } -} diff --git a/dialect/tests.rs b/dialect/tests.rs deleted file mode 100644 index 091a55c..0000000 --- a/dialect/tests.rs +++ /dev/null @@ -1,937 +0,0 @@ -use std::str::FromStr; - -use super::*; - -#[inline] -fn bool_flags(format: config::BoolFormat) -> config::DialectConfig { - config::DialectConfig { - bool_format: format, - ..Default::default() - } -} - -#[inline] -fn string_flags(format: config::StringFormat, escape: config::StringEscape) -> config::DialectConfig { - config::DialectConfig { - string_format: format, - string_escape: escape, - ..Default::default() - } -} - -#[inline] -fn ident_flags(format: config::IdentifierFormat, escape: config::IdentifierEscape) -> config::DialectConfig { - config::DialectConfig { - identifier_format: format, - identifier_escape: escape, - ..Default::default() - } -} - -fn write_str(flags: &config::DialectConfig, raw: &str) -> Result { - let mut out = String::new(); - flags.write_string_literal(&mut out, raw)?; - Ok(out) -} - -fn escape(flags: &config::DialectConfig, raw: &str) -> String { - let mut out = String::new(); - flags.escape_string(&mut out, raw).unwrap(); - out -} - -fn quote_ident(flags: &config::DialectConfig, ident: &str) -> String { - let mut out = String::new(); - flags.write_identifier(&mut out, ident).unwrap(); - out -} - -/// A writer that fails on the Nth `write_str` / `write_char` (1-based). -struct FailAfter { - remaining: usize, -} - -impl std::fmt::Write for FailAfter { - fn write_str(&mut self, s: &str) -> std::fmt::Result { - if s.is_empty() { - return Ok(()); - } - if self.remaining == 0 { - return Err(std::fmt::Error); - } - self.remaining -= 1; - Ok(()) - } -} - -/// Standard-SQL string lexer: quotes doubled, no backslash escapes. -/// Returns the unescaped body if `sql` is exactly one literal. -fn parse_quote_doubled(sql: &str) -> Option { - let rest = sql.strip_prefix('\'')?; - let mut body = String::new(); - let mut chars = rest.chars().peekable(); - while let Some(ch) = chars.next() { - if ch == '\'' { - if chars.peek() == Some(&'\'') { - chars.next(); - body.push('\''); - } else { - return chars.next().is_none().then_some(body); - } - } else { - body.push(ch); - } - } - None -} - -/// MySQL-style backslash lexer (NO_BACKSLASH_ESCAPES off). Unknown escapes -/// keep the escaped byte. Returns the body if the literal closes exactly once. -fn parse_backslash(sql: &str) -> Option { - let rest = sql.strip_prefix('\'')?; - let bytes = rest.as_bytes(); - let mut i = 0; - let mut body = Vec::new(); - while i < bytes.len() { - match bytes[i] { - b'\'' => { - i += 1; - return (i == bytes.len()).then(|| String::from_utf8(body).ok()).flatten(); - } - b'\\' => { - if i + 1 >= bytes.len() { - return None; - } - body.push(bytes[i + 1]); - i += 2; - } - b => { - body.push(b); - i += 1; - } - } - } - None -} - -fn assert_no_early_close_quote(escaped: &str) { - let mut chars = escaped.chars().peekable(); - while let Some(ch) = chars.next() { - if ch == '\'' { - assert_eq!( - chars.next(), - Some('\''), - "odd quote in quote-doubled escape: {escaped:?}" - ); - } - } -} - -const BREAKOUTS: &[&str] = &[ - "'", - "''", - "' OR '1'='1", - "' OR 1=1 --", - "'; DROP TABLE users; --", - "\\", - "\\'", - "\\\\", - "'''", - "admin'--", - "1' AND SLEEP(1)--", - "\" OR \"1\"=\"1", - "') OR ('1'='1", - "\n' OR 1=1 --", - "\r\n", - "foo\tbar", - "%' OR 1=1 --", - "_", - "/*", - "*/", - "--", - "#", - "\u{1a}", - "\u{7f}", - "\u{01}", - "café", - "I ❤️ SQL", - "\u{202e}1=1 RO", - "’", // U+2019, not a SQL quote - "'", // U+FF0C fullwidth is not the quote; fullwidth apostrophe is U+FF07 - "'", - "\u{ff07}", -]; - -#[test] -fn write_boolean_keyword_and_integer() { - let keyword = bool_flags(config::BoolFormat::Keyword); - let integer = bool_flags(config::BoolFormat::Integer); - - let mut out = String::new(); - keyword.write_boolean(&mut out, true).unwrap(); - keyword.write_boolean(&mut out, false).unwrap(); - assert_eq!(out, "TRUEFALSE"); - - out.clear(); - integer.write_boolean(&mut out, true).unwrap(); - integer.write_boolean(&mut out, false).unwrap(); - assert_eq!(out, "10"); -} - -#[test] -fn write_boolean_does_not_emit_quotes_or_lowercase() { - // MySQL may accept the integers; neither spelling is a string, so a - // caller that interpolates them into a quoted context would be wrong. - for value in [false, true] { - let mut keyword = String::new(); - let mut integer = String::new(); - bool_flags(config::BoolFormat::Keyword) - .write_boolean(&mut keyword, value) - .unwrap(); - - bool_flags(config::BoolFormat::Integer) - .write_boolean(&mut integer, value) - .unwrap(); - - assert!(!keyword.contains('\'')); - assert!(!integer.contains('\'')); - assert_eq!(keyword, keyword.to_ascii_uppercase()); - assert!(integer == "0" || integer == "1"); - } -} - -#[test] -fn write_boolean_propagates_writer_errors() { - let mut writer = FailAfter { remaining: 0 }; - let err = bool_flags(config::BoolFormat::Keyword) - .write_boolean(&mut writer, true) - .unwrap_err(); - - assert_eq!(err, errors::FormatError::Write); -} - -#[test] -fn escape_string_quote_doubles_and_leaves_the_rest() { - let flags = string_flags(config::StringFormat::SingleQuote, config::StringEscape::Quote); - assert_eq!(escape(&flags, ""), ""); - assert_eq!(escape(&flags, "plain"), "plain"); - assert_eq!(escape(&flags, "'"), "''"); - assert_eq!(escape(&flags, "''"), "''''"); - assert_eq!(escape(&flags, "a'b'c"), "a''b''c"); - // Backslash is data under standard SQL. Leaving it alone is correct for - // Postgres and wrong for MySQL with backslash escapes enabled. - assert_eq!(escape(&flags, "\\'"), "\\''"); - assert_eq!(escape(&flags, "\0"), "\0"); - assert_eq!(escape(&flags, "\n\r\t"), "\n\r\t"); - assert_eq!(escape(&flags, "100%_"), "100%_"); - assert_eq!(escape(&flags, "雪's"), "雪''s"); -} - -#[test] -fn escape_string_backslash_hex_covers_c0_and_del() { - let flags = string_flags(config::StringFormat::SingleQuote, config::StringEscape::BackslashHex); - assert_eq!(escape(&flags, "\\"), "\\\\"); - assert_eq!(escape(&flags, "'"), "\\'"); - assert_eq!(escape(&flags, "\n\r\t"), "\\n\\r\\t"); - assert_eq!(escape(&flags, "\0"), "\\x00"); - assert_eq!(escape(&flags, "\u{1}"), "\\x01"); - assert_eq!(escape(&flags, "\u{1a}"), "\\x1A"); - assert_eq!(escape(&flags, "\u{1f}"), "\\x1F"); - assert_eq!(escape(&flags, "\u{7f}"), "\\x7F"); - // Space, DEL-adjacent printable, and non-ASCII stay raw. Splitting a - // multibyte char here would corrupt the literal (and the unsafe span - // writer assumes ASCII split points). - assert_eq!(escape(&flags, " ~"), " ~"); - assert_eq!(escape(&flags, "雪's\\"), "雪\\'s\\\\"); - assert_eq!(escape(&flags, "\"quote"), "\"quote"); -} - -#[test] -fn escape_string_mysql_keeps_tab_and_uses_z_for_sub() { - let flags = string_flags(config::StringFormat::SingleQuote, config::StringEscape::BackslashMysql); - - assert_eq!(escape(&flags, "\\'\""), "\\\\\\'\\\""); - assert_eq!(escape(&flags, "\0"), "\\0"); - assert_eq!(escape(&flags, "\n\r"), "\\n\\r"); - assert_eq!(escape(&flags, "\t"), "\t"); - assert_eq!(escape(&flags, "\u{1a}"), "\\Z"); - // Other C0 bytes pass through. A strict ANSI client may reject them; - // MySQL's NO_BACKSLASH_ESCAPES-off lexer does not treat them as quotes. - assert_eq!(escape(&flags, "\u{01}"), "\u{01}"); - assert_eq!(escape(&flags, "\u{7f}"), "\u{7f}"); - assert_eq!(escape(&flags, "it\'s"), "it\\'s"); -} - -#[test] -fn escape_string_unicode_and_hex_zero_disagree_on_nul_and_quotes() { - let unicode = string_flags( - config::StringFormat::SingleQuote, - config::StringEscape::BackslashUnicode, - ); - - let unicode_quote = string_flags( - config::StringFormat::SingleQuote, - config::StringEscape::BackslashUnicodeQuote, - ); - let hex_zero = string_flags( - config::StringFormat::SingleQuote, - config::StringEscape::BackslashHexZero, - ); - - assert_eq!(escape(&unicode, "\0"), "\\0"); - assert_eq!(escape(&unicode, "\u{01}"), "\\u0001"); - assert_eq!(escape(&unicode, "\u{1a}"), "\\u001A"); - assert_eq!(escape(&unicode, "\u{7f}"), "\\u007F"); - assert_eq!(escape(&unicode, "\"'"), "\"\\'"); - assert_eq!(escape(&unicode_quote, "\"'"), "\\\"\\'"); - assert_eq!(escape(&hex_zero, "\0"), "\\0"); - assert_eq!(escape(&hex_zero, "\u{01}"), "\\x01"); - assert_eq!(escape(&hex_zero, "\u{7f}"), "\\x7F"); - assert_eq!(escape(&hex_zero, "\t\n"), "\\t\\n"); -} - -#[test] -fn escape_string_does_not_wrap_or_reject_nul() { - // NUL rejection belongs to `write_string_literal`. `escape_string` is the - // inner transform and must stay usable for already-open literals. - let flags = string_flags(config::StringFormat::SingleQuote, config::StringEscape::Quote); - let escaped = escape(&flags, "a\0b"); - assert_eq!(escaped, "a\0b"); - assert!(!escaped.starts_with('\'')); -} - -#[test] -fn write_string_literal_prefixes_and_empty() { - let cases = [ - (config::StringFormat::SingleQuote, "''"), - (config::StringFormat::PrefixBigE, "E''"), - (config::StringFormat::PrefixBigN, "N''"), - ]; - for (format, expected) in cases { - let flags = string_flags(format, config::StringEscape::Quote); - assert_eq!(write_str(&flags, "").unwrap(), expected); - assert_eq!(write_str(&flags, "a").unwrap(), expected.replace("''", "'a'")); - } -} - -#[test] -fn write_string_literal_rejects_nul_before_writing() { - let mut out = String::from("kept"); - let flags = string_flags(config::StringFormat::PrefixBigE, config::StringEscape::BackslashHex); - let err = flags.write_string_literal(&mut out, "a\0b").unwrap_err(); - assert_eq!(err, errors::FormatError::NulInString); - // C drivers (historical libmysql) truncate at NUL. Refusing before any - // write avoids a half-emitted literal that a caller might still send. - assert_eq!(out, "kept"); - assert!(flags.write_string_literal(&mut String::new(), "\0").is_err()); -} - -#[test] -fn write_string_literal_quote_mode_survives_classic_payloads() { - let flags = string_flags(config::StringFormat::SingleQuote, config::StringEscape::Quote); - for payload in BREAKOUTS { - if payload.contains('\0') { - continue; - } - let sql = write_str(&flags, payload).unwrap(); - let body = parse_quote_doubled(&sql).unwrap_or_else(|| panic!("quote breakout for {payload:?} -> {sql:?}")); - assert_eq!(body, *payload); - assert_no_early_close_quote(&sql[1..sql.len() - 1]); - } - assert_eq!(write_str(&flags, "' OR '1'='1").unwrap(), "''' OR ''1''=''1'"); - assert_eq!( - write_str(&flags, "'; DROP TABLE users; --").unwrap(), - "'''; DROP TABLE users; --'" - ); -} - -#[test] -fn write_string_literal_backslash_modes_do_not_end_on_backslash_quote() { - // The MySQL bug: escaping only `'` as `\'` (or not escaping `\`) lets - // `\'` close the string. Every backslash mode must escape `\` first. - let modes = [ - config::StringEscape::BackslashHex, - config::StringEscape::BackslashMysql, - config::StringEscape::BackslashUnicode, - config::StringEscape::BackslashHexZero, - config::StringEscape::BackslashUnicodeQuote, - ]; - for mode in modes { - let flags = string_flags(config::StringFormat::SingleQuote, mode); - for payload in ["\\'", "\\\\'", "'", "\\", "a\\'b", "\n' OR 1=1 --"] { - let sql = write_str(&flags, payload).unwrap(); - assert!( - parse_backslash(&sql).is_some(), - "{mode:?} breakout for {payload:?} -> {sql:?}" - ); - // `\'` must not be a closer. The backslash is escaped, then the quote. - if payload.contains('\\') { - assert!(sql.contains("\\\\"), "{mode:?} left a raw backslash in {sql:?}"); - } - } - } -} - -#[test] -fn write_string_literal_quote_mode_is_not_safe_for_mysql_backslash() { - // Documents the mismatch. Under a MySQL lexer, quote-doubling alone does - // not neutralize a leading backslash: `\'` becomes `\''`, which is an - // escaped quote plus a closer, i.e. an empty string and trailing junk. - let flags = string_flags(config::StringFormat::SingleQuote, config::StringEscape::Quote); - let sql = write_str(&flags, "\\' OR 1=1 --").unwrap(); - assert_eq!(sql, "'\\'' OR 1=1 --'"); - assert!(parse_backslash(&sql).is_none()); -} - -#[test] -fn write_string_literal_national_and_escape_prefixes_still_close() { - let e = string_flags(config::StringFormat::PrefixBigE, config::StringEscape::BackslashHex); - let n = string_flags(config::StringFormat::PrefixBigN, config::StringEscape::Quote); - assert_eq!(write_str(&e, "a'b\\").unwrap(), "E'a\\'b\\\\'"); - assert_eq!(write_str(&n, "a'b").unwrap(), "N'a''b'"); - // Prefix is outside the quotes. A payload must not be able to eat it. - assert!(write_str(&e, "x").unwrap().starts_with("E'")); - assert!(write_str(&n, "x").unwrap().starts_with("N'")); -} - -#[test] -fn write_string_literal_passes_unicode_quotes_and_like_wildcards() { - let flags = string_flags(config::StringFormat::SingleQuote, config::StringEscape::Quote); - // U+2019 / U+FF07 are not the ASCII delimiter. Treating them as quotes - // would corrupt data; not treating ASCII `'` as a quote would be SQLi. - assert_eq!(write_str(&flags, "it’s").unwrap(), "'it’s'"); - assert_eq!(write_str(&flags, "\u{ff07}").unwrap(), "'\u{ff07}'"); - assert_eq!(write_str(&flags, "100%_").unwrap(), "'100%_'"); - // U+202E (right-to-left override) can spoof a log line. It is still data. - let spoof = write_str(&flags, "\u{202e}--").unwrap(); - assert_eq!(spoof, "'\u{202e}--'"); -} - -#[test] -fn write_identifier_doubles_only_its_own_closer() { - let dq = ident_flags(config::IdentifierFormat::DoubleQuote, config::IdentifierEscape::Double); - - let bt = ident_flags(config::IdentifierFormat::Backtick, config::IdentifierEscape::Double); - let br = ident_flags( - config::IdentifierFormat::SquareBracket, - config::IdentifierEscape::Double, - ); - - assert_eq!(quote_ident(&dq, ""), "\"\""); - assert_eq!(quote_ident(&dq, "users"), "\"users\""); - assert_eq!(quote_ident(&dq, "a\"b"), "\"a\"\"b\""); - assert_eq!(quote_ident(&dq, "\""), "\"\"\"\""); - // The other delimiters are data. This is the Postgres / ANSI contract. - assert_eq!(quote_ident(&dq, "a`b]c'"), "\"a`b]c'\""); - - assert_eq!(quote_ident(&bt, "a`b"), "`a``b`"); - assert_eq!(quote_ident(&bt, "`"), "````"); - assert_eq!(quote_ident(&bt, "a\"b"), "`a\"b`"); - - assert_eq!(quote_ident(&br, "a]b"), "[a]]b]"); - assert_eq!(quote_ident(&br, "]"), "[]]]"); - assert_eq!(quote_ident(&br, "a[b"), "[a[b]"); - // SQL Server does not treat `[` as special inside brackets. Escaping it - // would change the identifier. - assert_eq!(quote_ident(&br, "dbo.users"), "[dbo.users]"); -} - -#[test] -fn write_identifier_rejects_delimiter_breakout() { - let samples = [ - ( - ident_flags(config::IdentifierFormat::DoubleQuote, config::IdentifierEscape::Double), - "\"", - "\"", - '"', - ), - ( - ident_flags(config::IdentifierFormat::Backtick, config::IdentifierEscape::Double), - "`", - "`", - '`', - ), - ( - ident_flags( - config::IdentifierFormat::SquareBracket, - config::IdentifierEscape::Double, - ), - "]", - "[", - ']', - ), - ]; - for (flags, closer, start, end) in samples { - for payload in [ - closer.to_string(), - format!("{closer}{closer}"), - format!("x{closer} OR 1=1 --"), - format!("{closer}; DROP TABLE users; --"), - ] { - let sql = quote_ident(&flags, &payload); - assert!( - sql.starts_with(start) && sql.ends_with(end), - "delimiters lost in {sql:?}" - ); - // Doubled closer never leaves a raw closer at an odd position. - let inner = &sql[start.len()..sql.len() - end.len_utf8()]; - let mut rest = inner; - let needle = closer.to_string(); - while let Some(at) = rest.find(&needle) { - assert!( - rest[at + needle.len()..].starts_with(&needle), - "undoubled closer in {sql:?}" - ); - rest = &rest[at + needle.len() * 2..]; - } - } - } -} - -#[test] -fn write_identifier_backslash_modes() { - let dq = ident_flags( - config::IdentifierFormat::DoubleQuote, - config::IdentifierEscape::Backslash, - ); - let bt = ident_flags(config::IdentifierFormat::Backtick, config::IdentifierEscape::Backslash); - let br = ident_flags( - config::IdentifierFormat::SquareBracket, - config::IdentifierEscape::Backslash, - ); - - assert_eq!(quote_ident(&dq, "a\"b"), "\"a\\\"b\""); - assert_eq!(quote_ident(&dq, "\\"), "\"\\\""); - // Standard SQL does not treat `\` as an identifier escape. `\"` still - // contains a raw closer, so this mode is only safe on engines that - // consume the backslash inside quoted identifiers (MySQL ANSI_QUOTES). - assert_eq!(quote_ident(&dq, "\""), "\"\\\"\""); - - assert_eq!(quote_ident(&bt, "a`b"), "`a\\`b`"); - assert_eq!(quote_ident(&bt, "\\"), "`\\\\`"); - assert_eq!(quote_ident(&bt, "\n\r\t"), "`\\n\\r\\t`"); - assert_eq!(quote_ident(&bt, "\u{01}"), "`\\x01`"); - assert_eq!(quote_ident(&bt, "\u{7f}"), "`\\x7F`"); - // Apostrophe is not the closer. Escaping it would change the name. - assert_eq!(quote_ident(&bt, "it's"), "`it's`"); - - assert_eq!(quote_ident(&br, "a]b"), "[a\\]b]"); - assert_eq!(quote_ident(&br, "]"), "[\\]]"); -} - -#[test] -fn write_identifier_trailing_backslash_can_eat_a_backslash_closer() { - // If the engine treats `\` as an escape, a trailing backslash escapes the - // closing delimiter and the next source token is absorbed into the name. - // Doubling mode does not have this hole. Callers must not pair - // `flags::IdentifierEscape::Backslash` with an engine that ignores backslashes, - // and must reject or double a trailing `\` on engines that honor it. - let bt = ident_flags(config::IdentifierFormat::Backtick, config::IdentifierEscape::Backslash); - - let br = ident_flags( - config::IdentifierFormat::SquareBracket, - config::IdentifierEscape::Backslash, - ); - - assert_eq!(quote_ident(&bt, "foo\\"), "`foo\\\\`"); - assert_eq!(quote_ident(&br, "foo\\"), "[foo\\]"); - assert!(quote_ident(&br, "foo\\").ends_with("\\]")); -} - -#[test] -fn write_identifier_comments_and_dots_stay_inside() { - let dq = ident_flags(config::IdentifierFormat::DoubleQuote, config::IdentifierEscape::Double); - - // A newline inside a quoted identifier is legal in some engines and a - // line-comment introducer in others if the closer is lost. Doubling keeps - // the closer, so `--` cannot comment out the rest of the statement. - assert_eq!(quote_ident(&dq, "a\n--\nb"), "\"a\n--\nb\""); - assert_eq!(quote_ident(&dq, "a.b"), "\"a.b\""); - assert_eq!(quote_ident(&dq, "users"), "\"users\""); -} - -#[test] -fn write_bits_spellings_and_unsupported() { - let mut out = String::new(); - let err = config::DialectConfig { - bit_format: config::BitFormat::Unsupported, - ..Default::default() - } - .write_bits(&mut out, "101") - .unwrap_err(); - - assert_eq!(err, errors::FormatError::Unsupported("BitSerializer")); - assert!(out.is_empty(), "unsupported must not write a partial literal"); - - let cases = [ - (config::BitFormat::PrefixBigB, "B'101'"), - (config::BitFormat::PrefixSmallB, "b'101'"), - (config::BitFormat::PostfixBigB, "'101'B"), - ]; - for (format, expected) in cases { - out.clear(); - config::DialectConfig { - bit_format: format, - ..Default::default() - } - .write_bits(&mut out, "101") - .unwrap(); - - assert_eq!(out, expected); - - out.clear(); - config::DialectConfig { - bit_format: format, - ..Default::default() - } - .write_bits(&mut out, "") - .unwrap(); - - assert_eq!(out, expected.replace("101", "")); - } -} - -#[test] -fn write_bits_embeds_input_unchanged() { - // Release builds do not re-check the alphabet. A `'` in `bits` would - // close the literal (`B'''; DROP`), so the debug assert is the guard. - let mut out = String::new(); - config::DialectConfig { - bit_format: config::BitFormat::PrefixBigB, - ..Default::default() - } - .write_bits(&mut out, "0") - .unwrap(); - - assert_eq!(out, "B'0'"); - - out.clear(); - - config::DialectConfig { - bit_format: config::BitFormat::PostfixBigB, - ..Default::default() - } - .write_bits(&mut out, "1") - .unwrap(); - - assert_eq!(out, "'1'B"); -} - -#[cfg(debug_assertions)] -#[test] -#[should_panic(expected = "write_bits: invalid input")] -fn write_bits_panics_on_quote_in_debug() { - let mut out = String::new(); - let _ = config::DialectConfig { - bit_format: config::BitFormat::PrefixBigB, - ..Default::default() - } - .write_bits(&mut out, "1'; DROP TABLE t; --"); -} - -#[test] -fn write_bytes_every_spelling_including_empty_and_quote_byte() { - // 0x27 is ASCII `'`. Hex spellings must emit `27`, never a raw quote. - let quote_byte = [0x27]; - let dead = [0xDE, 0xAD, 0xBE, 0xEF]; - let cases: [(config::BytesFormat, &str, &str); 10] = [ - (config::BytesFormat::PrefixBigX, "X''", "X'27'"), - (config::BytesFormat::PrefixZeroX, "0x", "0x27"), - (config::BytesFormat::SlashXBytea, "'\\x'::bytea", "'\\x27'::bytea"), - (config::BytesFormat::PrefixBigBSlashX, "B''", "B'\\x27'"), - (config::BytesFormat::HexToRaw, "HEXTORAW('')", "HEXTORAW('27')"), - (config::BytesFormat::Unhex, "UNHEX('')", "UNHEX('27')"), - ( - config::BytesFormat::ToVarbyte, - "TO_VARBYTE('', 'hex')", - "TO_VARBYTE('27', 'hex')", - ), - (config::BytesFormat::PostfixXbv, "''XBV", "'27'XBV"), - ( - config::BytesFormat::ToBinary, - "TO_BINARY('', 'HEX')", - "TO_BINARY('27', 'HEX')", - ), - (config::BytesFormat::SlashXBlob, "''::BLOB", "'\\x27'::BLOB"), - ]; - for (format, empty, quoted) in cases { - let flags = config::DialectConfig { - bytes_format: format, - ..Default::default() - }; - let mut out = String::new(); - flags.write_bytes(&mut out, &[]).unwrap(); - assert_eq!(out, empty, "{format:?} empty"); - out.clear(); - flags.write_bytes(&mut out, "e_byte).unwrap(); - assert_eq!(out, quoted, "{format:?} quote byte"); - assert!( - !out.contains("''") || format == config::BytesFormat::SlashXBlob || out.starts_with("''"), - "raw quote leaked into {out}" - ); - out.clear(); - flags.write_bytes(&mut out, &dead).unwrap(); - assert!( - out.contains("DEADBEEF") || out.contains("\\xDE\\xAD\\xBE\\xEF"), - "{out}" - ); - assert!(!out.contains("deadbeef"), "hex must be uppercase: {out}"); - } -} - -#[test] -fn write_bytes_slash_x_blob_empty_is_not_a_slash_escape() { - // Empty `'\x'::BLOB` would be a one-byte escape, not an empty blob. - let flags = config::DialectConfig { - bytes_format: config::BytesFormat::SlashXBlob, - ..Default::default() - }; - let mut out = String::new(); - flags.write_bytes(&mut out, &[]).unwrap(); - assert_eq!(out, "''::BLOB"); - out.clear(); - flags.write_bytes(&mut out, &[0x00]).unwrap(); - assert_eq!(out, "'\\x00'::BLOB"); -} - -#[test] -fn write_bytes_crosses_hex_and_slash_chunk_boundaries() { - let sixty_five = vec![0xABu8; 65]; - let thirty_three = vec![0xFFu8; 33]; - - let mut hex = String::new(); - config::DialectConfig { - bytes_format: config::BytesFormat::PrefixBigX, - ..Default::default() - } - .write_bytes(&mut hex, &sixty_five) - .unwrap(); - - assert_eq!(hex.len(), 2 + 65 * 2 + 1); - assert!(hex.starts_with("X'") && hex.ends_with('\'')); - assert_eq!(&hex[2..hex.len() - 1], &"AB".repeat(65)); - - let mut slash = String::new(); - config::DialectConfig { - bytes_format: config::BytesFormat::PrefixBigBSlashX, - ..Default::default() - } - .write_bytes(&mut slash, &thirty_three) - .unwrap(); - - assert_eq!(slash, format!("B'{}'", "\\xFF".repeat(33))); - - let mut blob = String::new(); - config::DialectConfig { - bytes_format: config::BytesFormat::SlashXBlob, - ..Default::default() - } - .write_bytes(&mut blob, &thirty_three) - .unwrap(); - - assert_eq!(blob, format!("'{}'::BLOB", "\\xFF".repeat(33))); -} - -#[test] -fn write_bytes_zero_prefix_empty_is_not_a_closed_literal() { - // `0x` with no digits is rejected by most engines. Locked so a caller - // that needs an empty blob picks another spelling. - let mut out = String::new(); - config::DialectConfig { - bytes_format: config::BytesFormat::PrefixZeroX, - ..Default::default() - } - .write_bytes(&mut out, &[]) - .unwrap(); - - assert_eq!(out, "0x"); -} - -#[test] -fn write_bytes_function_wrappers_cannot_be_closed_by_payload() { - // The hex alphabet is generated, not copied, so a byte cannot inject a - // quote into HEXTORAW / UNHEX / TO_BINARY / TO_VARBYTE. - let nasty = [0x27, 0x5C, 0x00, 0x22, 0x3B]; - for format in [ - config::BytesFormat::HexToRaw, - config::BytesFormat::Unhex, - config::BytesFormat::ToVarbyte, - config::BytesFormat::ToBinary, - ] { - let mut out = String::new(); - config::DialectConfig { - bytes_format: format, - ..Default::default() - } - .write_bytes(&mut out, &nasty) - .unwrap(); - - assert!(out.contains("275C00223B"), "{format:?} -> {out}"); - let hex = out - .split('\'') - .nth(1) - .unwrap_or_else(|| panic!("missing hex literal in {out}")); - - assert!( - hex.chars().all(|c| c.is_ascii_hexdigit()), - "{format:?} leaked non-hex into {out}" - ); - assert_eq!(hex, "275C00223B"); - } -} - -#[test] -fn writer_errors_surface_as_format_error() { - let mut flags = string_flags(config::StringFormat::PrefixBigN, config::StringEscape::Quote); - let mut writer = FailAfter { remaining: 0 }; - assert_eq!( - flags.write_string_literal(&mut writer, "x").unwrap_err(), - errors::FormatError::Write - ); - - let mut writer = FailAfter { remaining: 0 }; - - flags.bit_format = config::BitFormat::PrefixBigB; - assert!(flags.write_bits(&mut writer, "1").is_err()); -} - -fn column(name: &str) -> crate::expr::enum_::ExprInner { - crate::expr::enum_::ExprInner::Ident(crate::expr::enum_::Ident::from_str(name).unwrap()) -} - -fn binary(op: crate::expr::enum_::BinaryOp) -> crate::expr::enum_::Binary { - crate::expr::enum_::Binary { - left: Box::new(column("a")), - right: Box::new(column("b")), - op, - } -} - -fn render( - py: pyo3::Python<'_>, - flags: config::DialectConfig, - expr: &impl crate::sql::WriteSql, -) -> Result { - let dialect = PyDialect(flags); - let mut buf = crate::sql::BufferedSql::new(py); - expr.write_sql(&dialect, &mut buf)?; - Ok(buf.into_string()) -} - -fn error_text(err: crate::sql::PrepareError) -> String { - match err { - crate::sql::PrepareError::Python(err) => err.to_string(), - other => other.to_string(), - } -} - -/// Infix AND and OR, no NOT, no shifts, XOR as `#`. -/// -/// The old `Bitwise` enum could not name this mix. Each operator is its own bit. -#[test] -fn bitwise_and_or_with_hash_xor_rejects_not_and_shifts() { - use crate::expr::enum_::BinaryOp; - use crate::expr::enum_::Unary; - use crate::expr::enum_::UnaryOp; - - pyo3::Python::attach(|py| { - let flags = config::DialectConfig { - bitwise_operations: config::BitwiseOperations::AND | config::BitwiseOperations::OR, - bitwise_call_syntax: config::BitwiseCallSyntax::Infix, - bitwise_xor_spelling: config::BitwiseXorSpelling::Hash, - ..Default::default() - }; - - assert_eq!(render(py, flags, &binary(BinaryOp::BitAnd)).unwrap(), "\"a\" & \"b\""); - assert_eq!(render(py, flags, &binary(BinaryOp::BitOr)).unwrap(), "\"a\" | \"b\""); - assert_eq!(render(py, flags, &binary(BinaryOp::BitXor)).unwrap(), "\"a\" # \"b\""); - - let not = Unary { - op: UnaryOp::BitNot, - expr: Box::new(column("a")), - }; - assert!(error_text(render(py, flags, ¬).unwrap_err()).contains("bitwise NOT")); - assert!(error_text(render(py, flags, &binary(BinaryOp::LShift)).unwrap_err()).contains("bitwise shift")); - assert!(error_text(render(py, flags, &binary(BinaryOp::RShift)).unwrap_err()).contains("bitwise shift")); - }); -} - -/// `ROW` plus arity 1 rejects `ROW()`. Parentheses with an empty arity mask still need two elements. -#[test] -fn tuple_arity_is_independent_of_syntax() { - use crate::expr::enum_::Tuple; - - pyo3::Python::attach(|py| { - let row = config::DialectConfig { - tuple_syntax: config::TupleSyntax::Row, - tuple_arity: config::TupleArity::ONE, - ..Default::default() - }; - - let one = Tuple { - items: vec![column("a")], - }; - let two = Tuple { - items: vec![column("a"), column("b")], - }; - assert_eq!(render(py, row, &one).unwrap(), "ROW(\"a\")"); - assert_eq!(render(py, row, &two).unwrap(), "ROW(\"a\", \"b\")"); - assert!(error_text(render(py, row, &Tuple { items: vec![] }).unwrap_err()).contains("empty tuple")); - - let parens = config::DialectConfig { - tuple_syntax: config::TupleSyntax::Parentheses, - ..Default::default() - }; - - assert!(error_text(render(py, parens, &one).unwrap_err()).contains("at least two")); - assert_eq!(render(py, parens, &two).unwrap(), "(\"a\", \"b\")"); - }); -} - -/// `SCALAR`, `ANY`, and empty are three different `ESCAPE` policies. -#[test] -fn like_escape_flags_combine() { - use crate::expr::enum_::Like; - use crate::expr::enum_::LikePattern; - - fn like(any: bool, escape: Option) -> Like { - Like { - case_sensitive: true, - negated: false, - expr: Box::new(column("a")), - pattern: if any { - LikePattern::Any(vec![column("p")]) - } else { - LikePattern::One(Box::new(column("p"))) - }, - escape, - } - } - - pyo3::Python::attach(|py| { - let scalar = config::DialectConfig { - like_escape: config::LikeEscape::SCALAR, - like_any: config::LikeAny::PatternList, - ..Default::default() - }; - - let both = config::DialectConfig { - like_escape: config::LikeEscape::SCALAR | config::LikeEscape::ANY, - like_any: config::LikeAny::PatternList, - ..Default::default() - }; - - let none = config::DialectConfig { - like_any: config::LikeAny::PatternList, - ..Default::default() - }; - - assert_eq!( - render(py, scalar, &like(false, Some('!'))).unwrap(), - "\"a\" LIKE \"p\" ESCAPE '!'" - ); - assert!(error_text(render(py, scalar, &like(true, Some('!'))).unwrap_err()).contains("LIKE ANY ESCAPE")); - assert!(render(py, both, &like(true, Some('!'))).unwrap().contains("ESCAPE")); - assert!(error_text(render(py, none, &like(false, Some('!'))).unwrap_err()).contains("LIKE ESCAPE")); - }); -} From 03aa747d84ea4e03dec331d236fd25ecba9d939d Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Sat, 24 Oct 2026 13:46:46 +0330 Subject: [PATCH 07/11] Optimize PyDialect and fix some bugs --- Cargo.toml | 6 +- Makefile | 12 ++- src/ddl/schema.rs | 8 +- src/dialect/base.rs | 77 +++++++++-------- src/dialect/escape.rs | 190 ++++++++++++++++++++++++++++++++++++----- src/expr/base.rs | 12 +-- src/serializers/mod.rs | 8 +- src/sql.rs | 4 +- 8 files changed, 237 insertions(+), 80 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1dac6e2..4cea024 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,12 +29,12 @@ rapidquery-macros = { path = "rapidquery-macros" } smallvec = { version = "1.16.0", default-features = false } uuid = { version = "1.26.1", default-features = false } +itoa = { version = "1.0.18", default-features = false } # ONLY for inspiring the source sea-query = { version = "1.0.2", default-features = false } # ONLY for inspiring the source sqlparser = { version = "0.62.0", default-features = false } zmij = { version = "1.0.23", default-features = false } -itoa = { version = "1.0.18", default-features = false } [build-dependencies] pyo3-build-config = "0.30.1" @@ -59,15 +59,13 @@ print_stderr = "warn" print_stdout = "warn" [profile.dev] -strip = false lto = true -panic = "unwind" [profile.release] opt-level = 3 debug = false strip = false lto = true -panic = "abort" +panic = "unwind" incremental = false codegen-units = 1 diff --git a/Makefile b/Makefile index 4d4aadc..68db7fb 100644 --- a/Makefile +++ b/Makefile @@ -53,10 +53,16 @@ lint-rust: .PHONY: lint ## Lint rust and python source files lint: lint-python lint-rust +.PHONY: test-rust ## Run Rust tests +test-rust: + cargo test --workspace --no-default-features --features auto-initialize + +.PHONY: test-python ## Run Python tests +test-python: + uv run pytest -v -n auto + .PHONY: test ## Run all tests -test: - cargo test --workspace --all --no-default-features --features auto-initialize - uv run pytest -n auto +test: test-rust test-python .PHONY: all ## Run the standard set of checks performed in CI all: format lint build-dev test diff --git a/src/ddl/schema.rs b/src/ddl/schema.rs index e0c5230..831f083 100644 --- a/src/ddl/schema.rs +++ b/src/ddl/schema.rs @@ -123,8 +123,8 @@ impl PyCreateSchema { } /// Render this expression as raw SQL (values inlined as literals). - fn to_sql(&self, py: pyo3::Python, dialect: crate::dialect::PyDialect) -> pyo3::PyResult { - sql::write_sql(py, self, &dialect) + fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> pyo3::PyResult { + sql::write_sql(dialect.py(), self, dialect.get()) } fn __repr__(&self) -> String { @@ -259,8 +259,8 @@ impl PyDropSchema { } /// Render this expression as raw SQL (values inlined as literals). - fn to_sql(&self, py: pyo3::Python, dialect: crate::dialect::PyDialect) -> pyo3::PyResult { - sql::write_sql(py, self, &dialect) + fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> pyo3::PyResult { + sql::write_sql(dialect.py(), self, dialect.get()) } fn __repr__(&self) -> String { diff --git a/src/dialect/base.rs b/src/dialect/base.rs index 1f72226..aa53455 100644 --- a/src/dialect/base.rs +++ b/src/dialect/base.rs @@ -1,5 +1,4 @@ use crate::internal::template; -use crate::internal::utils; #[rapidquery_macros::pyclass("rapidquery.dialect.LikeConfig", from_py_object, eq, hash)] #[rapidquery_macros::define_options] @@ -99,15 +98,24 @@ rapidquery_macros::pyo3_constructor! { #[rapidquery_macros::pyclass("rapidquery.dialect.StringConfig", from_py_object, eq, hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct StringConfig { - /// String template e.g. `E'{quote}'` + /// String template e.g. `E'{hex}'` /// - /// Template key specify escaping method: - /// - `quote`: Escape `'` → `''`. Other characters are unchanged. - /// - `hex`: Escape `\\`, `\'`, `\n`, `\r`, `\t`; other C0/DEL as `\xNN`. - /// - `mysql`: (MySQL-specific) Like `hex`, but also `\"` and `\Z` for 0x1A; tab and other controls pass through. - /// - `unicode`: Other C0/DEL as `\uNNNN`. No `\"`. - /// - `hex_zero`: Other C0/DEL as `\xNN`. - /// - `unicode_quote`: `unicode` plus `\"`. + /// The template key picks how the text is escaped. A string containing `NUL` is rejected. + /// Pick a key that escapes the delimiter used in your template: `'{single}'` or `"{double}"`. + /// + /// Escape by doubling (other characters are unchanged): + /// - `single`: `'` → `''` (standard SQL, SQL Server, Oracle, SQLite, ...) + /// - `double`: `"` → `""` + /// + /// Escape with a backslash (`\` is always escaped): + /// - `hex`: Escape `\\`, `\'`, `\n`, `\r`, `\t`; other C0/DEL as `\xNN`. `"` is unchanged. + /// - `hex_quote`: `hex` plus `\"`. Use it for `"` delimited strings. + /// - `mysql`: (MySQL-specific) Escape `\\`, `\'`, `\"`, `\n`, `\r` and `\Z` for 0x1A; tab and other controls pass through. + /// - `unicode`: Like `hex`, but other C0/DEL as `\uNNNN`. `"` is unchanged. + /// - `unicode_quote`: `unicode` plus `\"`. Use it for `"` delimited strings. + /// + /// Only use a backslash key when the target database really reads backslash escapes + /// inside string literals (e.g. MySQL, BigQuery, Hive, Spark, or PostgreSQL `E'...'`). /// /// Prefer using `StringConfig::write_string_literal` instead of directly using this template. pub template: template::Template, @@ -120,7 +128,7 @@ pub struct StringConfig { rapidquery_macros::pyo3_constructor! { StringConfig => export_string_config_options { - template: template(quote?, hex?, mysql?, unicode?, hex_zero?, unicode_quote?; 1) = "'{quote}'", + template: template(single?, double?, hex?, hex_quote?, mysql?, unicode?, unicode_quote?; 1) = "'{single}'", concat: template(left, right; 2) = "{left} || {right}", } } @@ -384,9 +392,24 @@ pub struct PyDialect { /// Identifier template e.g. `"{double}"` /// - /// Template key specify escaping method: - /// - `double`: Escape identifier delimiters with doubling. - /// - `backslash`: Escape identifier delimiters with backslash. + /// The template key picks which character is escaped and how. Pick the key + /// that matches the delimiter in your template, e.g. `` `{backtick}` `` or `[{bracket}]`. + /// + /// Escape by doubling (`x` becomes `xx`; other characters are unchanged): + /// - `double`: `"` (PostgreSQL, SQLite, Oracle, Snowflake, ...) + /// - `single`: `'` + /// - `bracket`: `]` (SQL Server) + /// - `backtick`: `` ` `` (MySQL, Hive, Spark, Databricks, ...) + /// + /// Escape with a backslash (`\` and the delimiter get a backslash; `\n`, `\r`, `\t` are + /// written as escapes; other control characters as `\xNN`): + /// - `slashdouble`: `"` + /// - `slashsingle`: `'` + /// - `slashbracket`: `]` + /// - `slashbacktick`: `` ` `` (BigQuery) + /// + /// Only use a `slash*` key when the target database really reads backslash + /// escapes inside quoted identifiers. pub identifier: template::Template, /// String literal format, escaping, and concatenation. @@ -414,34 +437,16 @@ pub struct PyDialect { pub schema: SchemaConfig, } -// NOTE: using `crate::typeref::check_exact` has better performance than PyO3 default `FromPyObject`. -impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for PyDialect { - type Error = pyo3::PyErr; - - fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { - unsafe { - // SAFETY: `obj` is a borrowed `&Bound`, so `as_ptr()` is a - // live `PyObject*`. - if crate::typeref::check_exact(obj.as_ptr(), crate::typeref::DIALECT_TYPE) { - let cast = obj.cast_unchecked::(); - return Ok(cast.get().clone()); - } - - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected Dialect, got {}", - utils::get_type_name(obj.py(), obj.as_ptr()) - ) - } - } -} - rapidquery_macros::pyo3_constructor! { PyDialect => export_pydialect_options { placeholder: template(_; 1) = "?{}", like: class LikeConfig = LikeConfig::default(), bitwise: class BitwiseConfig = BitwiseConfig::default(), - identifier: template(double?, backslash?; 1) = "\"{double}\"", + identifier: template( + double?, single?, bracket?, backtick?, + slashdouble?, slashsingle?, slashbracket?, slashbacktick?; + 1 + ) = "\"{double}\"", string: class StringConfig = StringConfig::default(), cast: class CastConfig = CastConfig::default(), tuple: class TupleConfig = TupleConfig::default(), diff --git a/src/dialect/escape.rs b/src/dialect/escape.rs index a8e8b23..33fc418 100644 --- a/src/dialect/escape.rs +++ b/src/dialect/escape.rs @@ -73,18 +73,20 @@ impl base::StringConfig { self.template.write_to( writer, - // quote: Escape ' → ''. Other characters are unchanged. + // single: Escape ' → ''. Other characters are unchanged. + // double: Escape " → "". Other characters are unchanged. // hex: Escape \\, \', \n, \r, \t; other C0/DEL as \xNN. + // hex_quote: hex plus \". // mysql: (MySQL-specific) Like hex, but also \" and \Z for 0x1A; tab and other controls pass through. // unicode: Other C0/DEL as \uNNNN. No \". - // hex_zero: Other C0/DEL as \xNN. // unicode_quote: unicode plus \". |key, out| match key { - "quote" => template::replace_to(raw, '\'', "''", out), - "hex" => write_backslash_hex(out, raw), + "single" => template::replace_to(raw, '\'', "''", out), + "double" => template::replace_to(raw, '"', "\"\"", out), + "hex" => write_backslash_hex(out, raw, false), + "hex_quote" => write_backslash_hex(out, raw, true), "mysql" => write_backslash_mysql(out, raw), "unicode" => write_backslash_unicode(out, raw, false), - "hex_zero" => write_backslash_hex_zero(out, raw), "unicode_quote" => write_backslash_unicode(out, raw, true), _ => unreachable!("invalid key - write_string_literal"), }, @@ -99,11 +101,20 @@ impl base::PyDialect { pub fn write_identifier(&self, writer: &mut W, ident: &str) { self.identifier.write_to( writer, - // double: Escape identifier delimiters with doubling. - // backslash: Escape identifier delimiters with backslash + // double: `"` -> `""` + // single: `'` -> `''` + // bracket: `]` -> `]]` + // backtick: `` ` `` -> ``` `` ``` + // slash*: backslash style, see `write_identifier_backslash_escape` |key, out| match key { "double" => template::replace_to(ident, '"', "\"\"", out), - "backslash" => write_identifier_backtick_escape(out, ident), + "single" => template::replace_to(ident, '\'', "''", out), + "bracket" => template::replace_to(ident, ']', "]]", out), + "backtick" => template::replace_to(ident, '`', "``", out), + "slashdouble" => write_identifier_backslash_escape(out, ident, b'"', "\\\""), + "slashsingle" => write_identifier_backslash_escape(out, ident, b'\'', "\\'"), + "slashbracket" => write_identifier_backslash_escape(out, ident, b']', "\\]"), + "slashbacktick" => write_identifier_backslash_escape(out, ident, b'`', "\\`"), _ => unreachable!("invalid key - write_identifier"), }, ) @@ -174,12 +185,22 @@ fn write_unicode4_escape(writer: &mut W, code: u32) { write_hex2(writer, code as u8) } -/// Use the string escape table, plus `\``. +/// Backslash-style identifier escape for the delimiter byte `quote`. /// -/// `'` is left as itself. Only known escapes are emitted. -fn write_identifier_backtick_escape(writer: &mut W, ident: &str) { +/// `quote` must be an ASCII byte (so it is always a char boundary in UTF-8) and +/// `escaped` is what it becomes, e.g. `b'"'` and `"\\\""`. The backslash itself +/// is always escaped, otherwise an identifier ending in `\` would swallow the +/// closing delimiter. Other quote characters are left as they are. +fn write_identifier_backslash_escape( + writer: &mut W, + ident: &str, + quote: u8, + escaped: &'static str, +) { + debug_assert!(quote.is_ascii()); write_span_escapes(writer, ident, |b| match b { b'\\' => SpanEscape::Literal("\\\\"), + b if b == quote => SpanEscape::Literal(escaped), b'`' => SpanEscape::Literal("\\`"), b'\n' => SpanEscape::Literal("\\n"), b'\r' => SpanEscape::Literal("\\r"), @@ -243,10 +264,11 @@ fn write_span_escapes(writer: &mut W, string: &str, mut cl write_byte_span(writer, bytes, start, bytes.len()) } -fn write_backslash_hex(writer: &mut W, string: &str) { +fn write_backslash_hex(writer: &mut W, string: &str, escape_double_quote: bool) { write_span_escapes(writer, string, |b| match b { b'\\' => SpanEscape::Literal("\\\\"), b'\'' => SpanEscape::Literal("\\'"), + b'"' if escape_double_quote => SpanEscape::Literal("\\\""), b'\n' => SpanEscape::Literal("\\n"), b'\r' => SpanEscape::Literal("\\r"), b'\t' => SpanEscape::Literal("\\t"), @@ -282,15 +304,137 @@ fn write_backslash_unicode(writer: &mut W, string: &str, e }) } -fn write_backslash_hex_zero(writer: &mut W, string: &str) { - write_span_escapes(writer, string, |b| match b { - b'\\' => SpanEscape::Literal("\\\\"), - b'\'' => SpanEscape::Literal("\\'"), - b'\n' => SpanEscape::Literal("\\n"), - b'\r' => SpanEscape::Literal("\\r"), - b'\t' => SpanEscape::Literal("\\t"), - 0 => SpanEscape::Literal("\\0"), - b if b < 32 || b == 127 => SpanEscape::HexByte, - _ => SpanEscape::Keep, - }) +#[cfg(test)] +mod tests { + use crate::dialect::PyDialect; + use crate::dialect::StringConfig; + use crate::internal::template::Template; + + const KEYS: &[(&str, bool)] = &[ + ("double", false), + ("single", false), + ("bracket", false), + ("backtick", false), + ("slashdouble", false), + ("slashsingle", false), + ("slashbracket", false), + ("slashbacktick", false), + ]; + + fn render(tmpl: &str, ident: &str) -> String { + let d = PyDialect { + identifier: Template::parse(tmpl, KEYS, 1, 1).unwrap(), + ..Default::default() + }; + + let mut out = String::new(); + d.write_identifier(&mut out, ident); + out + } + + #[test] + fn doubling_keys() { + assert_eq!(render(r#""{double}""#, r#"a"b"#), r#""a""b""#); + assert_eq!(render("'{single}'", "a'b"), "'a''b'"); + assert_eq!(render("[{bracket}]", "a]b"), "[a]]b]"); + assert_eq!(render("`{backtick}`", "a`b"), "`a``b`"); + } + + #[test] + fn doubling_keys_leave_other_delimiters_alone() { + assert_eq!(render(r#""{double}""#, "it's `x` [y]"), r#""it's `x` [y]""#); + assert_eq!(render("`{backtick}`", r#"a"b"#), r#"`a"b`"#); + assert_eq!(render("[{bracket}]", "a[b"), "[a[b]"); + } + + #[test] + fn slash_keys_escape_delimiter() { + assert_eq!(render(r#""{slashdouble}""#, r#"a"b"#), r#""a\"b""#); + assert_eq!(render("'{slashsingle}'", "a'b"), r"'a\'b'"); + assert_eq!(render("[{slashbracket}]", "a]b"), r"[a\]b]"); + assert_eq!(render("`{slashbacktick}`", "a`b"), r"`a\`b`"); + } + + #[test] + fn slash_keys_escape_the_backslash_itself() { + // An identifier ending in `\` must not escape the closing delimiter. + assert_eq!(render(r#""{slashdouble}""#, r"a\"), r#""a\\""#); + assert_eq!(render("`{slashbacktick}`", r"a\`"), r"`a\\\``"); + } + + #[test] + fn slash_keys_escape_control_characters() { + assert_eq!(render("`{slashbacktick}`", "a\nb\tc\rd"), r"`a\nb\tc\rd`"); + assert_eq!(render("`{slashbacktick}`", "a\u{1}b"), r"`a\x01b`"); + } + + #[test] + fn non_ascii_is_kept() { + assert_eq!(render("`{slashbacktick}`", "é`"), r"`é\``"); + assert_eq!(render("`{backtick}`", "日本`"), "`日本```"); + } + + const STRING_KEYS: &[(&str, bool)] = &[ + ("single", false), + ("double", false), + ("hex", false), + ("hex_quote", false), + ("mysql", false), + ("unicode", false), + ("unicode_quote", false), + ]; + + fn render_string(tmpl: &str, raw: &str) -> Result { + let c = StringConfig { + template: Template::parse(tmpl, STRING_KEYS, 1, 1).unwrap(), + ..Default::default() + }; + + let mut out = String::new(); + c.write_string_literal(&mut out, raw)?; + Ok(out) + } + + #[test] + fn string_doubling_keys() { + assert_eq!(render_string("'{single}'", "it's").unwrap(), "'it''s'"); + assert_eq!( + render_string("\"{double}\"", "say \"hi\"").unwrap(), + "\"say \"\"hi\"\"\"" + ); + // The other quote is left alone. + assert_eq!(render_string("'{single}'", "a\"b").unwrap(), "'a\"b'"); + assert_eq!(render_string("\"{double}\"", "it's").unwrap(), "\"it's\""); + } + + #[test] + fn string_hex_keys() { + assert_eq!(render_string("'{hex}'", "a'b\\c\n").unwrap(), r"'a\'b\\c\n'"); + assert_eq!(render_string("'{hex}'", "a\"b").unwrap(), "'a\"b'"); + assert_eq!(render_string("'{hex}'", "a\u{1}").unwrap(), r"'a\x01'"); + assert_eq!(render_string("\"{hex_quote}\"", "a\"b'c\\").unwrap(), r#""a\"b\'c\\""#); + assert_eq!(render_string("\"{hex_quote}\"", "a\u{1}").unwrap(), r#""a\x01""#); + } + + #[test] + fn string_unicode_keys() { + assert_eq!(render_string("'{unicode}'", "a\u{1}\"").unwrap(), "'a\\u0001\"'"); + assert_eq!( + render_string("'{unicode_quote}'", "a\u{1}\"").unwrap(), + "'a\\u0001\\\"'" + ); + } + + #[test] + fn string_mysql_key() { + assert_eq!(render_string("'{mysql}'", "a\"b\u{1a}\t").unwrap(), "'a\\\"b\\Z\t'"); + } + + #[test] + fn string_rejects_nul() { + assert_eq!( + render_string("'{single}'", "a\0b"), + Err(crate::dialect::errors::FormatError::NulInString) + ); + } } diff --git a/src/expr/base.rs b/src/expr/base.rs index 801c003..ae30d2f 100644 --- a/src/expr/base.rs +++ b/src/expr/base.rs @@ -572,14 +572,14 @@ impl PyExpr { /// Render this expression as a parameterized statement. fn prepare<'py>( - slf: pyo3::PyRef<'py, Self>, - dialect: crate::dialect::PyDialect, + &self, + dialect: pyo3::Bound<'py, crate::dialect::PyDialect>, ) -> pyo3::PyResult<(String, alias::BoundObject<'py>)> { - let (sql, params) = sql::prepare(slf.py(), &slf.0, &dialect)?; + let (sql, params) = sql::prepare(dialect.py(), &self.0, dialect.get())?; let params = unsafe { utils::iterator_to_tuple( - slf.py(), + dialect.py(), params.len() as isize, params.into_iter().map(|x| Ok(x.into_ptr())), )? @@ -588,8 +588,8 @@ impl PyExpr { } /// Render this expression as raw SQL (values inlined as literals). - fn to_sql(slf: pyo3::PyRef<'_, Self>, dialect: crate::dialect::PyDialect) -> pyo3::PyResult { - sql::write_sql(slf.py(), &slf.0, &dialect) + fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> pyo3::PyResult { + sql::write_sql(dialect.py(), &self.0, dialect.get()) } fn __repr__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { diff --git a/src/serializers/mod.rs b/src/serializers/mod.rs index 709e4d8..e6f0788 100644 --- a/src/serializers/mod.rs +++ b/src/serializers/mod.rs @@ -28,9 +28,13 @@ pub struct PySerializer(pub SerializerType); #[pyo3::pymethods] impl PySerializer { /// Renders `object` as a SQL literal for `dialect`. - fn serialize(&self, dialect: crate::dialect::PyDialect, object: alias::BoundObject<'_>) -> pyo3::PyResult { + fn serialize( + &self, + dialect: pyo3::Bound<'_, crate::dialect::PyDialect>, + object: alias::BoundObject<'_>, + ) -> pyo3::PyResult { let mut buf = String::new(); - self.0.serialize(&dialect, &object, &mut buf)?; + self.0.serialize(dialect.get(), &object, &mut buf)?; Ok(buf) } diff --git a/src/sql.rs b/src/sql.rs index cdc8ff5..7d240b9 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -33,7 +33,7 @@ pub trait SqlWriter: FastWrite { /// /// ``` /// pyo3::Python::attach(|py| { -/// let buf = rapidquery::internal::sql::Buffered::new(py); +/// let buf = rapidquery::sql::Buffered::new(py); /// assert_eq!(buf.as_str(), ""); /// }); /// ``` @@ -106,7 +106,7 @@ impl<'py> SqlWriter for Buffer<'py> { /// /// ``` /// pyo3::Python::attach(|py| { -/// let sql = rapidquery::internal::sql::Parameterized::new(py); +/// let sql = rapidquery::sql::Parameterized::new(py); /// assert_eq!(sql.sql(), ""); /// assert!(sql.params().is_empty()); /// }); From 6b19f879d741135b9df07ee2cac53c1ee7631493 Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Sun, 25 Oct 2026 13:58:19 +0330 Subject: [PATCH 08/11] Write Python dialect stub-files --- AGENTS.md | 35 ++ Makefile | 3 +- rapidquery/dialect.py | 861 +++++++++++++++++++++++----------- rapidquery/dialect.pyi | 738 ++++++++++++++++++++++++++++- scripts/check_dialect_stub.sh | 32 ++ src/dialect/base.rs | 8 +- src/dialect/mod.rs | 2 + src/internal/template.rs | 2 +- src/sql.rs | 15 - 9 files changed, 1376 insertions(+), 320 deletions(-) create mode 100644 AGENTS.md create mode 100644 scripts/check_dialect_stub.sh diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0f87685 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# RapidQuery + +Python SQL query builder. Behavior lives in the Rust/PyO3 extension `rapidquery._core`. `rapidquery/*.py` re-exports it. `rapidquery/*.pyi` is hand-written. + +This branch is incomplete. Shipped DDL is `CREATE SCHEMA` / `DROP SCHEMA`. DML, DCL, and TCL are base classes only. + +## Commands + +- Setup: `make install` +- Dev extension: `make build-dev` (`maturin develop`) +- Release extension: `make build-prod` +- Format: `make format` (ruff, then `cargo +nightly fmt`) +- Lint: `make lint` (ruff check/format, nightly rustfmt, `cargo clippy --tests -- -D warnings`) +- Rust tests: `make test-rust` (`--no-default-features --features auto-initialize`). Default features do not link libpython. +- Python tests: `make test-python` (`pytest -v -n auto`). Rebuild the extension first when Rust changed. +- Full local gate: `make` (format, lint, build-dev, both test targets). + +CI (`.github/workflows/test.yml`) runs pytest only, on CPython 3.10–3.15, 3.14t/3.15t, and PyPy 3.11. Free-threaded jobs may fail. Rust tests and clippy are local. + +## Editing + +- Small diffs. Follow nearby modules. No new language, framework, or build tool. +- Python `>=3.10`. Ruff and rustfmt width are 120. Rustfmt uses `imports_granularity = "Item"` (`.rustfmt.toml`). Edition 2024. Nightly is for formatting only. +- A public type needs three matching pieces: `#[rapidquery_macros::pyclass("rapidquery..Name", ...)]`, a re-export in `rapidquery/.py`, and the same shape in `rapidquery/.pyi`. +- New SQL should be Standard SQL or supported by at least three dialects (`CONTRIBUTING.md`). +- `sea-query` and `sqlparser` are unused inspiration dependencies. Leave them unused. +- PyO3 modules set `gil_used = false`. +- After a meaningful change, run the tests that cover it. + +## Leave alone + +- Generated or local artifacts: `target/`, `rapidquery/*.so`, `__pycache__/`, `.venv/`. +- Lockfiles (`uv.lock`, `Cargo.lock`) unless a dependency change is requested. +- Secrets and `.env`. +- Local scratch excluded in `.git/info/exclude`: `a.py`, `builtin.rs`. diff --git a/Makefile b/Makefile index 68db7fb..701ad30 100644 --- a/Makefile +++ b/Makefile @@ -55,11 +55,12 @@ lint: lint-python lint-rust .PHONY: test-rust ## Run Rust tests test-rust: - cargo test --workspace --no-default-features --features auto-initialize + cargo test --workspace --no-default-features --features auto-initialize --all-targets .PHONY: test-python ## Run Python tests test-python: uv run pytest -v -n auto + bash scripts/check_dialect_stub.sh .PHONY: test ## Run all tests test: test-rust test-python diff --git a/rapidquery/dialect.py b/rapidquery/dialect.py index da3c793..85e6263 100644 --- a/rapidquery/dialect.py +++ b/rapidquery/dialect.py @@ -1,285 +1,576 @@ -# from ._core.dialect import ( -# OPT_ARRAY_FORMAT_CONSTRUCTOR, -# OPT_ARRAY_FORMAT_FUNCTION, -# OPT_ARRAY_FORMAT_LIST, -# OPT_ARRAY_FORMAT_UNSUPPORTED, -# OPT_BIT_FORMAT_POSTFIX_BIG_B, -# OPT_BIT_FORMAT_PREFIX_BIG_B, -# OPT_BIT_FORMAT_PREFIX_SMALL_B, -# OPT_BIT_FORMAT_UNSUPPORTED, -# OPT_BITWISE_CALL_SYNTAX_BIT_FUNCTION, -# OPT_BITWISE_CALL_SYNTAX_INFIX, -# OPT_BITWISE_CALL_SYNTAX_SHIFT_FUNCTION, -# OPT_BITWISE_OPERATIONS_AND, -# OPT_BITWISE_OPERATIONS_NOT, -# OPT_BITWISE_OPERATIONS_OR, -# OPT_BITWISE_OPERATIONS_SHL, -# OPT_BITWISE_OPERATIONS_SHR, -# OPT_BITWISE_XOR_SPELLING_BIT_FUNCTION, -# OPT_BITWISE_XOR_SPELLING_CARET, -# OPT_BITWISE_XOR_SPELLING_FUNCTION, -# OPT_BITWISE_XOR_SPELLING_HASH, -# OPT_BITWISE_XOR_SPELLING_UNSUPPORTED, -# OPT_BOOL_FORMAT_INTEGER, -# OPT_BOOL_FORMAT_KEYWORD, -# OPT_BYTES_FORMAT_HEX_TO_RAW, -# OPT_BYTES_FORMAT_POSTFIX_XBV, -# OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X, -# OPT_BYTES_FORMAT_PREFIX_BIG_X, -# OPT_BYTES_FORMAT_PREFIX_ZERO_X, -# OPT_BYTES_FORMAT_SLASH_X_BLOB, -# OPT_BYTES_FORMAT_SLASH_X_BYTEA, -# OPT_BYTES_FORMAT_TO_BINARY, -# OPT_BYTES_FORMAT_TO_VARBYTE, -# OPT_BYTES_FORMAT_UNHEX, -# OPT_CAST_TRY_CAST_SAFE_CAST, -# OPT_CAST_TRY_CAST_TRY_CAST, -# OPT_CAST_TRY_CAST_TRY_CAST_WORD, -# OPT_CAST_TRY_CAST_UNSUPPORTED, -# OPT_COLLATE_SPELLING_FUNCTION_STRING, -# OPT_COLLATE_SPELLING_INFIX_IDENT, -# OPT_COLLATE_SPELLING_UNSUPPORTED, -# OPT_DATE_FORMAT_KEYWORD, -# OPT_DATE_FORMAT_QUOTED, -# OPT_FUNCTION_FILTER_FILTER, -# OPT_FUNCTION_FILTER_FILTER_WHERE, -# OPT_FUNCTION_FILTER_INSIDE_WHERE, -# OPT_FUNCTION_FILTER_UNSUPPORTED, -# OPT_IDENTIFIER_ESCAPE_BACKSLASH, -# OPT_IDENTIFIER_ESCAPE_DOUBLE, -# OPT_IDENTIFIER_FORMAT_BACKTICK, -# OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE, -# OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET, -# OPT_ILIKE_EXPRESSION_STANDARD, -# OPT_ILIKE_EXPRESSION_USE_LIKE, -# OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER, -# OPT_INET_FORMAT_CAST, -# OPT_INET_FORMAT_QUOTED, -# OPT_INET_FORMAT_UNSUPPORTED, -# OPT_IS_DISTINCT_FROM_SPELLING_NULL_SAFE_EQUAL, -# OPT_IS_DISTINCT_FROM_SPELLING_STANDARD, -# OPT_IS_DISTINCT_FROM_SPELLING_UNSUPPORTED, -# OPT_JSON_FORMAT_JSON_KEYWORD, -# OPT_JSON_FORMAT_PARSE_JSON, -# OPT_JSON_FORMAT_QUOTED, -# OPT_LIKE_ANY_ARRAY_CONSTRUCTOR, -# OPT_LIKE_ANY_OR_EXPANSION, -# OPT_LIKE_ANY_PATTERN_LIST, -# OPT_LIKE_ESCAPE_ANY, -# OPT_LIKE_ESCAPE_SCALAR, -# OPT_MACADDR_FORMAT_CAST, -# OPT_MACADDR_FORMAT_QUOTED, -# OPT_MACADDR_FORMAT_UNSUPPORTED, -# OPT_MODULO_SPELLING_FUNCTION, -# OPT_MODULO_SPELLING_INFIX, -# OPT_MODULO_SPELLING_PERCENT, -# OPT_NULL_TREATMENT_INSIDE_PARENS, -# OPT_NULL_TREATMENT_POSTFIX, -# OPT_NULL_TREATMENT_UNSUPPORTED, -# OPT_PLACEHOLDER_SYNTAX_NUMBERED, -# OPT_PLACEHOLDER_SYNTAX_SIMPLE, -# OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS, -# OPT_RLIKE_EXPRESSION_REGEXP_LIKE, -# OPT_RLIKE_EXPRESSION_REGEXP_MATCHES, -# OPT_RLIKE_EXPRESSION_STANDARD, -# OPT_RLIKE_EXPRESSION_TILDE, -# OPT_RLIKE_EXPRESSION_UNSUPPORTED, -# OPT_SCHEMA_AUTHORIZATION_STYLE_OPTIONAL, -# OPT_SCHEMA_AUTHORIZATION_STYLE_UNSUPPORTED, -# OPT_SCHEMA_FEATURES_DROP_CASCADE, -# OPT_SCHEMA_FEATURES_DROP_RESTRICT, -# OPT_SCHEMA_FEATURES_EMBEDDED_DDL, -# OPT_SCHEMA_FEATURES_IF_NOT_EXISTS, -# OPT_SCHEMA_FEATURES_IS_DATABASE, -# OPT_SCHEMA_FEATURES_UNSUPPORTED, -# OPT_SCHEMA_LOCATION_STYLE_EXTERNAL_VOLUME, -# OPT_SCHEMA_LOCATION_STYLE_LOCATION_KEYWORD, -# OPT_SCHEMA_LOCATION_STYLE_MANAGED_LOCATION_KEYWORD, -# OPT_SCHEMA_LOCATION_STYLE_UNSUPPORTED, -# OPT_STRING_CONCAT_CONCAT_FUNCTION, -# OPT_STRING_CONCAT_PIPES, -# OPT_STRING_CONCAT_PLUS, -# OPT_STRING_ESCAPE_BACKSLASH_HEX, -# OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO, -# OPT_STRING_ESCAPE_BACKSLASH_MYSQL, -# OPT_STRING_ESCAPE_BACKSLASH_UNICODE, -# OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE, -# OPT_STRING_ESCAPE_QUOTE, -# OPT_STRING_FORMAT_PREFIX_BIG_E, -# OPT_STRING_FORMAT_PREFIX_BIG_N, -# OPT_STRING_FORMAT_SINGLE_QUOTE, -# OPT_TIME_FORMAT_KEYWORD, -# OPT_TIME_FORMAT_QUOTED, -# OPT_TIMESTAMP_FORMAT_QUOTED, -# OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ, -# OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE, -# OPT_TRIM_SPELLING_FUNCTION, -# OPT_TRIM_SPELLING_SPACES_ONLY, -# OPT_TRIM_SPELLING_SQL_FROM, -# OPT_TUPLE_ARITY_EMPTY, -# OPT_TUPLE_ARITY_ONE, -# OPT_TUPLE_SYNTAX_PARENTHESES, -# OPT_TUPLE_SYNTAX_ROW, -# OPT_TUPLE_SYNTAX_STRUCT, -# OPT_TUPLE_SYNTAX_UNSUPPORTED, -# OPT_UUID_FORMAT_CAST_UUID, -# OPT_UUID_FORMAT_QUOTED, -# OPT_UUID_FORMAT_UUID_KEYWORD, -# BigQuery, -# Databricks, -# Dialect, -# DuckDb, -# Hive, -# MsSql, -# MySql, -# Oracle, -# PostgreSql, -# Redshift, -# Snowflake, -# Spark, -# Sqlite, -# Teradata, -# ) -# -# __all__ = [ -# "OPT_ARRAY_FORMAT_CONSTRUCTOR", -# "OPT_ARRAY_FORMAT_FUNCTION", -# "OPT_ARRAY_FORMAT_LIST", -# "OPT_ARRAY_FORMAT_UNSUPPORTED", -# "OPT_BITWISE_CALL_SYNTAX_BIT_FUNCTION", -# "OPT_BITWISE_CALL_SYNTAX_INFIX", -# "OPT_BITWISE_CALL_SYNTAX_SHIFT_FUNCTION", -# "OPT_BITWISE_OPERATIONS_AND", -# "OPT_BITWISE_OPERATIONS_NOT", -# "OPT_BITWISE_OPERATIONS_OR", -# "OPT_BITWISE_OPERATIONS_SHL", -# "OPT_BITWISE_OPERATIONS_SHR", -# "OPT_BITWISE_XOR_SPELLING_BIT_FUNCTION", -# "OPT_BITWISE_XOR_SPELLING_CARET", -# "OPT_BITWISE_XOR_SPELLING_FUNCTION", -# "OPT_BITWISE_XOR_SPELLING_HASH", -# "OPT_BITWISE_XOR_SPELLING_UNSUPPORTED", -# "OPT_BIT_FORMAT_POSTFIX_BIG_B", -# "OPT_BIT_FORMAT_PREFIX_BIG_B", -# "OPT_BIT_FORMAT_PREFIX_SMALL_B", -# "OPT_BIT_FORMAT_UNSUPPORTED", -# "OPT_BOOL_FORMAT_INTEGER", -# "OPT_BOOL_FORMAT_KEYWORD", -# "OPT_BYTES_FORMAT_HEX_TO_RAW", -# "OPT_BYTES_FORMAT_POSTFIX_XBV", -# "OPT_BYTES_FORMAT_PREFIX_BIG_B_SLASH_X", -# "OPT_BYTES_FORMAT_PREFIX_BIG_X", -# "OPT_BYTES_FORMAT_PREFIX_ZERO_X", -# "OPT_BYTES_FORMAT_SLASH_X_BLOB", -# "OPT_BYTES_FORMAT_SLASH_X_BYTEA", -# "OPT_BYTES_FORMAT_TO_BINARY", -# "OPT_BYTES_FORMAT_TO_VARBYTE", -# "OPT_BYTES_FORMAT_UNHEX", -# "OPT_CAST_TRY_CAST_SAFE_CAST", -# "OPT_CAST_TRY_CAST_TRY_CAST", -# "OPT_CAST_TRY_CAST_TRY_CAST_WORD", -# "OPT_CAST_TRY_CAST_UNSUPPORTED", -# "OPT_COLLATE_SPELLING_FUNCTION_STRING", -# "OPT_COLLATE_SPELLING_INFIX_IDENT", -# "OPT_COLLATE_SPELLING_UNSUPPORTED", -# "OPT_DATE_FORMAT_KEYWORD", -# "OPT_DATE_FORMAT_QUOTED", -# "OPT_FUNCTION_FILTER_FILTER", -# "OPT_FUNCTION_FILTER_FILTER_WHERE", -# "OPT_FUNCTION_FILTER_INSIDE_WHERE", -# "OPT_FUNCTION_FILTER_UNSUPPORTED", -# "OPT_IDENTIFIER_ESCAPE_BACKSLASH", -# "OPT_IDENTIFIER_ESCAPE_DOUBLE", -# "OPT_IDENTIFIER_FORMAT_BACKTICK", -# "OPT_IDENTIFIER_FORMAT_DOUBLE_QUOTE", -# "OPT_IDENTIFIER_FORMAT_SQUARE_BRACKET", -# "OPT_ILIKE_EXPRESSION_STANDARD", -# "OPT_ILIKE_EXPRESSION_USE_LIKE", -# "OPT_ILIKE_EXPRESSION_USE_LIKE_LOWER", -# "OPT_INET_FORMAT_CAST", -# "OPT_INET_FORMAT_QUOTED", -# "OPT_INET_FORMAT_UNSUPPORTED", -# "OPT_IS_DISTINCT_FROM_SPELLING_NULL_SAFE_EQUAL", -# "OPT_IS_DISTINCT_FROM_SPELLING_STANDARD", -# "OPT_IS_DISTINCT_FROM_SPELLING_UNSUPPORTED", -# "OPT_JSON_FORMAT_JSON_KEYWORD", -# "OPT_JSON_FORMAT_PARSE_JSON", -# "OPT_JSON_FORMAT_QUOTED", -# "OPT_LIKE_ANY_ARRAY_CONSTRUCTOR", -# "OPT_LIKE_ANY_OR_EXPANSION", -# "OPT_LIKE_ANY_PATTERN_LIST", -# "OPT_LIKE_ESCAPE_ANY", -# "OPT_LIKE_ESCAPE_SCALAR", -# "OPT_MACADDR_FORMAT_CAST", -# "OPT_MACADDR_FORMAT_QUOTED", -# "OPT_MACADDR_FORMAT_UNSUPPORTED", -# "OPT_MODULO_SPELLING_FUNCTION", -# "OPT_MODULO_SPELLING_INFIX", -# "OPT_MODULO_SPELLING_PERCENT", -# "OPT_NULL_TREATMENT_INSIDE_PARENS", -# "OPT_NULL_TREATMENT_POSTFIX", -# "OPT_NULL_TREATMENT_UNSUPPORTED", -# "OPT_PLACEHOLDER_SYNTAX_NUMBERED", -# "OPT_PLACEHOLDER_SYNTAX_SIMPLE", -# "OPT_RLIKE_EXPRESSION_REGEXP_CONTAINS", -# "OPT_RLIKE_EXPRESSION_REGEXP_LIKE", -# "OPT_RLIKE_EXPRESSION_REGEXP_MATCHES", -# "OPT_RLIKE_EXPRESSION_STANDARD", -# "OPT_RLIKE_EXPRESSION_TILDE", -# "OPT_RLIKE_EXPRESSION_UNSUPPORTED", -# "OPT_SCHEMA_AUTHORIZATION_STYLE_OPTIONAL", -# "OPT_SCHEMA_AUTHORIZATION_STYLE_UNSUPPORTED", -# "OPT_SCHEMA_FEATURES_DROP_CASCADE", -# "OPT_SCHEMA_FEATURES_DROP_RESTRICT", -# "OPT_SCHEMA_FEATURES_EMBEDDED_DDL", -# "OPT_SCHEMA_FEATURES_IF_NOT_EXISTS", -# "OPT_SCHEMA_FEATURES_IS_DATABASE", -# "OPT_SCHEMA_FEATURES_UNSUPPORTED", -# "OPT_SCHEMA_LOCATION_STYLE_EXTERNAL_VOLUME", -# "OPT_SCHEMA_LOCATION_STYLE_LOCATION_KEYWORD", -# "OPT_SCHEMA_LOCATION_STYLE_MANAGED_LOCATION_KEYWORD", -# "OPT_SCHEMA_LOCATION_STYLE_UNSUPPORTED", -# "OPT_STRING_CONCAT_CONCAT_FUNCTION", -# "OPT_STRING_CONCAT_PIPES", -# "OPT_STRING_CONCAT_PLUS", -# "OPT_STRING_ESCAPE_BACKSLASH_HEX", -# "OPT_STRING_ESCAPE_BACKSLASH_HEX_ZERO", -# "OPT_STRING_ESCAPE_BACKSLASH_MYSQL", -# "OPT_STRING_ESCAPE_BACKSLASH_UNICODE", -# "OPT_STRING_ESCAPE_BACKSLASH_UNICODE_QUOTE", -# "OPT_STRING_ESCAPE_QUOTE", -# "OPT_STRING_FORMAT_PREFIX_BIG_E", -# "OPT_STRING_FORMAT_PREFIX_BIG_N", -# "OPT_STRING_FORMAT_SINGLE_QUOTE", -# "OPT_TIMESTAMP_FORMAT_QUOTED", -# "OPT_TIMESTAMP_FORMAT_TIMESTAMPTZ", -# "OPT_TIMESTAMP_FORMAT_WITH_TIMEZONE", -# "OPT_TIME_FORMAT_KEYWORD", -# "OPT_TIME_FORMAT_QUOTED", -# "OPT_TRIM_SPELLING_FUNCTION", -# "OPT_TRIM_SPELLING_SPACES_ONLY", -# "OPT_TRIM_SPELLING_SQL_FROM", -# "OPT_TUPLE_ARITY_EMPTY", -# "OPT_TUPLE_ARITY_ONE", -# "OPT_TUPLE_SYNTAX_PARENTHESES", -# "OPT_TUPLE_SYNTAX_ROW", -# "OPT_TUPLE_SYNTAX_STRUCT", -# "OPT_TUPLE_SYNTAX_UNSUPPORTED", -# "OPT_UUID_FORMAT_CAST_UUID", -# "OPT_UUID_FORMAT_QUOTED", -# "OPT_UUID_FORMAT_UUID_KEYWORD", -# "BigQuery", -# "Databricks", -# "Dialect", -# "DuckDb", -# "Hive", -# "MsSql", -# "MySql", -# "Oracle", -# "PostgreSql", -# "Redshift", -# "Snowflake", -# "Spark", -# "Sqlite", -# "Teradata", -# ] +"""SQL dialects and the flags that configure one. + +`Dialect` chooses quoting, literal spelling, and which syntax is legal. The +names at the bottom are built-in configurations. + +A template key selects an escape (`{hex}`, `{double}`, `{slashx}`, ...). A +positional `{}` is raw text for dates, times, timestamps, UUIDs, inet, and +MAC addresses, so the quotes belong in the template. JSON and schema location +receive an already quoted string. + +`placeholder` may be a bare marker (`'?'`) or include the 1-based index +(`'?{}'`, `'${}'`, `':{}'`). `ILIKE` has no case-sensitive `LIKE` fallback: +MsSql, MySql, and Sqlite reject it. +""" + +from ._core.dialect import ( + OPT_ANY_ARRAY_CONSTRUCTOR, + OPT_ANY_OR_EXPANSION, + OPT_ANY_PATTERN_LIST, + OPT_ARITY_EMPTY, + OPT_ARITY_ONE, + OPT_AUTHORIZATION_OPTIONAL, + OPT_AUTHORIZATION_UNSUPPORTED, + OPT_BOOLEAN_INTEGER, + OPT_BOOLEAN_KEYWORD, + OPT_COLLATE_FUNCTION_STRING, + OPT_COLLATE_INFIX_IDENT, + OPT_COLLATE_UNSUPPORTED, + OPT_ESCAPE_ANY, + OPT_ESCAPE_SCALAR, + OPT_FEATURES_DROP_CASCADE, + OPT_FEATURES_DROP_RESTRICT, + OPT_FEATURES_EMBEDDED_DDL, + OPT_FEATURES_IF_NOT_EXISTS, + OPT_FEATURES_IS_DATABASE, + OPT_FEATURES_SUPPORTED, + OPT_FUNCTION_FILTER_FILTER, + OPT_FUNCTION_FILTER_FILTER_WHERE, + OPT_FUNCTION_FILTER_INSIDE_WHERE, + OPT_FUNCTION_FILTER_UNSUPPORTED, + OPT_ILIKE_STANDARD, + OPT_ILIKE_UNSUPPORTED, + OPT_ILIKE_USE_LIKE_LOWER, + OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL, + OPT_IS_DISTINCT_FROM_STANDARD, + OPT_IS_DISTINCT_FROM_UNSUPPORTED, + OPT_MODULO_FUNCTION, + OPT_MODULO_INFIX, + OPT_MODULO_PERCENT, + OPT_NULL_TREATMENT_INSIDE_PARENS, + OPT_NULL_TREATMENT_POSTFIX, + OPT_NULL_TREATMENT_UNSUPPORTED, + OPT_OPERATIONS_AND, + OPT_OPERATIONS_NOT, + OPT_OPERATIONS_OR, + OPT_OPERATIONS_SHL, + OPT_OPERATIONS_SHR, + OPT_REGEX_REGEXP_CONTAINS, + OPT_REGEX_REGEXP_LIKE, + OPT_REGEX_REGEXP_MATCHES, + OPT_REGEX_STANDARD, + OPT_REGEX_TILDE, + OPT_REGEX_UNSUPPORTED, + OPT_SYNTAX_BIT_FUNCTION, + OPT_SYNTAX_INFIX, + OPT_SYNTAX_PARENTHESES, + OPT_SYNTAX_ROW, + OPT_SYNTAX_SHIFT_FUNCTION, + OPT_SYNTAX_STRUCT, + OPT_SYNTAX_UNSUPPORTED, + OPT_TRIM_FUNCTION, + OPT_TRIM_SPACES_ONLY, + OPT_TRIM_SQL_FROM, + OPT_XOR_BIT_FUNCTION, + OPT_XOR_CARET, + OPT_XOR_FUNCTION, + OPT_XOR_HASH, + OPT_XOR_UNSUPPORTED, + AggregateConfig, + BitwiseConfig, + CastConfig, + Dialect, + LikeConfig, + LiteralConfig, + OperatorConfig, + SchemaConfig, + StringConfig, + TemporalConfig, + TupleConfig, +) + +_ALL_BITWISE = OPT_OPERATIONS_AND | OPT_OPERATIONS_OR | OPT_OPERATIONS_NOT | OPT_OPERATIONS_SHL | OPT_OPERATIONS_SHR + +__all__ = [ + "OPT_ANY_ARRAY_CONSTRUCTOR", + "OPT_ANY_OR_EXPANSION", + "OPT_ANY_PATTERN_LIST", + "OPT_ARITY_EMPTY", + "OPT_ARITY_ONE", + "OPT_AUTHORIZATION_OPTIONAL", + "OPT_AUTHORIZATION_UNSUPPORTED", + "OPT_BOOLEAN_INTEGER", + "OPT_BOOLEAN_KEYWORD", + "OPT_COLLATE_FUNCTION_STRING", + "OPT_COLLATE_INFIX_IDENT", + "OPT_COLLATE_UNSUPPORTED", + "OPT_ESCAPE_ANY", + "OPT_ESCAPE_SCALAR", + "OPT_FEATURES_DROP_CASCADE", + "OPT_FEATURES_DROP_RESTRICT", + "OPT_FEATURES_EMBEDDED_DDL", + "OPT_FEATURES_IF_NOT_EXISTS", + "OPT_FEATURES_IS_DATABASE", + "OPT_FEATURES_SUPPORTED", + "OPT_FUNCTION_FILTER_FILTER", + "OPT_FUNCTION_FILTER_FILTER_WHERE", + "OPT_FUNCTION_FILTER_INSIDE_WHERE", + "OPT_FUNCTION_FILTER_UNSUPPORTED", + "OPT_ILIKE_STANDARD", + "OPT_ILIKE_UNSUPPORTED", + "OPT_ILIKE_USE_LIKE_LOWER", + "OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL", + "OPT_IS_DISTINCT_FROM_STANDARD", + "OPT_IS_DISTINCT_FROM_UNSUPPORTED", + "OPT_MODULO_FUNCTION", + "OPT_MODULO_INFIX", + "OPT_MODULO_PERCENT", + "OPT_NULL_TREATMENT_INSIDE_PARENS", + "OPT_NULL_TREATMENT_POSTFIX", + "OPT_NULL_TREATMENT_UNSUPPORTED", + "OPT_OPERATIONS_AND", + "OPT_OPERATIONS_NOT", + "OPT_OPERATIONS_OR", + "OPT_OPERATIONS_SHL", + "OPT_OPERATIONS_SHR", + "OPT_REGEX_REGEXP_CONTAINS", + "OPT_REGEX_REGEXP_LIKE", + "OPT_REGEX_REGEXP_MATCHES", + "OPT_REGEX_STANDARD", + "OPT_REGEX_TILDE", + "OPT_REGEX_UNSUPPORTED", + "OPT_SYNTAX_BIT_FUNCTION", + "OPT_SYNTAX_INFIX", + "OPT_SYNTAX_PARENTHESES", + "OPT_SYNTAX_ROW", + "OPT_SYNTAX_SHIFT_FUNCTION", + "OPT_SYNTAX_STRUCT", + "OPT_SYNTAX_UNSUPPORTED", + "OPT_TRIM_FUNCTION", + "OPT_TRIM_SPACES_ONLY", + "OPT_TRIM_SQL_FROM", + "OPT_XOR_BIT_FUNCTION", + "OPT_XOR_CARET", + "OPT_XOR_FUNCTION", + "OPT_XOR_HASH", + "OPT_XOR_UNSUPPORTED", + "AggregateConfig", + "BigQuery", + "BitwiseConfig", + "CastConfig", + "Databricks", + "Dialect", + "DuckDb", + "Hive", + "LikeConfig", + "LiteralConfig", + "MsSql", + "MySql", + "OperatorConfig", + "Oracle", + "PostgreSql", + "Redshift", + "SchemaConfig", + "Snowflake", + "Spark", + "Sqlite", + "StringConfig", + "TemporalConfig", + "Teradata", + "TupleConfig", +] + +BigQuery = Dialect( + placeholder="?", + like=LikeConfig( + ilike=OPT_ILIKE_USE_LIKE_LOWER, + regex=OPT_REGEX_REGEXP_CONTAINS, + ), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + identifier="`{slashbacktick}`", + string=StringConfig(template="'{hex}'"), + cast=CastConfig(try_cast="SAFE_CAST({expr} AS {type})"), + temporal=TemporalConfig( + date="DATE '{}'", + time="TIME '{}'", + datetime_naive="TIMESTAMP '{}'", + datetime_aware="TIMESTAMP WITH TIMEZONE '{}'", + ), + literal=LiteralConfig( + bytes="B'{slashx}'", + uuid="'{}'", + json="JSON {}", + array="ARRAY[{}]", + ), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, + trim=OPT_TRIM_FUNCTION, + collate=OPT_COLLATE_FUNCTION_STRING, + ), + aggregate=AggregateConfig( + null_treatment=OPT_NULL_TREATMENT_INSIDE_PARENS, + function_filter=OPT_FUNCTION_FILTER_INSIDE_WHERE, + ), + schema=SchemaConfig(features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS), +) + +Databricks = Dialect( + placeholder="?", + like=LikeConfig( + escape=OPT_ESCAPE_SCALAR, + any=OPT_ANY_PATTERN_LIST, + ilike=OPT_ILIKE_STANDARD, + regex=OPT_REGEX_STANDARD, + ), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + identifier="`{backtick}`", + string=StringConfig(template="'{unicode_quote}'"), + cast=CastConfig(supports_double_colon=True), + tuple=TupleConfig(syntax=OPT_SYNTAX_STRUCT, arity=OPT_ARITY_ONE), + temporal=TemporalConfig( + date="DATE '{}'", + time="'{}'", + datetime_naive="TIMESTAMP '{}'", + datetime_aware="TIMESTAMP WITH TIMEZONE '{}'", + ), + literal=LiteralConfig(uuid="'{}'"), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, + collate=OPT_COLLATE_INFIX_IDENT, + ), + aggregate=AggregateConfig( + null_treatment=OPT_NULL_TREATMENT_POSTFIX, + function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE, + ), + schema=SchemaConfig( + features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS, + location=" LOCATION {}", + ), +) + +DuckDb = Dialect( + placeholder="${}", + like=LikeConfig( + escape=OPT_ESCAPE_SCALAR, + ilike=OPT_ILIKE_STANDARD, + regex=OPT_REGEX_REGEXP_MATCHES, + supports_similar_to=True, + ), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_FUNCTION), + cast=CastConfig(supports_double_colon=True), + tuple=TupleConfig(syntax=OPT_SYNTAX_ROW, arity=OPT_ARITY_EMPTY | OPT_ARITY_ONE), + temporal=TemporalConfig( + date="DATE '{}'", + time="TIME '{}'", + datetime_naive="TIMESTAMP '{}'", + datetime_aware="TIMESTAMPTZ '{}'", + ), + literal=LiteralConfig( + bit="B'{}'", + bytes="'{slashx}'::BLOB", + inet="'{}'::inet", + uuid="UUID '{}'", + array="[{}]", + ), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, + trim=OPT_TRIM_FUNCTION, + collate=OPT_COLLATE_INFIX_IDENT, + ), + aggregate=AggregateConfig( + null_treatment=OPT_NULL_TREATMENT_INSIDE_PARENS, + function_filter=OPT_FUNCTION_FILTER_FILTER, + ), + schema=SchemaConfig(features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS), +) + +Hive = Dialect( + placeholder="?", + like=LikeConfig( + ilike=OPT_ILIKE_USE_LIKE_LOWER, + regex=OPT_REGEX_STANDARD, + ), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + identifier="`{backtick}`", + string=StringConfig(template="'{unicode}'"), + cast=CastConfig(try_cast=None), + tuple=TupleConfig(syntax=OPT_SYNTAX_STRUCT, arity=OPT_ARITY_ONE), + temporal=TemporalConfig( + date="DATE '{}'", + time="'{}'", + datetime_naive="TIMESTAMP '{}'", + datetime_aware="TIMESTAMP WITH TIMEZONE '{}'", + ), + literal=LiteralConfig( + bytes="UNHEX('{hex}')", + uuid="'{}'", + array="array({})", + ), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL, + trim=OPT_TRIM_SPACES_ONLY, + ), + schema=SchemaConfig( + features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_IS_DATABASE, + location=" LOCATION {}", + ), +) + +MsSql = Dialect( + placeholder="?", + like=LikeConfig(escape=OPT_ESCAPE_SCALAR), + bitwise=BitwiseConfig( + operations=OPT_OPERATIONS_AND | OPT_OPERATIONS_OR | OPT_OPERATIONS_NOT, + xor=OPT_XOR_CARET, + ), + identifier="[{bracket}]", + string=StringConfig(template="N'{single}'", concat="{left} + {right}"), + temporal=TemporalConfig( + date="N'{}'", + time="N'{}'", + datetime_naive="N'{}'", + datetime_aware="N'{}'", + ), + literal=LiteralConfig( + boolean=OPT_BOOLEAN_INTEGER, + bytes="0x{hex}", + uuid="N'{}'", + ), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, + collate=OPT_COLLATE_INFIX_IDENT, + ), + aggregate=AggregateConfig(null_treatment=OPT_NULL_TREATMENT_POSTFIX), + schema=SchemaConfig( + features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_EMBEDDED_DDL, + authorization=OPT_AUTHORIZATION_OPTIONAL, + ), +) + +MySql = Dialect( + placeholder="?", + like=LikeConfig(escape=OPT_ESCAPE_SCALAR, regex=OPT_REGEX_STANDARD), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + identifier="`{backtick}`", + string=StringConfig(template="'{mysql}'", concat="CONCAT({left}, {right})"), + cast=CastConfig(try_cast=None), + tuple=TupleConfig(syntax=OPT_SYNTAX_ROW, arity=OPT_ARITY_ONE), + temporal=TemporalConfig( + date="'{}'", + time="'{}'", + datetime_naive="'{}'", + datetime_aware="'{}'", + ), + literal=LiteralConfig( + bit="b'{}'", + boolean=OPT_BOOLEAN_INTEGER, + uuid="'{}'", + ), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL, + collate=OPT_COLLATE_INFIX_IDENT, + ), + schema=SchemaConfig( + features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_IS_DATABASE, + ), +) + +Oracle = Dialect( + placeholder=":{}", + like=LikeConfig( + escape=OPT_ESCAPE_SCALAR, + ilike=OPT_ILIKE_USE_LIKE_LOWER, + regex=OPT_REGEX_REGEXP_LIKE, + ), + bitwise=BitwiseConfig(operations=OPT_OPERATIONS_AND, syntax=OPT_SYNTAX_BIT_FUNCTION), + cast=CastConfig(try_cast=None), + temporal=TemporalConfig( + date="DATE '{}'", + time="'{}'", + datetime_naive="TIMESTAMP '{}'", + datetime_aware="TIMESTAMP WITH TIMEZONE '{}'", + ), + literal=LiteralConfig( + boolean=OPT_BOOLEAN_INTEGER, + bytes="HEXTORAW('{hex}')", + uuid="'{}'", + ), + operator=OperatorConfig(modulo=OPT_MODULO_FUNCTION, collate=OPT_COLLATE_INFIX_IDENT), + aggregate=AggregateConfig( + null_treatment=OPT_NULL_TREATMENT_POSTFIX, + function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE, + ), + schema=SchemaConfig(features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_EMBEDDED_DDL), +) + +PostgreSql = Dialect( + placeholder="${}", + like=LikeConfig( + escape=OPT_ESCAPE_SCALAR, + any=OPT_ANY_ARRAY_CONSTRUCTOR, + ilike=OPT_ILIKE_STANDARD, + regex=OPT_REGEX_TILDE, + supports_similar_to=True, + ), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_HASH), + string=StringConfig(template="E'{hex}'"), + cast=CastConfig(try_cast=None, supports_double_colon=True), + tuple=TupleConfig(syntax=OPT_SYNTAX_ROW, arity=OPT_ARITY_EMPTY | OPT_ARITY_ONE), + temporal=TemporalConfig( + date="DATE E'{}'", + time="TIME E'{}'", + datetime_naive="TIMESTAMP E'{}'", + datetime_aware="TIMESTAMPTZ E'{}'", + ), + literal=LiteralConfig( + bit="B'{}'", + bytes=r"'\x{hex}'::bytea", + inet="E'{}'::inet", + macaddr="E'{}'::macaddr", + uuid="E'{}'::uuid", + array="ARRAY[{}]", + ), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, + collate=OPT_COLLATE_INFIX_IDENT, + ), + aggregate=AggregateConfig(function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE), + schema=SchemaConfig( + features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_EMBEDDED_DDL, + authorization=OPT_AUTHORIZATION_OPTIONAL, + ), +) + +Redshift = Dialect( + placeholder="${}", + like=LikeConfig( + escape=OPT_ESCAPE_SCALAR, + ilike=OPT_ILIKE_STANDARD, + regex=OPT_REGEX_TILDE, + supports_similar_to=True, + ), + bitwise=BitwiseConfig(operations=OPT_OPERATIONS_AND | OPT_OPERATIONS_SHL | OPT_OPERATIONS_SHR), + cast=CastConfig(supports_double_colon=True), + temporal=TemporalConfig( + date="'{}'", + time="'{}'", + datetime_naive="'{}'", + datetime_aware="'{}'", + ), + literal=LiteralConfig(bytes="TO_VARBYTE('{hex}', 'hex')", uuid="'{}'"), + operator=OperatorConfig(collate=OPT_COLLATE_FUNCTION_STRING), + aggregate=AggregateConfig(null_treatment=OPT_NULL_TREATMENT_POSTFIX), + schema=SchemaConfig( + features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_EMBEDDED_DDL, + authorization=OPT_AUTHORIZATION_OPTIONAL, + ), +) + +Snowflake = Dialect( + placeholder=":{}", + like=LikeConfig( + escape=OPT_ESCAPE_SCALAR | OPT_ESCAPE_ANY, + any=OPT_ANY_PATTERN_LIST, + ilike=OPT_ILIKE_STANDARD, + regex=OPT_REGEX_STANDARD, + ), + bitwise=BitwiseConfig( + operations=_ALL_BITWISE, + syntax=OPT_SYNTAX_BIT_FUNCTION, + xor=OPT_XOR_BIT_FUNCTION, + ), + string=StringConfig(template="'{hex}'"), + cast=CastConfig(supports_double_colon=True), + temporal=TemporalConfig( + date="DATE '{}'", + time="'{}'", + datetime_naive="TIMESTAMP '{}'", + datetime_aware="TIMESTAMPTZ '{}'", + ), + literal=LiteralConfig( + bytes="TO_BINARY('{hex}', 'HEX')", + uuid="'{}'", + json="PARSE_JSON({})", + ), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, + trim=OPT_TRIM_FUNCTION, + collate=OPT_COLLATE_FUNCTION_STRING, + ), + aggregate=AggregateConfig(null_treatment=OPT_NULL_TREATMENT_POSTFIX), + schema=SchemaConfig( + features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS, + location=" EXTERNAL_VOLUME = {}", + ), +) + +Spark = Dialect( + placeholder="?", + like=LikeConfig( + escape=OPT_ESCAPE_SCALAR, + any=OPT_ANY_PATTERN_LIST, + ilike=OPT_ILIKE_STANDARD, + regex=OPT_REGEX_STANDARD, + ), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + identifier="`{backtick}`", + string=StringConfig(template="'{unicode_quote}'"), + cast=CastConfig(supports_double_colon=True), + tuple=TupleConfig(syntax=OPT_SYNTAX_STRUCT, arity=OPT_ARITY_ONE), + temporal=TemporalConfig( + date="DATE '{}'", + time="'{}'", + datetime_naive="TIMESTAMP '{}'", + datetime_aware="TIMESTAMP WITH TIMEZONE '{}'", + ), + literal=LiteralConfig(uuid="'{}'", array="array({})"), + operator=OperatorConfig(is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD), + aggregate=AggregateConfig( + null_treatment=OPT_NULL_TREATMENT_POSTFIX, + function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE, + ), + schema=SchemaConfig( + features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_IS_DATABASE, + location=" LOCATION {}", + ), +) + +Sqlite = Dialect( + like=LikeConfig(escape=OPT_ESCAPE_SCALAR), + bitwise=BitwiseConfig(operations=_ALL_BITWISE), + cast=CastConfig(try_cast=None), + tuple=TupleConfig(syntax=OPT_SYNTAX_PARENTHESES, arity=OPT_ARITY_EMPTY), + temporal=TemporalConfig( + date="'{}'", + time="'{}'", + datetime_naive="'{}'", + datetime_aware="'{}'", + ), + literal=LiteralConfig(boolean=OPT_BOOLEAN_INTEGER, uuid="'{}'"), + operator=OperatorConfig( + is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, + trim=OPT_TRIM_FUNCTION, + collate=OPT_COLLATE_INFIX_IDENT, + ), + aggregate=AggregateConfig(function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE), +) + +Teradata = Dialect( + placeholder="?", + like=LikeConfig(escape=OPT_ESCAPE_SCALAR, ilike=OPT_ILIKE_USE_LIKE_LOWER), + bitwise=BitwiseConfig( + operations=_ALL_BITWISE, + syntax=OPT_SYNTAX_SHIFT_FUNCTION, + xor=OPT_XOR_BIT_FUNCTION, + ), + cast=CastConfig(try_cast="TRYCAST({expr} AS {type})"), + temporal=TemporalConfig( + date="DATE '{}'", + time="'{}'", + datetime_naive="TIMESTAMP '{}'", + datetime_aware="TIMESTAMP WITH TIMEZONE '{}'", + ), + literal=LiteralConfig(bit="'{}'B", bytes="'{hex}'XBV", uuid="'{}'"), + operator=OperatorConfig(modulo=OPT_MODULO_INFIX), + aggregate=AggregateConfig(null_treatment=OPT_NULL_TREATMENT_INSIDE_PARENS), +) diff --git a/rapidquery/dialect.pyi b/rapidquery/dialect.pyi index 871dd15..9118f45 100644 --- a/rapidquery/dialect.pyi +++ b/rapidquery/dialect.pyi @@ -1,25 +1,731 @@ -""" -SQL dialects and the integer flags that configure one. -""" +"""SQL dialects and the flags that configure one.""" -import typing +from typing import Final, Self, final -@typing.final +@final class AggregateConfig: - def __new__(cls, *, null_treatment: int = ..., function_filter: int = ...) -> typing.Self: ... - def __eq__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... + """Aggregate ``{IGNORE|RESPECT} NULLS`` and ``FILTER`` spelling. + + Controls how null-treatment modifiers and aggregate filters are written + inside function calls such as ``FIRST_VALUE``. + """ + + def __new__( + cls, + *, + null_treatment: int = 0, + function_filter: int = 0, + ) -> Self: + """Create an ``AggregateConfig``. + + Args: + null_treatment: ``OPT_NULL_TREATMENT_*`` value selecting where + ``IGNORE NULLS`` / ``RESPECT NULLS`` is written. ``0`` rejects it. + function_filter: ``OPT_FUNCTION_FILTER_*`` value selecting the + ``FILTER`` syntax. ``0`` rejects it. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is an ``AggregateConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + @property - def null_treatment(self) -> int: ... + def null_treatment(self) -> int: + """``OPT_NULL_TREATMENT_*`` value: ``UNSUPPORTED``, ``INSIDE_PARENS``, or ``POSTFIX``. + + ``INSIDE_PARENS`` writes ``FIRST_VALUE(x IGNORE NULLS)``. + ``POSTFIX`` writes ``FIRST_VALUE(x) IGNORE NULLS``. + """ + @property - def function_filter(self) -> int: ... + def function_filter(self) -> int: + """``OPT_FUNCTION_FILTER_*`` value describing the aggregate ``FILTER`` syntax. + + ``FILTER`` writes ``f(x) FILTER (cond)``. + ``FILTER_WHERE`` writes ``f(x) FILTER (WHERE cond)``. + ``INSIDE_WHERE`` writes ``f(x WHERE cond)``. + """ -@typing.final +@final class BitwiseConfig: - def __new__(cls, *, operations: int = 0, syntax: int = ..., xor: int = ...) -> typing.Self: ... - def __eq__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... + """Which bitwise operators exist, and how ``& | ~ << >>`` and ``^`` are spelled. + + A bitwise operator is rejected unless its flag is set in ``operations``. + """ + + def __new__( + cls, + *, + operations: int = 0, + syntax: int = 0, + xor: int = 0, + ) -> Self: + """Create a ``BitwiseConfig``. + + Args: + operations: ``OPT_OPERATIONS_*`` flags for the operators the dialect supports. + syntax: ``OPT_SYNTAX_*`` value shared by ``&``, ``|``, ``~``, ``<<``, and ``>>``. + xor: ``OPT_XOR_*`` value selecting how ``^`` is written. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``BitwiseConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def operations(self) -> int: + """``OPT_OPERATIONS_*`` flags for the supported operators: AND, OR, NOT, SHL, SHR.""" + + @property + def syntax(self) -> int: + """``OPT_SYNTAX_*`` value for the spelling shared by AND, OR, NOT, and shifts. + + ``INFIX`` writes ``& | ~ << >>``. + ``BIT_FUNCTION`` writes ``BITAND`` / ``BITOR`` / ``BITNOT`` / ``BITSHIFTLEFT`` / ``BITSHIFTRIGHT``. + ``SHIFT_FUNCTION`` writes ``BITAND`` / ``BITOR`` / ``BITNOT`` / ``SHIFTLEFT`` / ``SHIFTRIGHT``. + """ + + @property + def xor(self) -> int: + """``OPT_XOR_*`` value for how ``^`` (XOR) is spelled. + + ``CARET`` writes ``left ^ right``. + ``HASH`` writes ``left # right``. In PostgreSQL, ``^`` is exponentiation. + ``FUNCTION`` writes ``xor(left, right)``. + ``BIT_FUNCTION`` writes ``BITXOR(left, right)``. + """ + +@final +class CastConfig: + """``TRY_CAST`` spelling, and whether ``expr::type`` is legal.""" + + def __new__( + cls, + *, + try_cast: str | None = "TRY_CAST({expr} AS {type})", + supports_double_colon: bool = False, + ) -> Self: + """Create a ``CastConfig``. + + Args: + try_cast: Template for ``TRY_CAST`` with ``{expr}`` and ``{type}`` keys, + or ``None`` if the dialect has no ``TRY_CAST``. + supports_double_colon: Whether ``expr::type`` is valid syntax. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``CastConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def try_cast(self) -> str | None: + """Template for ``TRY_CAST``, using ``{expr}`` and ``{type}`` keys, or ``None`` if unsupported.""" + + @property + def supports_double_colon(self) -> bool: + """``True`` if the dialect accepts the ``expr::type`` cast syntax.""" + +@final +class LikeConfig: + """``LIKE`` / ``ILIKE`` / ``SIMILAR TO`` / regex spelling and escape support.""" + + def __new__( + cls, + *, + escape: int = 0, + any: int = 0, + ilike: int = 0, + regex: int = 0, + supports_similar_to: bool = False, + ) -> Self: + """Create a ``LikeConfig``. + + Args: + escape: ``OPT_ESCAPE_*`` flags for where an ``ESCAPE`` clause is legal. + ``0`` writes no ``ESCAPE`` and rejects an escape character. + any: ``OPT_ANY_*`` value selecting how ``LIKE ANY`` / ``ILIKE ANY`` is rendered. + ilike: ``OPT_ILIKE_*`` value selecting how ``ILIKE`` is rendered. + regex: ``OPT_REGEX_*`` value selecting how regex matching is spelled. + supports_similar_to: Whether the dialect supports ``SIMILAR TO``. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``LikeConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def escape(self) -> int: + """``OPT_ESCAPE_*`` flags for where an ``ESCAPE`` clause is allowed. + + ``SCALAR`` allows a scalar ``LIKE`` pattern with ``ESCAPE``. + ``ANY`` allows native ``LIKE ANY (...) ESCAPE``. + """ + + @property + def any(self) -> int: + """``OPT_ANY_*`` value selecting the ``LIKE ANY`` rendering. + + ``OR_EXPANSION`` writes ``(x LIKE p1 OR x LIKE p2)``. + ``ARRAY_CONSTRUCTOR`` writes ``LIKE ANY (ARRAY[p, ...])``. + ``PATTERN_LIST`` writes ``LIKE ANY (p, ...)``. + """ + + @property + def ilike(self) -> int: + """``OPT_ILIKE_*`` value selecting how ``ILIKE`` is rendered. + + ``STANDARD`` uses native ``ILIKE``. + ``USE_LIKE_LOWER`` writes ``LIKE`` with ``LOWER()`` on both sides. + ``UNSUPPORTED`` rejects ``ILIKE``. + """ + + @property + def regex(self) -> int: + """``OPT_REGEX_*`` value selecting how regex matching is spelled. + + ``STANDARD`` writes ``expr [NOT] RLIKE pattern``. + ``TILDE`` writes ``expr ~ pattern`` / ``expr !~ pattern``. + ``REGEXP_LIKE``, ``REGEXP_CONTAINS``, and ``REGEXP_MATCHES`` write the matching function call. + ``UNSUPPORTED`` rejects regex matching. + """ + + @property + def supports_similar_to(self) -> bool: + """``True`` if the dialect supports the ``SIMILAR TO`` predicate.""" + +@final +class LiteralConfig: + """Boolean, bit, bytes, network, UUID, JSON, and array literals. + + ``bit``, ``inet``, ``macaddr``, and ``array`` are ``None`` when the dialect + has no spelling. ``json`` receives an already quoted string. + """ + + def __new__( + cls, + *, + boolean: int = 0, + bit: str | None = None, + bytes: str = "X'{hex}'", + inet: str | None = None, + macaddr: str | None = None, + uuid: str | None = None, + json: str = "{}", + array: str | None = None, + ) -> Self: + """Create a ``LiteralConfig``. + + Args: + boolean: ``OPT_BOOLEAN_*`` value. ``KEYWORD`` writes ``TRUE`` / ``FALSE``, + and ``INTEGER`` writes ``1`` / ``0``. + bit: Template for bit-string literals, such as ``B'{}'``, or ``None`` if unsupported. + bytes: Template for byte-string literals, using the ``{hex}`` key (e.g. ``X'{hex}'``). + inet: Template for inet literals, or ``None`` if the dialect has no spelling. + macaddr: Template for MAC address literals, or ``None`` if the dialect has no spelling. + uuid: Template for UUID literals, or ``None`` if the dialect has no spelling. + json: Template for JSON literals. The value already includes its quotes. + array: Template for array literals, such as ``ARRAY[{}]``, or ``None`` if unsupported. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``LiteralConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def boolean(self) -> int: + """``OPT_BOOLEAN_*`` value: ``KEYWORD`` for ``TRUE``/``FALSE``, or ``INTEGER`` for ``1``/``0``.""" + + @property + def bit(self) -> str | None: + """Template for bit-string literals, or ``None`` if the dialect has no bit literal.""" + + @property + def bytes(self) -> str: + """Template for byte-string literals, using the ``{hex}`` key (e.g. ``X'{hex}'``).""" + + @property + def inet(self) -> str | None: + """Template for inet literals, or ``None`` if unsupported.""" + + @property + def macaddr(self) -> str | None: + """Template for MAC address literals, or ``None`` if unsupported.""" + + @property + def uuid(self) -> str | None: + """Template for UUID literals, or ``None`` if unsupported.""" + + @property + def json(self) -> str: + """Template for JSON literals. The value passed in already includes its quotes.""" + + @property + def array(self) -> str | None: + """Template for array literals (e.g. ``ARRAY[{}]``), or ``None`` if unsupported.""" + +@final +class OperatorConfig: + """``%``, ``IS DISTINCT FROM``, ``TRIM``, and ``COLLATE`` spelling.""" + + def __new__( + cls, + *, + modulo: int = 0, + is_distinct_from: int = 0, + trim: int = 0, + collate: int = 0, + ) -> Self: + """Create an ``OperatorConfig``. + + Args: + modulo: ``OPT_MODULO_*`` value selecting how modulo is written. + is_distinct_from: ``OPT_IS_DISTINCT_FROM_*`` value selecting how + ``IS [NOT] DISTINCT FROM`` is written. + trim: ``OPT_TRIM_*`` value selecting how ``TRIM`` is written. + collate: ``OPT_COLLATE_*`` value selecting how ``COLLATE`` is written. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is an ``OperatorConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def modulo(self) -> int: + """``OPT_MODULO_*`` value for modulo. + + ``PERCENT`` writes ``left % right``. + ``FUNCTION`` writes ``MOD(left, right)``. + ``INFIX`` writes ``left MOD right``. + """ + + @property + def is_distinct_from(self) -> int: + """``OPT_IS_DISTINCT_FROM_*`` value for ``IS [NOT] DISTINCT FROM``. + + ``STANDARD`` writes ``left IS [NOT] DISTINCT FROM right``. + ``NULL_SAFE_EQUAL`` uses ``<=>`` for null-safe equality. ``IS DISTINCT FROM`` becomes ``NOT (left <=> right)``. + ``UNSUPPORTED`` rejects the operator. + """ + + @property + def trim(self) -> int: + """``OPT_TRIM_*`` value for how ``TRIM`` is spelled. + + ``SQL_FROM`` writes ``TRIM([LEADING|TRAILING] [chars FROM] expr)``. + ``FUNCTION`` writes ``TRIM`` / ``LTRIM`` / ``RTRIM`` ``(expr[, chars])``. + ``SPACES_ONLY`` writes ``TRIM`` / ``LTRIM`` / ``RTRIM`` ``(expr)``. Supplying a character set raises an error. + """ + + @property + def collate(self) -> int: + """``OPT_COLLATE_*`` value for how ``COLLATE`` is spelled. + + ``INFIX_IDENT`` writes ``expr COLLATE ident``. + ``FUNCTION_STRING`` writes ``COLLATE(expr, 'body')``, where the collation name is a string literal. + ``UNSUPPORTED`` rejects ``COLLATE``. + """ + +@final +class SchemaConfig: + """``CREATE`` / ``DROP SCHEMA`` features, owner clause, and location clause. + + ``features`` must include ``OPT_FEATURES_SUPPORTED`` or schema statements + are rejected. ``location`` receives an already quoted string and supplies + its own leading space (``' LOCATION {}'``). ``None`` rejects a location. + """ + + def __new__( + cls, + *, + features: int = 0, + authorization: int = 0, + location: str | None = None, + ) -> Self: + """Create a ``SchemaConfig``. + + Args: + features: ``OPT_FEATURES_*`` flags. Must include ``OPT_FEATURES_SUPPORTED`` + for any schema statement to be accepted. + authorization: ``OPT_AUTHORIZATION_*`` value for the ``AUTHORIZATION`` owner clause. + location: Template for the ``LOCATION`` clause, including its leading space + (e.g. ``' LOCATION {}'``), or ``None`` to reject a location. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``SchemaConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def features(self) -> int: + """``OPT_FEATURES_*`` flags describing schema support. + + ``SUPPORTED`` means the dialect supports schemas. Other flags cover ``IF NOT EXISTS``, + embedded DDL in ``CREATE SCHEMA``, whether a schema is a database, and ``CASCADE`` / ``RESTRICT`` on ``DROP SCHEMA``. + """ + + @property + def authorization(self) -> int: + """``OPT_AUTHORIZATION_*`` value for the ``AUTHORIZATION`` owner clause. + + ``UNSUPPORTED`` means the clause is not allowed. + ``OPTIONAL`` means the clause is allowed but not required. + """ + + @property + def location(self) -> str | None: + """Template for the ``LOCATION`` clause, including its leading space, or ``None`` if unsupported.""" + +@final +class StringConfig: + """String literal template and ``||`` / ``+`` / ``CONCAT`` spelling. + + The template key selects the escape: ``{single}``, ``{double}``, ``{hex}``, + ``{hex_quote}``, ``{mysql}``, ``{unicode}``, or ``{unicode_quote}``. + ``concat`` keys are ``{left}`` and ``{right}``. + """ + + def __new__( + cls, + *, + template: str = "'{single}'", + concat: str = "{left} || {right}", + ) -> Self: + """Create a ``StringConfig``. + + Args: + template: String literal template with one escape key, such as ``'{single}'`` + or ``E'{hex}'``. A string containing NUL is rejected. + concat: Template for concatenation with ``{left}`` and ``{right}`` keys. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``StringConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + @property - def null_treatment(self) -> int: ... + def template(self) -> str: + """String literal template, containing one escape key such as ``{single}`` or ``{hex}``.""" + @property - def function_filter(self) -> int: ... + def concat(self) -> str: + """Concatenation template using ``{left}`` and ``{right}`` keys (e.g. ``{left} || {right}``).""" + +@final +class TemporalConfig: + """Date, time, and timestamp literal templates. + + ``{}`` is the raw text (``2024-01-15``), so the quotes belong in the + template (``DATE '{}'``, ``DATE E'{}'``). + """ + + def __new__( + cls, + *, + date: str = "{}", + time: str = "{}", + datetime_naive: str = "{}", + datetime_aware: str = "{}", + ) -> Self: + """Create a ``TemporalConfig``. + + Args: + date: Template for date literals, such as ``DATE '{}'``. + time: Template for time literals, such as ``TIME '{}'``. + datetime_naive: Template for timestamps without a timezone, such as ``TIMESTAMP '{}'``. + datetime_aware: Template for timestamps with a timezone, such as ``TIMESTAMPTZ '{}'``. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``TemporalConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def date(self) -> str: + """Template for date literals. ``{}`` is replaced by the raw date text.""" + + @property + def time(self) -> str: + """Template for time literals. ``{}`` is replaced by the raw time text.""" + + @property + def datetime_naive(self) -> str: + """Template for timestamps without a timezone. ``{}`` is replaced by the raw text.""" + + @property + def datetime_aware(self) -> str: + """Template for timestamps with a timezone. ``{}`` is replaced by the raw text.""" + +@final +class TupleConfig: + """Tuple / row-value spelling and which arities below 2 are legal.""" + + def __new__( + cls, + *, + syntax: int = 0, + arity: int = 0, + ) -> Self: + """Create a ``TupleConfig``. + + Args: + syntax: ``OPT_SYNTAX_*`` value selecting how a tuple is written. + arity: ``OPT_ARITY_*`` flags for tuple sizes below 2 that are allowed. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``TupleConfig`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def syntax(self) -> int: + """``OPT_SYNTAX_*`` value for how a tuple is written. + + ``ROW`` writes ``ROW(...)``. + ``STRUCT`` writes ``struct(...)``. + ``PARENTHESES`` writes ``(a, b, ...)``. + ``UNSUPPORTED`` rejects tuples. + """ + + @property + def arity(self) -> int: + """``OPT_ARITY_*`` flags for tuple sizes below 2 that are allowed. + + Sizes 2 and above are always allowed when ``syntax`` is supported. + ``OPT_ARITY_EMPTY`` allows size 0. ``OPT_ARITY_ONE`` allows size 1. + """ + +@final +class Dialect: + """Quoting, literal spelling, and which syntax is legal. + + ``placeholder`` may be a bare marker (``'?'``) or include the 1-based + index (``'?{}'``, ``'${}'``, ``':{}'``). ``identifier`` keys are + ``{double}``, ``{single}``, ``{bracket}``, ``{backtick}``, and the + ``slash*`` backslash forms. + """ + + def __new__( + cls, + *, + placeholder: str = "?{}", + like: LikeConfig = ..., + bitwise: BitwiseConfig = ..., + identifier: str = '"{double}"', + string: StringConfig = ..., + cast: CastConfig = ..., + tuple: TupleConfig = ..., + temporal: TemporalConfig = ..., + literal: LiteralConfig = ..., + operator: OperatorConfig = ..., + aggregate: AggregateConfig = ..., + schema: SchemaConfig = ..., + ) -> Self: + """Create a ``Dialect``. + + Args: + placeholder: Positional parameter template. Use ``'?'`` for a bare marker, + or include ``{}`` for the 1-based index (e.g. ``'${}'`` gives ``$1``, ``$2``). + like: ``LIKE`` / ``ILIKE`` / regex configuration. + bitwise: Bitwise operator support and spelling. + identifier: Identifier template such as ``"{double}"`` or ``` `{backtick}` ```. + The key must match the delimiter in the template. + string: String literal and concatenation configuration. + cast: Cast syntax configuration (``TRY_CAST``, ``::``). + tuple: Tuple and row-value configuration. + temporal: Date, time, and timestamp literal templates. + literal: Boolean, bit, bytes, network, UUID, JSON, and array literal configuration. + operator: Spelling of modulo, ``IS DISTINCT FROM``, ``TRIM``, and ``COLLATE``. + aggregate: Aggregate ``{IGNORE|RESPECT} NULLS`` and ``FILTER`` configuration. + schema: Schema and DDL feature configuration. + """ + + def __eq__(self, value: object, /) -> bool: + """Return ``True`` if ``value`` is a ``Dialect`` with equal settings.""" + + def __hash__(self) -> int: + """Return a hash consistent with ``__eq__``.""" + + @property + def placeholder(self) -> str: + """Positional parameter template, such as ``'?{}'``, ``'?'``, or ``'${}'``.""" + + @property + def like(self) -> LikeConfig: + """``LIKE`` / ``ILIKE`` / regex configuration.""" + + @property + def bitwise(self) -> BitwiseConfig: + """Bitwise operator support and spelling.""" + + @property + def identifier(self) -> str: + """Identifier quoting template, such as ``"{double}"`` or ``` `{backtick}` ```.""" + + @property + def string(self) -> StringConfig: + """String literal and concatenation configuration.""" + + @property + def cast(self) -> CastConfig: + """Cast syntax configuration (``TRY_CAST`` and ``::``).""" + + @property + def tuple(self) -> TupleConfig: + """Tuple and row-value configuration.""" + + @property + def temporal(self) -> TemporalConfig: + """Date, time, and timestamp literal templates.""" + + @property + def literal(self) -> LiteralConfig: + """Boolean, bit, bytes, network, UUID, JSON, and array literal configuration.""" + + @property + def operator(self) -> OperatorConfig: + """Spelling of modulo, ``IS DISTINCT FROM``, ``TRIM``, and ``COLLATE``.""" + + @property + def aggregate(self) -> AggregateConfig: + """Aggregate ``{IGNORE|RESPECT} NULLS`` and ``FILTER`` configuration.""" + + @property + def schema(self) -> SchemaConfig: + """Schema and DDL feature configuration.""" + +# --------------------------------------------------------------------------- +# Feature flags. Combine with ``|`` and pass to the matching ``*Config``. +# A flag that is not set means the dialect does not support that form. +# --------------------------------------------------------------------------- + +# AggregateConfig.function_filter: where the aggregate FILTER clause is written. +OPT_FUNCTION_FILTER_UNSUPPORTED: Final[int] # Aggregate FILTER is not supported. +OPT_FUNCTION_FILTER_FILTER: Final[int] # f(x) FILTER (cond) +OPT_FUNCTION_FILTER_FILTER_WHERE: Final[int] # f(x) FILTER (WHERE cond) +OPT_FUNCTION_FILTER_INSIDE_WHERE: Final[int] # f(x WHERE cond) + +# AggregateConfig.null_treatment: where IGNORE NULLS / RESPECT NULLS is written. +OPT_NULL_TREATMENT_UNSUPPORTED: Final[int] # IGNORE/RESPECT NULLS is not supported. +OPT_NULL_TREATMENT_INSIDE_PARENS: Final[int] # FIRST_VALUE(x IGNORE NULLS) +OPT_NULL_TREATMENT_POSTFIX: Final[int] # FIRST_VALUE(x) IGNORE NULLS + +# BitwiseConfig.operations: which bitwise operators are available. +OPT_OPERATIONS_AND: Final[int] # & +OPT_OPERATIONS_OR: Final[int] # | +OPT_OPERATIONS_NOT: Final[int] # ~ +OPT_OPERATIONS_SHL: Final[int] # << +OPT_OPERATIONS_SHR: Final[int] # >> + +# BitwiseConfig.syntax: spelling shared by AND, OR, NOT, and the shifts. +OPT_SYNTAX_INFIX: Final[int] # & | ~ << >> +OPT_SYNTAX_BIT_FUNCTION: Final[int] # BITAND / BITOR / BITNOT / BITSHIFTLEFT / BITSHIFTRIGHT +OPT_SYNTAX_SHIFT_FUNCTION: Final[int] # BITAND / BITOR / BITNOT / SHIFTLEFT / SHIFTRIGHT + +# BitwiseConfig.xor: spelling of ^. +OPT_XOR_UNSUPPORTED: Final[int] # XOR is not supported. +OPT_XOR_CARET: Final[int] # left ^ right +OPT_XOR_HASH: Final[int] # left # right +OPT_XOR_FUNCTION: Final[int] # xor(left, right) +OPT_XOR_BIT_FUNCTION: Final[int] # BITXOR(left, right) + +# LikeConfig.escape: where an ESCAPE clause is allowed. +OPT_ESCAPE_SCALAR: Final[int] # Scalar LIKE ... ESCAPE, including OR expansion. +OPT_ESCAPE_ANY: Final[int] # Native LIKE ANY (...) ESCAPE. + +# LikeConfig.any: rendering of LIKE ANY / ILIKE ANY. +OPT_ANY_OR_EXPANSION: Final[int] # (x LIKE p1 OR x LIKE p2) +OPT_ANY_ARRAY_CONSTRUCTOR: Final[int] # LIKE ANY (ARRAY[p, ...]) +OPT_ANY_PATTERN_LIST: Final[int] # LIKE ANY (p, ...) + +# LikeConfig.ilike: rendering of ILIKE. +OPT_ILIKE_UNSUPPORTED: Final[int] # ILIKE is not supported. +OPT_ILIKE_STANDARD: Final[int] # Native ILIKE. +OPT_ILIKE_USE_LIKE_LOWER: Final[int] # LIKE with LOWER() on both sides. + +# LikeConfig.regex: spelling of regex matching. +OPT_REGEX_UNSUPPORTED: Final[int] # Regex matching is not supported. +OPT_REGEX_STANDARD: Final[int] # expr [NOT] RLIKE pattern +OPT_REGEX_TILDE: Final[int] # expr ~ pattern / expr !~ pattern +OPT_REGEX_REGEXP_LIKE: Final[int] # REGEXP_LIKE(expr, pattern) +OPT_REGEX_REGEXP_CONTAINS: Final[int] # REGEXP_CONTAINS(expr, pattern) +OPT_REGEX_REGEXP_MATCHES: Final[int] # regexp_matches(expr, pattern) + +# OperatorConfig.is_distinct_from: spelling of IS [NOT] DISTINCT FROM. +OPT_IS_DISTINCT_FROM_UNSUPPORTED: Final[int] # Not supported. +OPT_IS_DISTINCT_FROM_STANDARD: Final[int] # left IS [NOT] DISTINCT FROM right +OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL: Final[int] # Uses <=>, where IS DISTINCT FROM is NOT (left <=> right). + +# OperatorConfig.modulo: spelling of %. +OPT_MODULO_PERCENT: Final[int] # left % right +OPT_MODULO_FUNCTION: Final[int] # MOD(left, right) +OPT_MODULO_INFIX: Final[int] # left MOD right + +# OperatorConfig.trim: spelling of TRIM. +OPT_TRIM_SQL_FROM: Final[int] # TRIM([LEADING|TRAILING] [chars FROM] expr) +OPT_TRIM_FUNCTION: Final[int] # TRIM / LTRIM / RTRIM (expr[, chars]) +OPT_TRIM_SPACES_ONLY: Final[int] # TRIM / LTRIM / RTRIM (expr); a character set raises. + +# OperatorConfig.collate: spelling of COLLATE. +OPT_COLLATE_UNSUPPORTED: Final[int] # Not supported. +OPT_COLLATE_INFIX_IDENT: Final[int] # expr COLLATE ident +OPT_COLLATE_FUNCTION_STRING: Final[int] # COLLATE(expr, 'body') + +# TupleConfig.arity: tuple sizes below 2 that are allowed. +OPT_ARITY_EMPTY: Final[int] # Size 0. +OPT_ARITY_ONE: Final[int] # Size 1. + +# TupleConfig.syntax: how a tuple is written. +OPT_SYNTAX_ROW: Final[int] # ROW(...) +OPT_SYNTAX_STRUCT: Final[int] # struct(...) +OPT_SYNTAX_PARENTHESES: Final[int] # (a, b, ...) +OPT_SYNTAX_UNSUPPORTED: Final[int] # Tuples are not supported. + +# LiteralConfig.boolean: spelling of boolean literals. +OPT_BOOLEAN_KEYWORD: Final[int] # TRUE / FALSE +OPT_BOOLEAN_INTEGER: Final[int] # 1 / 0 + +# SchemaConfig.features: schema statement support. +OPT_FEATURES_SUPPORTED: Final[int] # Schemas are supported. Required for any schema statement. +OPT_FEATURES_IF_NOT_EXISTS: Final[int] # CREATE SCHEMA IF NOT EXISTS +OPT_FEATURES_EMBEDDED_DDL: Final[int] # DDL statements embedded in CREATE SCHEMA +OPT_FEATURES_IS_DATABASE: Final[int] # A schema means the same thing as a database. +OPT_FEATURES_DROP_CASCADE: Final[int] # DROP SCHEMA ... CASCADE +OPT_FEATURES_DROP_RESTRICT: Final[int] # DROP SCHEMA ... RESTRICT + +# SchemaConfig.authorization: AUTHORIZATION owner clause. +OPT_AUTHORIZATION_UNSUPPORTED: Final[int] # Not allowed. +OPT_AUTHORIZATION_OPTIONAL: Final[int] # Allowed, but not required. + +BigQuery: Final[Dialect] +Databricks: Final[Dialect] +DuckDb: Final[Dialect] +Hive: Final[Dialect] +MsSql: Final[Dialect] +MySql: Final[Dialect] +Oracle: Final[Dialect] +PostgreSql: Final[Dialect] +Redshift: Final[Dialect] +Snowflake: Final[Dialect] +Spark: Final[Dialect] +Sqlite: Final[Dialect] +Teradata: Final[Dialect] diff --git a/scripts/check_dialect_stub.sh b/scripts/check_dialect_stub.sh new file mode 100644 index 0000000..40844cd --- /dev/null +++ b/scripts/check_dialect_stub.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +rs="src/dialect/base.rs" +pyi="rapidquery/dialect.pyi" +missing=0 + +miss() { + echo "MISSING in stub: $1" + missing=1 +} + +# 1. Python classes: pyclass("rapidquery.dialect.Name") +grep -oE 'pyclass\("rapidquery\.dialect\.[A-Za-z]+"' "$rs" | sed 's/.*\.//; s/"//' | +while read -r c; do grep -q "^class $c\b" "$pyi" || miss "class $c"; done + +# 2. Constructor fields: "name:" lines inside pyo3_constructor! { ... } +awk '/pyo3_constructor!/{on=1} on&&/^}/{on=0} on' "$rs" | +grep -oE '^\s+[a-z_]+:' | tr -d ' :' | sort -u | +while read -r f; do grep -q "def $f(" "$pyi" || miss "field $f"; done + +# 3. Flags: "field: bits|enum Type { A, B }" -> OPT__ +tr '\n' ' ' < "$rs" | +grep -oE '[a-z_]+: (bits|enum) [A-Za-z]+ \{[^}]*\}' | +while read -r line; do + field=$(echo "${line%%:*}" | tr a-z A-Z) + variants=$(echo "$line" | sed 's/.*{//; s/}//; s/,/ /g') + for v in $variants; do + snake=$(echo "$v" | sed 's/\([a-z0-9]\)\([A-Z]\)/\1_\2/g' | tr a-z A-Z) + grep -q "^OPT_${field}_${snake}:" "$pyi" || miss "OPT_${field}_${snake}" + done +done + +exit $missing diff --git a/src/dialect/base.rs b/src/dialect/base.rs index aa53455..2c581e8 100644 --- a/src/dialect/base.rs +++ b/src/dialect/base.rs @@ -381,7 +381,10 @@ rapidquery_macros::pyo3_constructor! { #[rapidquery_macros::define_options] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PyDialect { - /// Positional parameter template e.g. `'?{}'` + /// Positional parameter template e.g. `'?{}'` or `'?'`. + /// + /// `{}` is the 1-based index and may be omitted. `'?'` writes the marker + /// alone. `'${}'` writes `$1`, `$2`, and so on. pub placeholder: template::Template, /// `LIKE` / `ILIKE` / regex configuration. @@ -439,7 +442,8 @@ pub struct PyDialect { rapidquery_macros::pyo3_constructor! { PyDialect => export_pydialect_options { - placeholder: template(_; 1) = "?{}", + // `?` on the key lets a dialect omit `{}` (`'?'` writes the marker alone). + placeholder: template(_?; 0..=1) = "?{}", like: class LikeConfig = LikeConfig::default(), bitwise: class BitwiseConfig = BitwiseConfig::default(), identifier: template( diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 7daf60b..12f5d1a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -16,6 +16,8 @@ pub mod dialect_mod { #[pymodule_export] use super::LikeConfig; #[pymodule_export] + use super::LiteralConfig; + #[pymodule_export] use super::OperatorConfig; #[pymodule_export] use super::PyDialect; diff --git a/src/internal/template.rs b/src/internal/template.rs index 7cdb42e..3e6b548 100644 --- a/src/internal/template.rs +++ b/src/internal/template.rs @@ -176,7 +176,7 @@ impl Template { } if matches.len() > maximum as usize { - return Err(TemplateParseError::TooManyPlaceholder(minimum, matches.len())); + return Err(TemplateParseError::TooManyPlaceholder(maximum, matches.len())); } for (idx, (key, required)) in keys.iter().enumerate() { diff --git a/src/sql.rs b/src/sql.rs index 7d240b9..bd0185f 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -30,13 +30,6 @@ pub trait SqlWriter: FastWrite { /// /// Build one with a Python token, append text through [`SqlWriter`], then take /// the string with [`Self::into_string`]. -/// -/// ``` -/// pyo3::Python::attach(|py| { -/// let buf = rapidquery::sql::Buffered::new(py); -/// assert_eq!(buf.as_str(), ""); -/// }); -/// ``` pub struct Buffer<'py> { py: pyo3::Python<'py>, sql: String, @@ -103,14 +96,6 @@ impl<'py> SqlWriter for Buffer<'py> { /// /// [`std::fmt::Write`] appends text. [`Self::push_param`] appends a placeholder /// and stores the object. Numbering starts at 1. -/// -/// ``` -/// pyo3::Python::attach(|py| { -/// let sql = rapidquery::sql::Parameterized::new(py); -/// assert_eq!(sql.sql(), ""); -/// assert!(sql.params().is_empty()); -/// }); -/// ``` pub struct Parameterized<'py> { py: pyo3::Python<'py>, sql: String, From e00e738f51b4822158d5de68e76071b27a65c331 Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Sun, 25 Oct 2026 16:38:45 +0330 Subject: [PATCH 09/11] Update tests --- Makefile | 1 - rapidquery-macros/pyo3_constructor.rs | 39 +- rapidquery/dialect.py | 411 ++++++++++----------- rapidquery/dialect.pyi | 509 ++++++++++++++++---------- scripts/check_dialect_stub.sh | 32 -- src/ddl/schema.rs | 5 +- src/dialect/base.rs | 32 +- src/dialect/escape.rs | 46 +++ src/expr/enum_/like.rs | 12 +- src/expr/enum_/nodes.rs | 40 +- src/serializers/functions.rs | 6 +- tests/test_ddl_schema.py | 66 ++-- tests/test_dialect.py | 139 ++++--- tests/test_expr.py | 6 +- tests/test_serializers.py | 18 +- 15 files changed, 756 insertions(+), 606 deletions(-) delete mode 100644 scripts/check_dialect_stub.sh diff --git a/Makefile b/Makefile index 701ad30..955df71 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,6 @@ test-rust: .PHONY: test-python ## Run Python tests test-python: uv run pytest -v -n auto - bash scripts/check_dialect_stub.sh .PHONY: test ## Run all tests test: test-rust test-python diff --git a/rapidquery-macros/pyo3_constructor.rs b/rapidquery-macros/pyo3_constructor.rs index 98ca9d6..1cd7f05 100644 --- a/rapidquery-macros/pyo3_constructor.rs +++ b/rapidquery-macros/pyo3_constructor.rs @@ -292,7 +292,7 @@ fn screaming_snake(s: &str) -> String { out } -/// Name of the exported module constant: `OPT__`. +/// Name of the exported module constant: `OPT__`. fn const_name(field: &Ident, item: &Ident) -> String { format!( "OPT_{}_{}", @@ -508,22 +508,29 @@ impl Field { } fn exports(&self) -> Vec { - let name = &self.name; match &self.kind { - Kind::Enum { ty, variants } => variants - .iter() - .map(|v| { - let c = const_name(name, v); - quote!(module.add(#c, <#ty>::#v as u8)?;) - }) - .collect(), - Kind::Bits { ty, flags } => flags - .iter() - .map(|f| { - let c = const_name(name, f); - quote!(module.add(#c, <#ty>::#f.bits())?;) - }) - .collect(), + Kind::Enum { ty, variants } => { + let Type::Path(type_name) = ty else { unreachable!() }; + + variants + .iter() + .map(|v| { + let c = const_name(type_name.path.get_ident().unwrap(), v); + quote!(module.add(#c, <#ty>::#v as u8)?;) + }) + .collect() + } + Kind::Bits { ty, flags } => { + let Type::Path(type_name) = ty else { unreachable!() }; + + flags + .iter() + .map(|f| { + let c = const_name(type_name.path.get_ident().unwrap(), f); + quote!(module.add(#c, <#ty>::#f.bits())?;) + }) + .collect() + } _ => Vec::new(), } } diff --git a/rapidquery/dialect.py b/rapidquery/dialect.py index 85e6263..39a9cae 100644 --- a/rapidquery/dialect.py +++ b/rapidquery/dialect.py @@ -1,81 +1,66 @@ -"""SQL dialects and the flags that configure one. - -`Dialect` chooses quoting, literal spelling, and which syntax is legal. The -names at the bottom are built-in configurations. - -A template key selects an escape (`{hex}`, `{double}`, `{slashx}`, ...). A -positional `{}` is raw text for dates, times, timestamps, UUIDs, inet, and -MAC addresses, so the quotes belong in the template. JSON and schema location -receive an already quoted string. - -`placeholder` may be a bare marker (`'?'`) or include the 1-based index -(`'?{}'`, `'${}'`, `':{}'`). `ILIKE` has no case-sensitive `LIKE` fallback: -MsSql, MySql, and Sqlite reject it. -""" - from ._core.dialect import ( - OPT_ANY_ARRAY_CONSTRUCTOR, - OPT_ANY_OR_EXPANSION, - OPT_ANY_PATTERN_LIST, - OPT_ARITY_EMPTY, - OPT_ARITY_ONE, - OPT_AUTHORIZATION_OPTIONAL, - OPT_AUTHORIZATION_UNSUPPORTED, - OPT_BOOLEAN_INTEGER, - OPT_BOOLEAN_KEYWORD, - OPT_COLLATE_FUNCTION_STRING, - OPT_COLLATE_INFIX_IDENT, - OPT_COLLATE_UNSUPPORTED, - OPT_ESCAPE_ANY, - OPT_ESCAPE_SCALAR, - OPT_FEATURES_DROP_CASCADE, - OPT_FEATURES_DROP_RESTRICT, - OPT_FEATURES_EMBEDDED_DDL, - OPT_FEATURES_IF_NOT_EXISTS, - OPT_FEATURES_IS_DATABASE, - OPT_FEATURES_SUPPORTED, + OPT_BITWISE_OPERATIONS_AND, + OPT_BITWISE_OPERATIONS_NOT, + OPT_BITWISE_OPERATIONS_OR, + OPT_BITWISE_OPERATIONS_SHL, + OPT_BITWISE_OPERATIONS_SHR, + OPT_BITWISE_SYNTAX_BIT_FUNCTION, + OPT_BITWISE_SYNTAX_INFIX, + OPT_BITWISE_SYNTAX_SHIFT_FUNCTION, + OPT_BITWISE_XOR_BIT_FUNCTION, + OPT_BITWISE_XOR_CARET, + OPT_BITWISE_XOR_FUNCTION, + OPT_BITWISE_XOR_HASH, + OPT_BITWISE_XOR_UNSUPPORTED, + OPT_BOOL_FORMAT_INTEGER, + OPT_BOOL_FORMAT_KEYWORD, OPT_FUNCTION_FILTER_FILTER, OPT_FUNCTION_FILTER_FILTER_WHERE, OPT_FUNCTION_FILTER_INSIDE_WHERE, OPT_FUNCTION_FILTER_UNSUPPORTED, - OPT_ILIKE_STANDARD, - OPT_ILIKE_UNSUPPORTED, - OPT_ILIKE_USE_LIKE_LOWER, + OPT_I_LIKE_STRATEGY_STANDARD, + OPT_I_LIKE_STRATEGY_UNSUPPORTED, + OPT_I_LIKE_STRATEGY_USE_LIKE_LOWER, OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL, OPT_IS_DISTINCT_FROM_STANDARD, OPT_IS_DISTINCT_FROM_UNSUPPORTED, - OPT_MODULO_FUNCTION, - OPT_MODULO_INFIX, - OPT_MODULO_PERCENT, + OPT_LIKE_ANY_ARRAY_CONSTRUCTOR, + OPT_LIKE_ANY_OR_EXPANSION, + OPT_LIKE_ANY_PATTERN_LIST, + OPT_LIKE_ESCAPE_ANY, + OPT_LIKE_ESCAPE_SCALAR, + OPT_LIKE_REGEX_REGEXP_CONTAINS, + OPT_LIKE_REGEX_REGEXP_LIKE, + OPT_LIKE_REGEX_REGEXP_MATCHES, + OPT_LIKE_REGEX_STANDARD, + OPT_LIKE_REGEX_TILDE, + OPT_LIKE_REGEX_UNSUPPORTED, OPT_NULL_TREATMENT_INSIDE_PARENS, OPT_NULL_TREATMENT_POSTFIX, OPT_NULL_TREATMENT_UNSUPPORTED, - OPT_OPERATIONS_AND, - OPT_OPERATIONS_NOT, - OPT_OPERATIONS_OR, - OPT_OPERATIONS_SHL, - OPT_OPERATIONS_SHR, - OPT_REGEX_REGEXP_CONTAINS, - OPT_REGEX_REGEXP_LIKE, - OPT_REGEX_REGEXP_MATCHES, - OPT_REGEX_STANDARD, - OPT_REGEX_TILDE, - OPT_REGEX_UNSUPPORTED, - OPT_SYNTAX_BIT_FUNCTION, - OPT_SYNTAX_INFIX, - OPT_SYNTAX_PARENTHESES, - OPT_SYNTAX_ROW, - OPT_SYNTAX_SHIFT_FUNCTION, - OPT_SYNTAX_STRUCT, - OPT_SYNTAX_UNSUPPORTED, - OPT_TRIM_FUNCTION, - OPT_TRIM_SPACES_ONLY, - OPT_TRIM_SQL_FROM, - OPT_XOR_BIT_FUNCTION, - OPT_XOR_CARET, - OPT_XOR_FUNCTION, - OPT_XOR_HASH, - OPT_XOR_UNSUPPORTED, + OPT_OPERATOR_COLLATE_FUNCTION_STRING, + OPT_OPERATOR_COLLATE_INFIX_IDENT, + OPT_OPERATOR_COLLATE_UNSUPPORTED, + OPT_OPERATOR_MODULO_FUNCTION, + OPT_OPERATOR_MODULO_INFIX, + OPT_OPERATOR_MODULO_PERCENT, + OPT_OPERATOR_TRIM_FUNCTION, + OPT_OPERATOR_TRIM_SPACES_ONLY, + OPT_OPERATOR_TRIM_SQL_FROM, + OPT_SCHEMA_AUTHORIZATION_OPTIONAL, + OPT_SCHEMA_AUTHORIZATION_UNSUPPORTED, + OPT_SCHEMA_FEATURES_DROP_CASCADE, + OPT_SCHEMA_FEATURES_DROP_RESTRICT, + OPT_SCHEMA_FEATURES_EMBEDDED_DDL, + OPT_SCHEMA_FEATURES_IF_NOT_EXISTS, + OPT_SCHEMA_FEATURES_IS_DATABASE, + OPT_SCHEMA_FEATURES_SUPPORTED, + OPT_TUPLE_ARITY_EMPTY, + OPT_TUPLE_ARITY_ONE, + OPT_TUPLE_SYNTAX_PARENTHESES, + OPT_TUPLE_SYNTAX_ROW, + OPT_TUPLE_SYNTAX_STRUCT, + OPT_TUPLE_SYNTAX_UNSUPPORTED, AggregateConfig, BitwiseConfig, CastConfig, @@ -89,71 +74,77 @@ TupleConfig, ) -_ALL_BITWISE = OPT_OPERATIONS_AND | OPT_OPERATIONS_OR | OPT_OPERATIONS_NOT | OPT_OPERATIONS_SHL | OPT_OPERATIONS_SHR +_ALL_BITWISE = ( + OPT_BITWISE_OPERATIONS_AND + | OPT_BITWISE_OPERATIONS_OR + | OPT_BITWISE_OPERATIONS_NOT + | OPT_BITWISE_OPERATIONS_SHL + | OPT_BITWISE_OPERATIONS_SHR +) __all__ = [ - "OPT_ANY_ARRAY_CONSTRUCTOR", - "OPT_ANY_OR_EXPANSION", - "OPT_ANY_PATTERN_LIST", - "OPT_ARITY_EMPTY", - "OPT_ARITY_ONE", - "OPT_AUTHORIZATION_OPTIONAL", - "OPT_AUTHORIZATION_UNSUPPORTED", - "OPT_BOOLEAN_INTEGER", - "OPT_BOOLEAN_KEYWORD", - "OPT_COLLATE_FUNCTION_STRING", - "OPT_COLLATE_INFIX_IDENT", - "OPT_COLLATE_UNSUPPORTED", - "OPT_ESCAPE_ANY", - "OPT_ESCAPE_SCALAR", - "OPT_FEATURES_DROP_CASCADE", - "OPT_FEATURES_DROP_RESTRICT", - "OPT_FEATURES_EMBEDDED_DDL", - "OPT_FEATURES_IF_NOT_EXISTS", - "OPT_FEATURES_IS_DATABASE", - "OPT_FEATURES_SUPPORTED", + "OPT_BITWISE_OPERATIONS_AND", + "OPT_BITWISE_OPERATIONS_NOT", + "OPT_BITWISE_OPERATIONS_OR", + "OPT_BITWISE_OPERATIONS_SHL", + "OPT_BITWISE_OPERATIONS_SHR", + "OPT_BITWISE_SYNTAX_BIT_FUNCTION", + "OPT_BITWISE_SYNTAX_INFIX", + "OPT_BITWISE_SYNTAX_SHIFT_FUNCTION", + "OPT_BITWISE_XOR_BIT_FUNCTION", + "OPT_BITWISE_XOR_CARET", + "OPT_BITWISE_XOR_FUNCTION", + "OPT_BITWISE_XOR_HASH", + "OPT_BITWISE_XOR_UNSUPPORTED", + "OPT_BOOL_FORMAT_INTEGER", + "OPT_BOOL_FORMAT_KEYWORD", "OPT_FUNCTION_FILTER_FILTER", "OPT_FUNCTION_FILTER_FILTER_WHERE", "OPT_FUNCTION_FILTER_INSIDE_WHERE", "OPT_FUNCTION_FILTER_UNSUPPORTED", - "OPT_ILIKE_STANDARD", - "OPT_ILIKE_UNSUPPORTED", - "OPT_ILIKE_USE_LIKE_LOWER", "OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL", "OPT_IS_DISTINCT_FROM_STANDARD", "OPT_IS_DISTINCT_FROM_UNSUPPORTED", - "OPT_MODULO_FUNCTION", - "OPT_MODULO_INFIX", - "OPT_MODULO_PERCENT", + "OPT_I_LIKE_STRATEGY_STANDARD", + "OPT_I_LIKE_STRATEGY_UNSUPPORTED", + "OPT_I_LIKE_STRATEGY_USE_LIKE_LOWER", + "OPT_LIKE_ANY_ARRAY_CONSTRUCTOR", + "OPT_LIKE_ANY_OR_EXPANSION", + "OPT_LIKE_ANY_PATTERN_LIST", + "OPT_LIKE_ESCAPE_ANY", + "OPT_LIKE_ESCAPE_SCALAR", + "OPT_LIKE_REGEX_REGEXP_CONTAINS", + "OPT_LIKE_REGEX_REGEXP_LIKE", + "OPT_LIKE_REGEX_REGEXP_MATCHES", + "OPT_LIKE_REGEX_STANDARD", + "OPT_LIKE_REGEX_TILDE", + "OPT_LIKE_REGEX_UNSUPPORTED", "OPT_NULL_TREATMENT_INSIDE_PARENS", "OPT_NULL_TREATMENT_POSTFIX", "OPT_NULL_TREATMENT_UNSUPPORTED", - "OPT_OPERATIONS_AND", - "OPT_OPERATIONS_NOT", - "OPT_OPERATIONS_OR", - "OPT_OPERATIONS_SHL", - "OPT_OPERATIONS_SHR", - "OPT_REGEX_REGEXP_CONTAINS", - "OPT_REGEX_REGEXP_LIKE", - "OPT_REGEX_REGEXP_MATCHES", - "OPT_REGEX_STANDARD", - "OPT_REGEX_TILDE", - "OPT_REGEX_UNSUPPORTED", - "OPT_SYNTAX_BIT_FUNCTION", - "OPT_SYNTAX_INFIX", - "OPT_SYNTAX_PARENTHESES", - "OPT_SYNTAX_ROW", - "OPT_SYNTAX_SHIFT_FUNCTION", - "OPT_SYNTAX_STRUCT", - "OPT_SYNTAX_UNSUPPORTED", - "OPT_TRIM_FUNCTION", - "OPT_TRIM_SPACES_ONLY", - "OPT_TRIM_SQL_FROM", - "OPT_XOR_BIT_FUNCTION", - "OPT_XOR_CARET", - "OPT_XOR_FUNCTION", - "OPT_XOR_HASH", - "OPT_XOR_UNSUPPORTED", + "OPT_OPERATOR_COLLATE_FUNCTION_STRING", + "OPT_OPERATOR_COLLATE_INFIX_IDENT", + "OPT_OPERATOR_COLLATE_UNSUPPORTED", + "OPT_OPERATOR_MODULO_FUNCTION", + "OPT_OPERATOR_MODULO_INFIX", + "OPT_OPERATOR_MODULO_PERCENT", + "OPT_OPERATOR_TRIM_FUNCTION", + "OPT_OPERATOR_TRIM_SPACES_ONLY", + "OPT_OPERATOR_TRIM_SQL_FROM", + "OPT_SCHEMA_AUTHORIZATION_OPTIONAL", + "OPT_SCHEMA_AUTHORIZATION_UNSUPPORTED", + "OPT_SCHEMA_FEATURES_DROP_CASCADE", + "OPT_SCHEMA_FEATURES_DROP_RESTRICT", + "OPT_SCHEMA_FEATURES_EMBEDDED_DDL", + "OPT_SCHEMA_FEATURES_IF_NOT_EXISTS", + "OPT_SCHEMA_FEATURES_IS_DATABASE", + "OPT_SCHEMA_FEATURES_SUPPORTED", + "OPT_TUPLE_ARITY_EMPTY", + "OPT_TUPLE_ARITY_ONE", + "OPT_TUPLE_SYNTAX_PARENTHESES", + "OPT_TUPLE_SYNTAX_ROW", + "OPT_TUPLE_SYNTAX_STRUCT", + "OPT_TUPLE_SYNTAX_UNSUPPORTED", "AggregateConfig", "BigQuery", "BitwiseConfig", @@ -183,10 +174,10 @@ BigQuery = Dialect( placeholder="?", like=LikeConfig( - ilike=OPT_ILIKE_USE_LIKE_LOWER, - regex=OPT_REGEX_REGEXP_CONTAINS, + ilike=OPT_I_LIKE_STRATEGY_USE_LIKE_LOWER, + regex=OPT_LIKE_REGEX_REGEXP_CONTAINS, ), - bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_BITWISE_XOR_CARET), identifier="`{slashbacktick}`", string=StringConfig(template="'{hex}'"), cast=CastConfig(try_cast="SAFE_CAST({expr} AS {type})"), @@ -204,29 +195,29 @@ ), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, - trim=OPT_TRIM_FUNCTION, - collate=OPT_COLLATE_FUNCTION_STRING, + trim=OPT_OPERATOR_TRIM_FUNCTION, + collate=OPT_OPERATOR_COLLATE_FUNCTION_STRING, ), aggregate=AggregateConfig( null_treatment=OPT_NULL_TREATMENT_INSIDE_PARENS, function_filter=OPT_FUNCTION_FILTER_INSIDE_WHERE, ), - schema=SchemaConfig(features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS), + schema=SchemaConfig(features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS), ) Databricks = Dialect( placeholder="?", like=LikeConfig( - escape=OPT_ESCAPE_SCALAR, - any=OPT_ANY_PATTERN_LIST, - ilike=OPT_ILIKE_STANDARD, - regex=OPT_REGEX_STANDARD, + escape=OPT_LIKE_ESCAPE_SCALAR, + any=OPT_LIKE_ANY_PATTERN_LIST, + ilike=OPT_I_LIKE_STRATEGY_STANDARD, + regex=OPT_LIKE_REGEX_STANDARD, ), - bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_BITWISE_XOR_CARET), identifier="`{backtick}`", string=StringConfig(template="'{unicode_quote}'"), cast=CastConfig(supports_double_colon=True), - tuple=TupleConfig(syntax=OPT_SYNTAX_STRUCT, arity=OPT_ARITY_ONE), + tuple=TupleConfig(syntax=OPT_TUPLE_SYNTAX_STRUCT, arity=OPT_TUPLE_ARITY_ONE), temporal=TemporalConfig( date="DATE '{}'", time="'{}'", @@ -236,14 +227,14 @@ literal=LiteralConfig(uuid="'{}'"), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, - collate=OPT_COLLATE_INFIX_IDENT, + collate=OPT_OPERATOR_COLLATE_INFIX_IDENT, ), aggregate=AggregateConfig( null_treatment=OPT_NULL_TREATMENT_POSTFIX, function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE, ), schema=SchemaConfig( - features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS, + features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS, location=" LOCATION {}", ), ) @@ -251,14 +242,14 @@ DuckDb = Dialect( placeholder="${}", like=LikeConfig( - escape=OPT_ESCAPE_SCALAR, - ilike=OPT_ILIKE_STANDARD, - regex=OPT_REGEX_REGEXP_MATCHES, + escape=OPT_LIKE_ESCAPE_SCALAR, + ilike=OPT_I_LIKE_STRATEGY_STANDARD, + regex=OPT_LIKE_REGEX_REGEXP_MATCHES, supports_similar_to=True, ), - bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_FUNCTION), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_BITWISE_XOR_FUNCTION), cast=CastConfig(supports_double_colon=True), - tuple=TupleConfig(syntax=OPT_SYNTAX_ROW, arity=OPT_ARITY_EMPTY | OPT_ARITY_ONE), + tuple=TupleConfig(syntax=OPT_TUPLE_SYNTAX_ROW, arity=OPT_TUPLE_ARITY_EMPTY | OPT_TUPLE_ARITY_ONE), temporal=TemporalConfig( date="DATE '{}'", time="TIME '{}'", @@ -274,27 +265,27 @@ ), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, - trim=OPT_TRIM_FUNCTION, - collate=OPT_COLLATE_INFIX_IDENT, + trim=OPT_OPERATOR_TRIM_FUNCTION, + collate=OPT_OPERATOR_COLLATE_INFIX_IDENT, ), aggregate=AggregateConfig( null_treatment=OPT_NULL_TREATMENT_INSIDE_PARENS, function_filter=OPT_FUNCTION_FILTER_FILTER, ), - schema=SchemaConfig(features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS), + schema=SchemaConfig(features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS), ) Hive = Dialect( placeholder="?", like=LikeConfig( - ilike=OPT_ILIKE_USE_LIKE_LOWER, - regex=OPT_REGEX_STANDARD, + ilike=OPT_I_LIKE_STRATEGY_USE_LIKE_LOWER, + regex=OPT_LIKE_REGEX_STANDARD, ), - bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_BITWISE_XOR_CARET), identifier="`{backtick}`", string=StringConfig(template="'{unicode}'"), cast=CastConfig(try_cast=None), - tuple=TupleConfig(syntax=OPT_SYNTAX_STRUCT, arity=OPT_ARITY_ONE), + tuple=TupleConfig(syntax=OPT_TUPLE_SYNTAX_STRUCT, arity=OPT_TUPLE_ARITY_ONE), temporal=TemporalConfig( date="DATE '{}'", time="'{}'", @@ -308,20 +299,20 @@ ), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL, - trim=OPT_TRIM_SPACES_ONLY, + trim=OPT_OPERATOR_TRIM_SPACES_ONLY, ), schema=SchemaConfig( - features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_IS_DATABASE, + features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS | OPT_SCHEMA_FEATURES_IS_DATABASE, location=" LOCATION {}", ), ) MsSql = Dialect( placeholder="?", - like=LikeConfig(escape=OPT_ESCAPE_SCALAR), + like=LikeConfig(escape=OPT_LIKE_ESCAPE_SCALAR), bitwise=BitwiseConfig( - operations=OPT_OPERATIONS_AND | OPT_OPERATIONS_OR | OPT_OPERATIONS_NOT, - xor=OPT_XOR_CARET, + operations=OPT_BITWISE_OPERATIONS_AND | OPT_BITWISE_OPERATIONS_OR | OPT_BITWISE_OPERATIONS_NOT, + xor=OPT_BITWISE_XOR_CARET, ), identifier="[{bracket}]", string=StringConfig(template="N'{single}'", concat="{left} + {right}"), @@ -332,29 +323,29 @@ datetime_aware="N'{}'", ), literal=LiteralConfig( - boolean=OPT_BOOLEAN_INTEGER, + boolean=OPT_BOOL_FORMAT_INTEGER, bytes="0x{hex}", uuid="N'{}'", ), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, - collate=OPT_COLLATE_INFIX_IDENT, + collate=OPT_OPERATOR_COLLATE_INFIX_IDENT, ), aggregate=AggregateConfig(null_treatment=OPT_NULL_TREATMENT_POSTFIX), schema=SchemaConfig( - features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_EMBEDDED_DDL, - authorization=OPT_AUTHORIZATION_OPTIONAL, + features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_EMBEDDED_DDL, + authorization=OPT_SCHEMA_AUTHORIZATION_OPTIONAL, ), ) MySql = Dialect( placeholder="?", - like=LikeConfig(escape=OPT_ESCAPE_SCALAR, regex=OPT_REGEX_STANDARD), - bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + like=LikeConfig(escape=OPT_LIKE_ESCAPE_SCALAR, regex=OPT_LIKE_REGEX_STANDARD), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_BITWISE_XOR_CARET), identifier="`{backtick}`", string=StringConfig(template="'{mysql}'", concat="CONCAT({left}, {right})"), cast=CastConfig(try_cast=None), - tuple=TupleConfig(syntax=OPT_SYNTAX_ROW, arity=OPT_ARITY_ONE), + tuple=TupleConfig(syntax=OPT_TUPLE_SYNTAX_ROW, arity=OPT_TUPLE_ARITY_ONE), temporal=TemporalConfig( date="'{}'", time="'{}'", @@ -363,26 +354,26 @@ ), literal=LiteralConfig( bit="b'{}'", - boolean=OPT_BOOLEAN_INTEGER, + boolean=OPT_BOOL_FORMAT_INTEGER, uuid="'{}'", ), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL, - collate=OPT_COLLATE_INFIX_IDENT, + collate=OPT_OPERATOR_COLLATE_INFIX_IDENT, ), schema=SchemaConfig( - features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_IS_DATABASE, + features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS | OPT_SCHEMA_FEATURES_IS_DATABASE, ), ) Oracle = Dialect( placeholder=":{}", like=LikeConfig( - escape=OPT_ESCAPE_SCALAR, - ilike=OPT_ILIKE_USE_LIKE_LOWER, - regex=OPT_REGEX_REGEXP_LIKE, + escape=OPT_LIKE_ESCAPE_SCALAR, + ilike=OPT_I_LIKE_STRATEGY_USE_LIKE_LOWER, + regex=OPT_LIKE_REGEX_REGEXP_LIKE, ), - bitwise=BitwiseConfig(operations=OPT_OPERATIONS_AND, syntax=OPT_SYNTAX_BIT_FUNCTION), + bitwise=BitwiseConfig(operations=OPT_BITWISE_OPERATIONS_AND, syntax=OPT_BITWISE_SYNTAX_BIT_FUNCTION), cast=CastConfig(try_cast=None), temporal=TemporalConfig( date="DATE '{}'", @@ -391,31 +382,31 @@ datetime_aware="TIMESTAMP WITH TIMEZONE '{}'", ), literal=LiteralConfig( - boolean=OPT_BOOLEAN_INTEGER, + boolean=OPT_BOOL_FORMAT_INTEGER, bytes="HEXTORAW('{hex}')", uuid="'{}'", ), - operator=OperatorConfig(modulo=OPT_MODULO_FUNCTION, collate=OPT_COLLATE_INFIX_IDENT), + operator=OperatorConfig(modulo=OPT_OPERATOR_MODULO_FUNCTION, collate=OPT_OPERATOR_COLLATE_INFIX_IDENT), aggregate=AggregateConfig( null_treatment=OPT_NULL_TREATMENT_POSTFIX, function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE, ), - schema=SchemaConfig(features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_EMBEDDED_DDL), + schema=SchemaConfig(features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_EMBEDDED_DDL), ) PostgreSql = Dialect( placeholder="${}", like=LikeConfig( - escape=OPT_ESCAPE_SCALAR, - any=OPT_ANY_ARRAY_CONSTRUCTOR, - ilike=OPT_ILIKE_STANDARD, - regex=OPT_REGEX_TILDE, + escape=OPT_LIKE_ESCAPE_SCALAR, + any=OPT_LIKE_ANY_ARRAY_CONSTRUCTOR, + ilike=OPT_I_LIKE_STRATEGY_STANDARD, + regex=OPT_LIKE_REGEX_TILDE, supports_similar_to=True, ), - bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_HASH), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_BITWISE_XOR_HASH), string=StringConfig(template="E'{hex}'"), cast=CastConfig(try_cast=None, supports_double_colon=True), - tuple=TupleConfig(syntax=OPT_SYNTAX_ROW, arity=OPT_ARITY_EMPTY | OPT_ARITY_ONE), + tuple=TupleConfig(syntax=OPT_TUPLE_SYNTAX_ROW, arity=OPT_TUPLE_ARITY_EMPTY | OPT_TUPLE_ARITY_ONE), temporal=TemporalConfig( date="DATE E'{}'", time="TIME E'{}'", @@ -432,24 +423,26 @@ ), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, - collate=OPT_COLLATE_INFIX_IDENT, + collate=OPT_OPERATOR_COLLATE_INFIX_IDENT, ), aggregate=AggregateConfig(function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE), schema=SchemaConfig( - features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_EMBEDDED_DDL, - authorization=OPT_AUTHORIZATION_OPTIONAL, + features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS | OPT_SCHEMA_FEATURES_EMBEDDED_DDL, + authorization=OPT_SCHEMA_AUTHORIZATION_OPTIONAL, ), ) Redshift = Dialect( placeholder="${}", like=LikeConfig( - escape=OPT_ESCAPE_SCALAR, - ilike=OPT_ILIKE_STANDARD, - regex=OPT_REGEX_TILDE, + escape=OPT_LIKE_ESCAPE_SCALAR, + ilike=OPT_I_LIKE_STRATEGY_STANDARD, + regex=OPT_LIKE_REGEX_TILDE, supports_similar_to=True, ), - bitwise=BitwiseConfig(operations=OPT_OPERATIONS_AND | OPT_OPERATIONS_SHL | OPT_OPERATIONS_SHR), + bitwise=BitwiseConfig( + operations=OPT_BITWISE_OPERATIONS_AND | OPT_BITWISE_OPERATIONS_SHL | OPT_BITWISE_OPERATIONS_SHR + ), cast=CastConfig(supports_double_colon=True), temporal=TemporalConfig( date="'{}'", @@ -458,26 +451,26 @@ datetime_aware="'{}'", ), literal=LiteralConfig(bytes="TO_VARBYTE('{hex}', 'hex')", uuid="'{}'"), - operator=OperatorConfig(collate=OPT_COLLATE_FUNCTION_STRING), + operator=OperatorConfig(collate=OPT_OPERATOR_COLLATE_FUNCTION_STRING), aggregate=AggregateConfig(null_treatment=OPT_NULL_TREATMENT_POSTFIX), schema=SchemaConfig( - features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_EMBEDDED_DDL, - authorization=OPT_AUTHORIZATION_OPTIONAL, + features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS | OPT_SCHEMA_FEATURES_EMBEDDED_DDL, + authorization=OPT_SCHEMA_AUTHORIZATION_OPTIONAL, ), ) Snowflake = Dialect( placeholder=":{}", like=LikeConfig( - escape=OPT_ESCAPE_SCALAR | OPT_ESCAPE_ANY, - any=OPT_ANY_PATTERN_LIST, - ilike=OPT_ILIKE_STANDARD, - regex=OPT_REGEX_STANDARD, + escape=OPT_LIKE_ESCAPE_SCALAR | OPT_LIKE_ESCAPE_ANY, + any=OPT_LIKE_ANY_PATTERN_LIST, + ilike=OPT_I_LIKE_STRATEGY_STANDARD, + regex=OPT_LIKE_REGEX_STANDARD, ), bitwise=BitwiseConfig( operations=_ALL_BITWISE, - syntax=OPT_SYNTAX_BIT_FUNCTION, - xor=OPT_XOR_BIT_FUNCTION, + syntax=OPT_BITWISE_SYNTAX_BIT_FUNCTION, + xor=OPT_BITWISE_XOR_BIT_FUNCTION, ), string=StringConfig(template="'{hex}'"), cast=CastConfig(supports_double_colon=True), @@ -494,12 +487,12 @@ ), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, - trim=OPT_TRIM_FUNCTION, - collate=OPT_COLLATE_FUNCTION_STRING, + trim=OPT_OPERATOR_TRIM_FUNCTION, + collate=OPT_OPERATOR_COLLATE_FUNCTION_STRING, ), aggregate=AggregateConfig(null_treatment=OPT_NULL_TREATMENT_POSTFIX), schema=SchemaConfig( - features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS, + features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS, location=" EXTERNAL_VOLUME = {}", ), ) @@ -507,16 +500,16 @@ Spark = Dialect( placeholder="?", like=LikeConfig( - escape=OPT_ESCAPE_SCALAR, - any=OPT_ANY_PATTERN_LIST, - ilike=OPT_ILIKE_STANDARD, - regex=OPT_REGEX_STANDARD, + escape=OPT_LIKE_ESCAPE_SCALAR, + any=OPT_LIKE_ANY_PATTERN_LIST, + ilike=OPT_I_LIKE_STRATEGY_STANDARD, + regex=OPT_LIKE_REGEX_STANDARD, ), - bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_XOR_CARET), + bitwise=BitwiseConfig(operations=_ALL_BITWISE, xor=OPT_BITWISE_XOR_CARET), identifier="`{backtick}`", string=StringConfig(template="'{unicode_quote}'"), cast=CastConfig(supports_double_colon=True), - tuple=TupleConfig(syntax=OPT_SYNTAX_STRUCT, arity=OPT_ARITY_ONE), + tuple=TupleConfig(syntax=OPT_TUPLE_SYNTAX_STRUCT, arity=OPT_TUPLE_ARITY_ONE), temporal=TemporalConfig( date="DATE '{}'", time="'{}'", @@ -530,38 +523,38 @@ function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE, ), schema=SchemaConfig( - features=OPT_FEATURES_SUPPORTED | OPT_FEATURES_IF_NOT_EXISTS | OPT_FEATURES_IS_DATABASE, + features=OPT_SCHEMA_FEATURES_SUPPORTED | OPT_SCHEMA_FEATURES_IF_NOT_EXISTS | OPT_SCHEMA_FEATURES_IS_DATABASE, location=" LOCATION {}", ), ) Sqlite = Dialect( - like=LikeConfig(escape=OPT_ESCAPE_SCALAR), + like=LikeConfig(escape=OPT_LIKE_ESCAPE_SCALAR), bitwise=BitwiseConfig(operations=_ALL_BITWISE), cast=CastConfig(try_cast=None), - tuple=TupleConfig(syntax=OPT_SYNTAX_PARENTHESES, arity=OPT_ARITY_EMPTY), + tuple=TupleConfig(syntax=OPT_TUPLE_SYNTAX_PARENTHESES, arity=OPT_TUPLE_ARITY_EMPTY), temporal=TemporalConfig( date="'{}'", time="'{}'", datetime_naive="'{}'", datetime_aware="'{}'", ), - literal=LiteralConfig(boolean=OPT_BOOLEAN_INTEGER, uuid="'{}'"), + literal=LiteralConfig(boolean=OPT_BOOL_FORMAT_INTEGER, uuid="'{}'"), operator=OperatorConfig( is_distinct_from=OPT_IS_DISTINCT_FROM_STANDARD, - trim=OPT_TRIM_FUNCTION, - collate=OPT_COLLATE_INFIX_IDENT, + trim=OPT_OPERATOR_TRIM_FUNCTION, + collate=OPT_OPERATOR_COLLATE_INFIX_IDENT, ), aggregate=AggregateConfig(function_filter=OPT_FUNCTION_FILTER_FILTER_WHERE), ) Teradata = Dialect( placeholder="?", - like=LikeConfig(escape=OPT_ESCAPE_SCALAR, ilike=OPT_ILIKE_USE_LIKE_LOWER), + like=LikeConfig(escape=OPT_LIKE_ESCAPE_SCALAR, ilike=OPT_I_LIKE_STRATEGY_USE_LIKE_LOWER), bitwise=BitwiseConfig( operations=_ALL_BITWISE, - syntax=OPT_SYNTAX_SHIFT_FUNCTION, - xor=OPT_XOR_BIT_FUNCTION, + syntax=OPT_BITWISE_SYNTAX_SHIFT_FUNCTION, + xor=OPT_BITWISE_XOR_BIT_FUNCTION, ), cast=CastConfig(try_cast="TRYCAST({expr} AS {type})"), temporal=TemporalConfig( @@ -571,6 +564,6 @@ datetime_aware="TIMESTAMP WITH TIMEZONE '{}'", ), literal=LiteralConfig(bit="'{}'B", bytes="'{hex}'XBV", uuid="'{}'"), - operator=OperatorConfig(modulo=OPT_MODULO_INFIX), + operator=OperatorConfig(modulo=OPT_OPERATOR_MODULO_INFIX), aggregate=AggregateConfig(null_treatment=OPT_NULL_TREATMENT_INSIDE_PARENS), ) diff --git a/rapidquery/dialect.pyi b/rapidquery/dialect.pyi index 9118f45..bb21a7b 100644 --- a/rapidquery/dialect.pyi +++ b/rapidquery/dialect.pyi @@ -7,7 +7,8 @@ class AggregateConfig: """Aggregate ``{IGNORE|RESPECT} NULLS`` and ``FILTER`` spelling. Controls how null-treatment modifiers and aggregate filters are written - inside function calls such as ``FIRST_VALUE``. + inside function calls such as ``FIRST_VALUE``. Both options default to + ``UNSUPPORTED``, which rejects the syntax. """ def __new__( @@ -20,9 +21,11 @@ class AggregateConfig: Args: null_treatment: ``OPT_NULL_TREATMENT_*`` value selecting where - ``IGNORE NULLS`` / ``RESPECT NULLS`` is written. ``0`` rejects it. + ``IGNORE NULLS`` / ``RESPECT NULLS`` is written. Defaults to + ``UNSUPPORTED`` (``0``), which rejects it. function_filter: ``OPT_FUNCTION_FILTER_*`` value selecting the - ``FILTER`` syntax. ``0`` rejects it. + aggregate ``FILTER`` syntax. Defaults to ``UNSUPPORTED`` (``0``), + which rejects it. """ def __eq__(self, value: object, /) -> bool: @@ -33,19 +36,21 @@ class AggregateConfig: @property def null_treatment(self) -> int: - """``OPT_NULL_TREATMENT_*`` value: ``UNSUPPORTED``, ``INSIDE_PARENS``, or ``POSTFIX``. + """``OPT_NULL_TREATMENT_*`` value for ``{IGNORE|RESPECT} NULLS`` in functions. + ``UNSUPPORTED`` rejects the syntax. ``INSIDE_PARENS`` writes ``FIRST_VALUE(x IGNORE NULLS)``. ``POSTFIX`` writes ``FIRST_VALUE(x) IGNORE NULLS``. """ @property def function_filter(self) -> int: - """``OPT_FUNCTION_FILTER_*`` value describing the aggregate ``FILTER`` syntax. + """``OPT_FUNCTION_FILTER_*`` value for the function ``FILTER`` syntax. - ``FILTER`` writes ``f(x) FILTER (cond)``. - ``FILTER_WHERE`` writes ``f(x) FILTER (WHERE cond)``. - ``INSIDE_WHERE`` writes ``f(x WHERE cond)``. + ``UNSUPPORTED`` rejects the syntax. + ``FILTER`` writes ``FIRST_VALUE(x) FILTER (id > 10)``. + ``FILTER_WHERE`` writes ``FIRST_VALUE(x) FILTER (WHERE id > 10)``. + ``INSIDE_WHERE`` writes ``FIRST_VALUE(x WHERE id > 10)``. """ @final @@ -53,6 +58,8 @@ class BitwiseConfig: """Which bitwise operators exist, and how ``& | ~ << >>`` and ``^`` are spelled. A bitwise operator is rejected unless its flag is set in ``operations``. + AND, OR, NOT, and the shifts share one spelling (``syntax``). XOR has its + own spelling (``xor``). """ def __new__( @@ -65,9 +72,13 @@ class BitwiseConfig: """Create a ``BitwiseConfig``. Args: - operations: ``OPT_OPERATIONS_*`` flags for the operators the dialect supports. - syntax: ``OPT_SYNTAX_*`` value shared by ``&``, ``|``, ``~``, ``<<``, and ``>>``. - xor: ``OPT_XOR_*`` value selecting how ``^`` is written. + operations: ``OPT_BITWISE_OPERATIONS_*`` flags (``AND``, ``OR``, ``NOT``, + ``SHL``, ``SHR``) for the operators the dialect supports. A missing + flag rejects that operator. Defaults to none (``0``). + syntax: ``OPT_BITWISE_SYNTAX_*`` value shared by ``&``, ``|``, ``~``, + ``<<``, and ``>>``. Defaults to ``INFIX``. + xor: ``OPT_BITWISE_XOR_*`` value selecting how ``^`` is written. + Defaults to ``UNSUPPORTED`` (``0``), which rejects XOR. """ def __eq__(self, value: object, /) -> bool: @@ -78,11 +89,15 @@ class BitwiseConfig: @property def operations(self) -> int: - """``OPT_OPERATIONS_*`` flags for the supported operators: AND, OR, NOT, SHL, SHR.""" + """``OPT_BITWISE_OPERATIONS_*`` flags for the operators the dialect can render. + + Flags: ``AND``, ``OR``, ``NOT``, ``SHL``, ``SHR``. A missing flag rejects + that operator. XOR is controlled separately by ``xor``. + """ @property def syntax(self) -> int: - """``OPT_SYNTAX_*`` value for the spelling shared by AND, OR, NOT, and shifts. + """``OPT_BITWISE_SYNTAX_*`` value for the spelling shared by AND, OR, NOT, and shifts. ``INFIX`` writes ``& | ~ << >>``. ``BIT_FUNCTION`` writes ``BITAND`` / ``BITOR`` / ``BITNOT`` / ``BITSHIFTLEFT`` / ``BITSHIFTRIGHT``. @@ -91,8 +106,9 @@ class BitwiseConfig: @property def xor(self) -> int: - """``OPT_XOR_*`` value for how ``^`` (XOR) is spelled. + """``OPT_BITWISE_XOR_*`` value for how ``^`` (XOR) is spelled. + ``UNSUPPORTED`` rejects XOR. ``CARET`` writes ``left ^ right``. ``HASH`` writes ``left # right``. In PostgreSQL, ``^`` is exponentiation. ``FUNCTION`` writes ``xor(left, right)``. @@ -112,9 +128,11 @@ class CastConfig: """Create a ``CastConfig``. Args: - try_cast: Template for ``TRY_CAST`` with ``{expr}`` and ``{type}`` keys, - or ``None`` if the dialect has no ``TRY_CAST``. - supports_double_colon: Whether ``expr::type`` is valid syntax. + try_cast: Template for ``TRY_CAST`` with exactly the keys ``{expr}`` and + ``{type}`` (e.g. ``TRY_CAST({expr} AS {type})``, the default), or + ``None`` if the dialect has no ``TRY_CAST``. + supports_double_colon: Whether the ``expr::type`` cast syntax is valid. + Defaults to ``False``. """ def __eq__(self, value: object, /) -> bool: @@ -125,7 +143,7 @@ class CastConfig: @property def try_cast(self) -> str | None: - """Template for ``TRY_CAST``, using ``{expr}`` and ``{type}`` keys, or ``None`` if unsupported.""" + """Template for ``TRY_CAST``, using the ``{expr}`` and ``{type}`` keys, or ``None`` if unsupported.""" @property def supports_double_colon(self) -> bool: @@ -147,12 +165,18 @@ class LikeConfig: """Create a ``LikeConfig``. Args: - escape: ``OPT_ESCAPE_*`` flags for where an ``ESCAPE`` clause is legal. - ``0`` writes no ``ESCAPE`` and rejects an escape character. - any: ``OPT_ANY_*`` value selecting how ``LIKE ANY`` / ``ILIKE ANY`` is rendered. - ilike: ``OPT_ILIKE_*`` value selecting how ``ILIKE`` is rendered. - regex: ``OPT_REGEX_*`` value selecting how regex matching is spelled. - supports_similar_to: Whether the dialect supports ``SIMILAR TO``. + escape: ``OPT_LIKE_ESCAPE_*`` flags for where an inlined ``LIKE ... ESCAPE`` + clause is legal. Empty (``0``, the default) writes no ``ESCAPE`` and + rejects a set escape character. ``SCALAR | ANY`` allows both a scalar + pattern and native ``LIKE ANY``. + any: ``OPT_LIKE_ANY_*`` value selecting how ``LIKE ANY`` / ``ILIKE ANY`` + is rendered. Defaults to ``OR_EXPANSION``. + ilike: ``OPT_I_LIKE_STRATEGY_*`` value selecting whether ``ILIKE`` is + native or falls back to another form. Defaults to ``UNSUPPORTED``. + regex: ``OPT_LIKE_REGEX_*`` value selecting how regex matching is spelled. + Defaults to ``UNSUPPORTED``. + supports_similar_to: Whether the dialect supports the ``SIMILAR TO`` + predicate. Defaults to ``False``. """ def __eq__(self, value: object, /) -> bool: @@ -163,38 +187,43 @@ class LikeConfig: @property def escape(self) -> int: - """``OPT_ESCAPE_*`` flags for where an ``ESCAPE`` clause is allowed. + """``OPT_LIKE_ESCAPE_*`` flags for where an inlined ``ESCAPE`` clause is legal. - ``SCALAR`` allows a scalar ``LIKE`` pattern with ``ESCAPE``. - ``ANY`` allows native ``LIKE ANY (...) ESCAPE``. + Empty writes no ``ESCAPE`` and rejects a set escape character. + ``SCALAR`` allows scalar ``LIKE`` and an ``OR`` expansion. + ``ANY`` allows native ``LIKE ANY (p, ...) ESCAPE``. + ``SCALAR | ANY`` allows both. """ @property def any(self) -> int: - """``OPT_ANY_*`` value selecting the ``LIKE ANY`` rendering. + """``OPT_LIKE_ANY_*`` value selecting the ``LIKE ANY`` / ``ILIKE ANY`` rendering. - ``OR_EXPANSION`` writes ``(x LIKE p1 OR x LIKE p2)``. + ``OR_EXPANSION`` writes ``(x LIKE p1 OR x LIKE p2)``. Useful when the + dialect has no native support. ``ARRAY_CONSTRUCTOR`` writes ``LIKE ANY (ARRAY[p, ...])``. ``PATTERN_LIST`` writes ``LIKE ANY (p, ...)``. """ @property def ilike(self) -> int: - """``OPT_ILIKE_*`` value selecting how ``ILIKE`` is rendered. + """``OPT_I_LIKE_STRATEGY_*`` value: does the dialect support ``ILIKE``, or should it fall back? + ``UNSUPPORTED`` rejects ``ILIKE``. ``STANDARD`` uses native ``ILIKE``. ``USE_LIKE_LOWER`` writes ``LIKE`` with ``LOWER()`` on both sides. - ``UNSUPPORTED`` rejects ``ILIKE``. """ @property def regex(self) -> int: - """``OPT_REGEX_*`` value selecting how regex matching is spelled. + """``OPT_LIKE_REGEX_*`` value selecting how regex matching (``RLIKE`` / ``~`` / ``REGEXP_LIKE``, ...) is spelled. + ``UNSUPPORTED`` rejects regex matching. ``STANDARD`` writes ``expr [NOT] RLIKE pattern``. ``TILDE`` writes ``expr ~ pattern`` / ``expr !~ pattern``. - ``REGEXP_LIKE``, ``REGEXP_CONTAINS``, and ``REGEXP_MATCHES`` write the matching function call. - ``UNSUPPORTED`` rejects regex matching. + ``REGEXP_LIKE`` writes ``REGEXP_LIKE(expr, pattern)``. + ``REGEXP_CONTAINS`` writes ``REGEXP_CONTAINS(expr, pattern)``. + ``REGEXP_MATCHES`` writes ``regexp_matches(expr, pattern)``. """ @property @@ -205,8 +234,10 @@ class LikeConfig: class LiteralConfig: """Boolean, bit, bytes, network, UUID, JSON, and array literals. - ``bit``, ``inet``, ``macaddr``, and ``array`` are ``None`` when the dialect - has no spelling. ``json`` receives an already quoted string. + ``bit``, ``inet``, ``macaddr``, ``uuid``, and ``array`` are ``None`` when the + dialect has no spelling. Every template other than ``bytes`` has exactly one + ``{}`` placeholder. ``json`` receives a value that is already escaped and + includes its own quotes. """ def __new__( @@ -221,18 +252,27 @@ class LiteralConfig: json: str = "{}", array: str | None = None, ) -> Self: - """Create a ``LiteralConfig``. + r"""Create a ``LiteralConfig``. Args: - boolean: ``OPT_BOOLEAN_*`` value. ``KEYWORD`` writes ``TRUE`` / ``FALSE``, - and ``INTEGER`` writes ``1`` / ``0``. - bit: Template for bit-string literals, such as ``B'{}'``, or ``None`` if unsupported. - bytes: Template for byte-string literals, using the ``{hex}`` key (e.g. ``X'{hex}'``). - inet: Template for inet literals, or ``None`` if the dialect has no spelling. - macaddr: Template for MAC address literals, or ``None`` if the dialect has no spelling. - uuid: Template for UUID literals, or ``None`` if the dialect has no spelling. - json: Template for JSON literals. The value already includes its quotes. - array: Template for array literals, such as ``ARRAY[{}]``, or ``None`` if unsupported. + boolean: ``OPT_BOOL_FORMAT_*`` value. ``KEYWORD`` (default) writes + ``TRUE`` / ``FALSE``, and ``INTEGER`` writes ``1`` / ``0``. + bit: Template for bit-string literals, such as ``B'{}'``, or ``None`` + if unsupported. Prefer ``serialize_bits`` over using it directly. + bytes: Template for byte-string literals, with exactly one key: + ``{hex}`` (bytes as hex, e.g. ``DEADBEEF``) or ``{slashx}`` (each + byte as ``\xNN``). Defaults to ``X'{hex}'``. Prefer + ``serialize_bytes`` over using it directly. + inet: Template for inet literals, such as ``'{}'::inet``, or ``None`` + if the dialect has no spelling. + macaddr: Template for MAC address literals, such as ``'{}'::macaddr``, + or ``None`` if the dialect has no spelling. + uuid: Template for UUID literals, such as ``'{}'::uuid``, or ``None`` + if the dialect has no spelling. + json: Template for JSON literals, such as ``JSON {}``. The value is + escaped and already includes its quotes. Defaults to ``{}``. + array: Template for array literals, such as ``ARRAY[{}]``, or ``None`` + if unsupported. """ def __eq__(self, value: object, /) -> bool: @@ -243,36 +283,56 @@ class LiteralConfig: @property def boolean(self) -> int: - """``OPT_BOOLEAN_*`` value: ``KEYWORD`` for ``TRUE``/``FALSE``, or ``INTEGER`` for ``1``/``0``.""" + """``OPT_BOOL_FORMAT_*`` value: ``KEYWORD`` for ``TRUE``/``FALSE``, or ``INTEGER`` for ``1``/``0``.""" @property def bit(self) -> str | None: - """Template for bit-string literals, or ``None`` if the dialect has no bit literal.""" + """Template for bit-string literals (e.g. ``B'{}'``), or ``None`` if the dialect has no bit literal. + + Prefer ``serialize_bits`` over using this template directly. + """ @property def bytes(self) -> str: - """Template for byte-string literals, using the ``{hex}`` key (e.g. ``X'{hex}'``).""" + r"""Template for byte-string literals (e.g. ``X'{hex}'``). + + Template keys: ``{hex}`` is the bytes as hex (e.g. ``DEADBEEF``), and + ``{slashx}`` is each byte as ``\xNN``. Prefer ``serialize_bytes`` over + using this template directly. + """ @property def inet(self) -> str | None: - """Template for inet literals, or ``None`` if unsupported.""" + """Template for inet literals (e.g. ``'{}'::inet``), or ``None`` if unsupported.""" @property def macaddr(self) -> str | None: - """Template for MAC address literals, or ``None`` if unsupported.""" + """Template for MAC address literals (e.g. ``'{}'::macaddr``), or ``None`` if unsupported.""" @property def uuid(self) -> str | None: - """Template for UUID literals, or ``None`` if unsupported.""" + """Template for UUID literals (e.g. ``'{}'::uuid``), or ``None`` if unsupported.""" @property def json(self) -> str: - """Template for JSON literals. The value passed in already includes its quotes.""" + """Template for JSON literals (e.g. ``JSON {}``). + + The value passed in is already escaped and includes its own quotes. + """ @property def array(self) -> str | None: """Template for array literals (e.g. ``ARRAY[{}]``), or ``None`` if unsupported.""" + def serialize_boolean(self, value: bool, /) -> str: + """Write a boolean literal as ``TRUE``/``FALSE`` or ``1``/``0``, according to ``boolean``.""" + + def serialize_bits(self, value: str, /) -> str: + """Write a bit-string (a string of ``'0'``/``'1'`` characters) as a native literal using ``bit``.""" + + def serialize_bytes(self, value: bytes, /) -> str: + """Write ``bytes`` as a native blob/bytea/varbyte literal using the ``bytes`` template.""" + @final class OperatorConfig: """``%``, ``IS DISTINCT FROM``, ``TRIM``, and ``COLLATE`` spelling.""" @@ -288,11 +348,14 @@ class OperatorConfig: """Create an ``OperatorConfig``. Args: - modulo: ``OPT_MODULO_*`` value selecting how modulo is written. + modulo: ``OPT_OPERATOR_MODULO_*`` value selecting how modulo is + written. Defaults to ``PERCENT``. is_distinct_from: ``OPT_IS_DISTINCT_FROM_*`` value selecting how - ``IS [NOT] DISTINCT FROM`` is written. - trim: ``OPT_TRIM_*`` value selecting how ``TRIM`` is written. - collate: ``OPT_COLLATE_*`` value selecting how ``COLLATE`` is written. + ``IS [NOT] DISTINCT FROM`` is written. Defaults to ``UNSUPPORTED``. + trim: ``OPT_OPERATOR_TRIM_*`` value selecting how ``TRIM`` is written. + Defaults to ``SQL_FROM``. + collate: ``OPT_OPERATOR_COLLATE_*`` value selecting how ``COLLATE`` is + written. Defaults to ``UNSUPPORTED``. """ def __eq__(self, value: object, /) -> bool: @@ -303,7 +366,7 @@ class OperatorConfig: @property def modulo(self) -> int: - """``OPT_MODULO_*`` value for modulo. + """``OPT_OPERATOR_MODULO_*`` value for how binary modulo (``%``) is spelled. ``PERCENT`` writes ``left % right``. ``FUNCTION`` writes ``MOD(left, right)``. @@ -314,14 +377,15 @@ class OperatorConfig: def is_distinct_from(self) -> int: """``OPT_IS_DISTINCT_FROM_*`` value for ``IS [NOT] DISTINCT FROM``. - ``STANDARD`` writes ``left IS [NOT] DISTINCT FROM right``. - ``NULL_SAFE_EQUAL`` uses ``<=>`` for null-safe equality. ``IS DISTINCT FROM`` becomes ``NOT (left <=> right)``. ``UNSUPPORTED`` rejects the operator. + ``STANDARD`` writes ``left IS [NOT] DISTINCT FROM right``. + ``NULL_SAFE_EQUAL`` uses ``<=>``, which is null-safe equality + (``IS NOT DISTINCT FROM``). ``IS DISTINCT FROM`` is written as ``NOT (left <=> right)``. """ @property def trim(self) -> int: - """``OPT_TRIM_*`` value for how ``TRIM`` is spelled. + """``OPT_OPERATOR_TRIM_*`` value for how ``TRIM`` is spelled. ``SQL_FROM`` writes ``TRIM([LEADING|TRAILING] [chars FROM] expr)``. ``FUNCTION`` writes ``TRIM`` / ``LTRIM`` / ``RTRIM`` ``(expr[, chars])``. @@ -330,20 +394,20 @@ class OperatorConfig: @property def collate(self) -> int: - """``OPT_COLLATE_*`` value for how ``COLLATE`` is spelled. + """``OPT_OPERATOR_COLLATE_*`` value for how ``COLLATE`` is spelled. + ``UNSUPPORTED`` rejects ``COLLATE``. ``INFIX_IDENT`` writes ``expr COLLATE ident``. ``FUNCTION_STRING`` writes ``COLLATE(expr, 'body')``, where the collation name is a string literal. - ``UNSUPPORTED`` rejects ``COLLATE``. """ @final class SchemaConfig: """``CREATE`` / ``DROP SCHEMA`` features, owner clause, and location clause. - ``features`` must include ``OPT_FEATURES_SUPPORTED`` or schema statements - are rejected. ``location`` receives an already quoted string and supplies - its own leading space (``' LOCATION {}'``). ``None`` rejects a location. + ``features`` must include ``OPT_SCHEMA_FEATURES_SUPPORTED`` or schema + statements are rejected. ``location`` receives a value that is already + escaped and includes its own quotes; ``None`` rejects a location. """ def __new__( @@ -356,11 +420,14 @@ class SchemaConfig: """Create a ``SchemaConfig``. Args: - features: ``OPT_FEATURES_*`` flags. Must include ``OPT_FEATURES_SUPPORTED`` - for any schema statement to be accepted. - authorization: ``OPT_AUTHORIZATION_*`` value for the ``AUTHORIZATION`` owner clause. - location: Template for the ``LOCATION`` clause, including its leading space - (e.g. ``' LOCATION {}'``), or ``None`` to reject a location. + features: ``OPT_SCHEMA_FEATURES_*`` flags describing schema support. + Must include ``OPT_SCHEMA_FEATURES_SUPPORTED`` for any schema + statement to be accepted. Defaults to none (``0``). + authorization: ``OPT_SCHEMA_AUTHORIZATION_*`` value for the + ``AUTHORIZATION`` owner clause. Defaults to ``UNSUPPORTED``. + location: Template for the schema location clause with exactly one + ``{}`` placeholder (e.g. ``LOCATION {}``), or ``None`` (default) + to reject a location. The value is escaped and includes its own quotes. """ def __eq__(self, value: object, /) -> bool: @@ -371,15 +438,19 @@ class SchemaConfig: @property def features(self) -> int: - """``OPT_FEATURES_*`` flags describing schema support. - - ``SUPPORTED`` means the dialect supports schemas. Other flags cover ``IF NOT EXISTS``, - embedded DDL in ``CREATE SCHEMA``, whether a schema is a database, and ``CASCADE`` / ``RESTRICT`` on ``DROP SCHEMA``. + """``OPT_SCHEMA_FEATURES_*`` flags for the independent schema features supported. + + ``SUPPORTED``: the dialect supports schemas. + ``IF_NOT_EXISTS``: ``CREATE SCHEMA IF NOT EXISTS`` is supported. + ``EMBEDDED_DDL``: DDL statements can be embedded in ``CREATE SCHEMA``. + ``IS_DATABASE``: a schema has the same meaning as a database in this dialect. + ``DROP_CASCADE``: ``DROP SCHEMA ... CASCADE`` is supported. + ``DROP_RESTRICT``: ``DROP SCHEMA ... RESTRICT`` is supported. """ @property def authorization(self) -> int: - """``OPT_AUTHORIZATION_*`` value for the ``AUTHORIZATION`` owner clause. + """``OPT_SCHEMA_AUTHORIZATION_*`` value: how the schema owner (``AUTHORIZATION``) is specified. ``UNSUPPORTED`` means the clause is not allowed. ``OPTIONAL`` means the clause is allowed but not required. @@ -387,14 +458,17 @@ class SchemaConfig: @property def location(self) -> str | None: - """Template for the ``LOCATION`` clause, including its leading space, or ``None`` if unsupported.""" + """Template for how a schema location is expressed (e.g. ``LOCATION {}``), or ``None`` if unsupported. + + The value is escaped and includes its own quotes. + """ @final class StringConfig: - """String literal template and ``||`` / ``+`` / ``CONCAT`` spelling. + r"""String literal template and ``||`` / ``+`` / ``CONCAT`` spelling. - The template key selects the escape: ``{single}``, ``{double}``, ``{hex}``, - ``{hex_quote}``, ``{mysql}``, ``{unicode}``, or ``{unicode_quote}``. + The template key selects how text is escaped: ``{single}``, ``{double}``, + ``{hex}``, ``{hex_quote}``, ``{mysql}``, ``{unicode}``, or ``{unicode_quote}``. ``concat`` keys are ``{left}`` and ``{right}``. """ @@ -404,12 +478,32 @@ class StringConfig: template: str = "'{single}'", concat: str = "{left} || {right}", ) -> Self: - """Create a ``StringConfig``. + r"""Create a ``StringConfig``. Args: - template: String literal template with one escape key, such as ``'{single}'`` - or ``E'{hex}'``. A string containing NUL is rejected. - concat: Template for concatenation with ``{left}`` and ``{right}`` keys. + template: String literal template with exactly one escape key, such as + ``'{single}'`` (default) or ``E'{hex}'``. A string containing NUL + is rejected. Pick a key that escapes the delimiter used in the + template (``'{single}'`` or ``"{double}"``). + + Escape by doubling (other characters are unchanged): + + - ``single``: ``'`` becomes ``''`` (standard SQL, SQL Server, Oracle, SQLite, ...). + - ``double``: ``"`` becomes ``""``. + + Escape with a backslash (``\`` is always escaped): + + - ``hex``: escapes ``\\``, ``\'``, ``\n``, ``\r``, ``\t``; other C0/DEL as ``\xNN``. ``"`` is unchanged. + - ``hex_quote``: ``hex`` plus ``\"``. Use for ``"`` delimited strings. + - ``mysql``: (MySQL-specific) escapes ``\\``, ``\'``, ``\"``, ``\n``, ``\r`` and ``\Z`` for 0x1A; tab and other controls pass through. + - ``unicode``: like ``hex``, but other C0/DEL as ``\uNNNN``. ``"`` is unchanged. + - ``unicode_quote``: ``unicode`` plus ``\"``. Use for ``"`` delimited strings. + + Only use a backslash key when the target database really reads + backslash escapes inside string literals (e.g. MySQL, BigQuery, + Hive, Spark, or PostgreSQL ``E'...'``). + concat: Template for binary string concatenation with exactly the keys + ``{left}`` and ``{right}``. Defaults to ``{left} || {right}``. """ def __eq__(self, value: object, /) -> bool: @@ -420,18 +514,28 @@ class StringConfig: @property def template(self) -> str: - """String literal template, containing one escape key such as ``{single}`` or ``{hex}``.""" + """String literal template (e.g. ``E'{hex}'``), containing one escape key. + + The key (``{single}``, ``{double}``, ``{hex}``, ``{hex_quote}``, ``{mysql}``, + ``{unicode}``, or ``{unicode_quote}``) picks how the text is escaped. + A string containing NUL is rejected. Prefer ``serialize_string`` over using + the template directly. + """ @property def concat(self) -> str: - """Concatenation template using ``{left}`` and ``{right}`` keys (e.g. ``{left} || {right}``).""" + """Template for binary string concatenation (e.g. ``{left} || {right}``), using the ``{left}`` and ``{right}`` keys.""" + + def serialize_string(self, value: str, /) -> str: + """Escape ``value`` according to ``template`` and wrap it as a complete string literal.""" @final class TemporalConfig: """Date, time, and timestamp literal templates. - ``{}`` is the raw text (``2024-01-15``), so the quotes belong in the - template (``DATE '{}'``, ``DATE E'{}'``). + Each template has exactly one ``{}`` placeholder, which is the raw text + (``2024-01-15``), so the quotes belong in the template (``DATE '{}'``, + ``DATE E'{}'``). All default to ``{}``. """ def __new__( @@ -447,8 +551,10 @@ class TemporalConfig: Args: date: Template for date literals, such as ``DATE '{}'``. time: Template for time literals, such as ``TIME '{}'``. - datetime_naive: Template for timestamps without a timezone, such as ``TIMESTAMP '{}'``. - datetime_aware: Template for timestamps with a timezone, such as ``TIMESTAMPTZ '{}'``. + datetime_naive: Template for naive timestamps (without a timezone), + such as ``TIMESTAMP '{}'``. + datetime_aware: Template for timezone-aware timestamps, such as + ``TIMESTAMPTZ '{}'``. """ def __eq__(self, value: object, /) -> bool: @@ -459,19 +565,19 @@ class TemporalConfig: @property def date(self) -> str: - """Template for date literals. ``{}`` is replaced by the raw date text.""" + """Template for date literals (e.g. ``DATE '{}'``). ``{}`` is replaced by the raw date text.""" @property def time(self) -> str: - """Template for time literals. ``{}`` is replaced by the raw time text.""" + """Template for time literals (e.g. ``TIME '{}'``). ``{}`` is replaced by the raw time text.""" @property def datetime_naive(self) -> str: - """Template for timestamps without a timezone. ``{}`` is replaced by the raw text.""" + """Template for naive timestamps (e.g. ``TIMESTAMP '{}'``). ``{}`` is replaced by the raw text.""" @property def datetime_aware(self) -> str: - """Template for timestamps with a timezone. ``{}`` is replaced by the raw text.""" + """Template for timezone-aware timestamps (e.g. ``TIMESTAMPTZ '{}'``). ``{}`` is replaced by the raw text.""" @final class TupleConfig: @@ -486,8 +592,11 @@ class TupleConfig: """Create a ``TupleConfig``. Args: - syntax: ``OPT_SYNTAX_*`` value selecting how a tuple is written. - arity: ``OPT_ARITY_*`` flags for tuple sizes below 2 that are allowed. + syntax: ``OPT_TUPLE_SYNTAX_*`` value selecting how a tuple is written. + Defaults to ``UNSUPPORTED``. + arity: ``OPT_TUPLE_ARITY_*`` flags for tuple sizes below 2 that are + allowed. Defaults to none (``0``), meaning a tuple needs at least + two elements. """ def __eq__(self, value: object, /) -> bool: @@ -498,25 +607,28 @@ class TupleConfig: @property def syntax(self) -> int: - """``OPT_SYNTAX_*`` value for how a tuple is written. + """``OPT_TUPLE_SYNTAX_*`` value for how a tuple / row value is written. + ``UNSUPPORTED`` rejects tuples. ``ROW`` writes ``ROW(...)``. ``STRUCT`` writes ``struct(...)``. ``PARENTHESES`` writes ``(a, b, ...)``. - ``UNSUPPORTED`` rejects tuples. + + Which arities are legal is controlled by ``arity``, not by another syntax value. """ @property def arity(self) -> int: - """``OPT_ARITY_*`` flags for tuple sizes below 2 that are allowed. + """``OPT_TUPLE_ARITY_*`` flags for tuple sizes below 2 that the syntax accepts. - Sizes 2 and above are always allowed when ``syntax`` is supported. - ``OPT_ARITY_EMPTY`` allows size 0. ``OPT_ARITY_ONE`` allows size 1. + Sizes 2 and above are always allowed when ``syntax`` is not ``UNSUPPORTED``. + Empty means a tuple needs at least two elements. + ``OPT_TUPLE_ARITY_EMPTY`` allows size 0. ``OPT_TUPLE_ARITY_ONE`` allows size 1. """ @final class Dialect: - """Quoting, literal spelling, and which syntax is legal. + r"""Quoting, literal spelling, and which syntax is legal. ``placeholder`` may be a bare marker (``'?'``) or include the 1-based index (``'?{}'``, ``'${}'``, ``':{}'``). ``identifier`` keys are @@ -540,23 +652,48 @@ class Dialect: aggregate: AggregateConfig = ..., schema: SchemaConfig = ..., ) -> Self: - """Create a ``Dialect``. + r"""Create a ``Dialect``. Args: - placeholder: Positional parameter template. Use ``'?'`` for a bare marker, - or include ``{}`` for the 1-based index (e.g. ``'${}'`` gives ``$1``, ``$2``). + placeholder: Positional parameter template with at most one ``{}``, + which is the 1-based index and may be omitted. ``'?'`` writes the + marker alone; ``'${}'`` writes ``$1``, ``$2``, and so on. + Defaults to ``'?{}'``. like: ``LIKE`` / ``ILIKE`` / regex configuration. bitwise: Bitwise operator support and spelling. - identifier: Identifier template such as ``"{double}"`` or ``` `{backtick}` ```. - The key must match the delimiter in the template. - string: String literal and concatenation configuration. + identifier: Identifier template with exactly one key, such as + ``"{double}"`` (default) or ``` `{backtick}` ```. The key must match + the delimiter in the template and picks how the name is escaped. + + Escape by doubling (``x`` becomes ``xx``; other characters are unchanged): + + - ``double``: ``"`` (PostgreSQL, SQLite, Oracle, Snowflake, ...). + - ``single``: ``'``. + - ``bracket``: ``]`` (SQL Server). + - ``backtick``: ``` ` ``` (MySQL, Hive, Spark, Databricks, ...). + + Escape with a backslash (``\`` and the delimiter get a backslash; + ``\n``, ``\r``, ``\t`` are written as escapes; other control + characters as ``\xNN``): + + - ``slashdouble``: ``"``. + - ``slashsingle``: ``'``. + - ``slashbracket``: ``]``. + - ``slashbacktick``: ``` ` ``` (BigQuery). + + Only use a ``slash*`` key when the target database really reads + backslash escapes inside quoted identifiers. + string: String literal format, escaping, and concatenation. cast: Cast syntax configuration (``TRY_CAST``, ``::``). - tuple: Tuple and row-value configuration. - temporal: Date, time, and timestamp literal templates. - literal: Boolean, bit, bytes, network, UUID, JSON, and array literal configuration. + tuple: Tuple / row-value syntax and arity support. + temporal: Date, time, and timestamp literal spelling. + literal: Other literal formats (bool, bit, bytes, inet, MAC address, + UUID, JSON, array). operator: Spelling of modulo, ``IS DISTINCT FROM``, ``TRIM``, and ``COLLATE``. - aggregate: Aggregate ``{IGNORE|RESPECT} NULLS`` and ``FILTER`` configuration. - schema: Schema and DDL feature configuration. + aggregate: Aggregate ``{IGNORE|RESPECT} NULLS`` and ``FILTER`` syntax. + schema: Schema / DDL feature flags and clause styles. + + Every config argument defaults to its class's default-constructed value. """ def __eq__(self, value: object, /) -> bool: @@ -567,7 +704,10 @@ class Dialect: @property def placeholder(self) -> str: - """Positional parameter template, such as ``'?{}'``, ``'?'``, or ``'${}'``.""" + """Positional parameter template, such as ``'?{}'``, ``'?'``, or ``'${}'``. + + ``{}`` is the 1-based index and may be omitted; ``'?'`` writes the marker alone. + """ @property def like(self) -> LikeConfig: @@ -579,11 +719,15 @@ class Dialect: @property def identifier(self) -> str: - """Identifier quoting template, such as ``"{double}"`` or ``` `{backtick}` ```.""" + """Identifier quoting template, such as ``"{double}"`` or ``` `{backtick}` ```. + + The key (``double``, ``single``, ``bracket``, ``backtick``, or a + ``slash*`` variant) picks which character is escaped and how. + """ @property def string(self) -> StringConfig: - """String literal and concatenation configuration.""" + """String literal format, escaping, and concatenation.""" @property def cast(self) -> CastConfig: @@ -591,32 +735,27 @@ class Dialect: @property def tuple(self) -> TupleConfig: - """Tuple and row-value configuration.""" + """Tuple / row-value syntax and arity support.""" @property def temporal(self) -> TemporalConfig: - """Date, time, and timestamp literal templates.""" + """Date, time, and timestamp literal spelling.""" @property def literal(self) -> LiteralConfig: - """Boolean, bit, bytes, network, UUID, JSON, and array literal configuration.""" + """Other literal formats: boolean, bit, bytes, inet, MAC address, UUID, JSON, and array.""" @property def operator(self) -> OperatorConfig: - """Spelling of modulo, ``IS DISTINCT FROM``, ``TRIM``, and ``COLLATE``.""" + """Miscellaneous operator spellings: modulo, ``IS DISTINCT FROM``, ``TRIM``, and ``COLLATE``.""" @property def aggregate(self) -> AggregateConfig: - """Aggregate ``{IGNORE|RESPECT} NULLS`` and ``FILTER`` configuration.""" + """Aggregate ``{IGNORE|RESPECT} NULLS`` and ``FILTER`` syntax.""" @property def schema(self) -> SchemaConfig: - """Schema and DDL feature configuration.""" - -# --------------------------------------------------------------------------- -# Feature flags. Combine with ``|`` and pass to the matching ``*Config``. -# A flag that is not set means the dialect does not support that form. -# --------------------------------------------------------------------------- + """Schema / DDL feature flags and clause styles.""" # AggregateConfig.function_filter: where the aggregate FILTER clause is written. OPT_FUNCTION_FILTER_UNSUPPORTED: Final[int] # Aggregate FILTER is not supported. @@ -629,92 +768,94 @@ OPT_NULL_TREATMENT_UNSUPPORTED: Final[int] # IGNORE/RESPECT NULLS is not suppor OPT_NULL_TREATMENT_INSIDE_PARENS: Final[int] # FIRST_VALUE(x IGNORE NULLS) OPT_NULL_TREATMENT_POSTFIX: Final[int] # FIRST_VALUE(x) IGNORE NULLS -# BitwiseConfig.operations: which bitwise operators are available. -OPT_OPERATIONS_AND: Final[int] # & -OPT_OPERATIONS_OR: Final[int] # | -OPT_OPERATIONS_NOT: Final[int] # ~ -OPT_OPERATIONS_SHL: Final[int] # << -OPT_OPERATIONS_SHR: Final[int] # >> +# BitwiseConfig.operations: flags for which bitwise operators are available. A missing flag rejects the operator. +OPT_BITWISE_OPERATIONS_AND: Final[int] # & +OPT_BITWISE_OPERATIONS_OR: Final[int] # | +OPT_BITWISE_OPERATIONS_NOT: Final[int] # ~ +OPT_BITWISE_OPERATIONS_SHL: Final[int] # << +OPT_BITWISE_OPERATIONS_SHR: Final[int] # >> # BitwiseConfig.syntax: spelling shared by AND, OR, NOT, and the shifts. -OPT_SYNTAX_INFIX: Final[int] # & | ~ << >> -OPT_SYNTAX_BIT_FUNCTION: Final[int] # BITAND / BITOR / BITNOT / BITSHIFTLEFT / BITSHIFTRIGHT -OPT_SYNTAX_SHIFT_FUNCTION: Final[int] # BITAND / BITOR / BITNOT / SHIFTLEFT / SHIFTRIGHT +OPT_BITWISE_SYNTAX_INFIX: Final[int] # & | ~ << >> +OPT_BITWISE_SYNTAX_BIT_FUNCTION: Final[int] # BITAND / BITOR / BITNOT / BITSHIFTLEFT / BITSHIFTRIGHT +OPT_BITWISE_SYNTAX_SHIFT_FUNCTION: Final[int] # BITAND / BITOR / BITNOT / SHIFTLEFT / SHIFTRIGHT # BitwiseConfig.xor: spelling of ^. -OPT_XOR_UNSUPPORTED: Final[int] # XOR is not supported. -OPT_XOR_CARET: Final[int] # left ^ right -OPT_XOR_HASH: Final[int] # left # right -OPT_XOR_FUNCTION: Final[int] # xor(left, right) -OPT_XOR_BIT_FUNCTION: Final[int] # BITXOR(left, right) +OPT_BITWISE_XOR_UNSUPPORTED: Final[int] # XOR is not supported. +OPT_BITWISE_XOR_CARET: Final[int] # left ^ right +OPT_BITWISE_XOR_HASH: Final[int] # left # right (PostgreSQL's ^ is exponent) +OPT_BITWISE_XOR_FUNCTION: Final[int] # xor(left, right) +OPT_BITWISE_XOR_BIT_FUNCTION: Final[int] # BITXOR(left, right) -# LikeConfig.escape: where an ESCAPE clause is allowed. -OPT_ESCAPE_SCALAR: Final[int] # Scalar LIKE ... ESCAPE, including OR expansion. -OPT_ESCAPE_ANY: Final[int] # Native LIKE ANY (...) ESCAPE. +# LikeConfig.escape: flags for where an ESCAPE clause is allowed. Empty rejects any escape character. +OPT_LIKE_ESCAPE_SCALAR: Final[int] # Scalar LIKE ... ESCAPE, including OR expansion. +OPT_LIKE_ESCAPE_ANY: Final[int] # Native LIKE ANY (p, ...) ESCAPE. # LikeConfig.any: rendering of LIKE ANY / ILIKE ANY. -OPT_ANY_OR_EXPANSION: Final[int] # (x LIKE p1 OR x LIKE p2) -OPT_ANY_ARRAY_CONSTRUCTOR: Final[int] # LIKE ANY (ARRAY[p, ...]) -OPT_ANY_PATTERN_LIST: Final[int] # LIKE ANY (p, ...) +OPT_LIKE_ANY_OR_EXPANSION: Final[int] # (x LIKE p1 OR x LIKE p2); for dialects without native support. +OPT_LIKE_ANY_ARRAY_CONSTRUCTOR: Final[int] # LIKE ANY (ARRAY[p, ...]) +OPT_LIKE_ANY_PATTERN_LIST: Final[int] # LIKE ANY (p, ...) # LikeConfig.ilike: rendering of ILIKE. -OPT_ILIKE_UNSUPPORTED: Final[int] # ILIKE is not supported. -OPT_ILIKE_STANDARD: Final[int] # Native ILIKE. -OPT_ILIKE_USE_LIKE_LOWER: Final[int] # LIKE with LOWER() on both sides. +OPT_I_LIKE_STRATEGY_UNSUPPORTED: Final[int] # ILIKE is not supported. +OPT_I_LIKE_STRATEGY_STANDARD: Final[int] # Native ILIKE. +OPT_I_LIKE_STRATEGY_USE_LIKE_LOWER: Final[int] # LIKE with LOWER() on both sides. # LikeConfig.regex: spelling of regex matching. -OPT_REGEX_UNSUPPORTED: Final[int] # Regex matching is not supported. -OPT_REGEX_STANDARD: Final[int] # expr [NOT] RLIKE pattern -OPT_REGEX_TILDE: Final[int] # expr ~ pattern / expr !~ pattern -OPT_REGEX_REGEXP_LIKE: Final[int] # REGEXP_LIKE(expr, pattern) -OPT_REGEX_REGEXP_CONTAINS: Final[int] # REGEXP_CONTAINS(expr, pattern) -OPT_REGEX_REGEXP_MATCHES: Final[int] # regexp_matches(expr, pattern) +OPT_LIKE_REGEX_UNSUPPORTED: Final[int] # Regex matching is not supported. +OPT_LIKE_REGEX_STANDARD: Final[int] # expr [NOT] RLIKE pattern +OPT_LIKE_REGEX_TILDE: Final[int] # expr ~ pattern / expr !~ pattern +OPT_LIKE_REGEX_REGEXP_LIKE: Final[int] # REGEXP_LIKE(expr, pattern) +OPT_LIKE_REGEX_REGEXP_CONTAINS: Final[int] # REGEXP_CONTAINS(expr, pattern) +OPT_LIKE_REGEX_REGEXP_MATCHES: Final[int] # regexp_matches(expr, pattern) # OperatorConfig.is_distinct_from: spelling of IS [NOT] DISTINCT FROM. OPT_IS_DISTINCT_FROM_UNSUPPORTED: Final[int] # Not supported. OPT_IS_DISTINCT_FROM_STANDARD: Final[int] # left IS [NOT] DISTINCT FROM right -OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL: Final[int] # Uses <=>, where IS DISTINCT FROM is NOT (left <=> right). +OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL: Final[ + int +] # <=> is IS NOT DISTINCT FROM; IS DISTINCT FROM is NOT (left <=> right). # OperatorConfig.modulo: spelling of %. -OPT_MODULO_PERCENT: Final[int] # left % right -OPT_MODULO_FUNCTION: Final[int] # MOD(left, right) -OPT_MODULO_INFIX: Final[int] # left MOD right +OPT_OPERATOR_MODULO_PERCENT: Final[int] # left % right +OPT_OPERATOR_MODULO_FUNCTION: Final[int] # MOD(left, right) +OPT_OPERATOR_MODULO_INFIX: Final[int] # left MOD right # OperatorConfig.trim: spelling of TRIM. -OPT_TRIM_SQL_FROM: Final[int] # TRIM([LEADING|TRAILING] [chars FROM] expr) -OPT_TRIM_FUNCTION: Final[int] # TRIM / LTRIM / RTRIM (expr[, chars]) -OPT_TRIM_SPACES_ONLY: Final[int] # TRIM / LTRIM / RTRIM (expr); a character set raises. +OPT_OPERATOR_TRIM_SQL_FROM: Final[int] # TRIM([LEADING|TRAILING] [chars FROM] expr) +OPT_OPERATOR_TRIM_FUNCTION: Final[int] # TRIM / LTRIM / RTRIM (expr[, chars]) +OPT_OPERATOR_TRIM_SPACES_ONLY: Final[int] # TRIM / LTRIM / RTRIM (expr); a character set raises. # OperatorConfig.collate: spelling of COLLATE. -OPT_COLLATE_UNSUPPORTED: Final[int] # Not supported. -OPT_COLLATE_INFIX_IDENT: Final[int] # expr COLLATE ident -OPT_COLLATE_FUNCTION_STRING: Final[int] # COLLATE(expr, 'body') +OPT_OPERATOR_COLLATE_UNSUPPORTED: Final[int] # Not supported. +OPT_OPERATOR_COLLATE_INFIX_IDENT: Final[int] # expr COLLATE ident +OPT_OPERATOR_COLLATE_FUNCTION_STRING: Final[int] # COLLATE(expr, 'body'); the body is a string literal. -# TupleConfig.arity: tuple sizes below 2 that are allowed. -OPT_ARITY_EMPTY: Final[int] # Size 0. -OPT_ARITY_ONE: Final[int] # Size 1. +# TupleConfig.arity: flags for tuple sizes below 2 that are allowed (size 2+ is always allowed if syntax is supported). +OPT_TUPLE_ARITY_EMPTY: Final[int] # Size 0. +OPT_TUPLE_ARITY_ONE: Final[int] # Size 1. # TupleConfig.syntax: how a tuple is written. -OPT_SYNTAX_ROW: Final[int] # ROW(...) -OPT_SYNTAX_STRUCT: Final[int] # struct(...) -OPT_SYNTAX_PARENTHESES: Final[int] # (a, b, ...) -OPT_SYNTAX_UNSUPPORTED: Final[int] # Tuples are not supported. +OPT_TUPLE_SYNTAX_ROW: Final[int] # ROW(...) +OPT_TUPLE_SYNTAX_STRUCT: Final[int] # struct(...) +OPT_TUPLE_SYNTAX_PARENTHESES: Final[int] # (a, b, ...) +OPT_TUPLE_SYNTAX_UNSUPPORTED: Final[int] # Tuples are not supported. # LiteralConfig.boolean: spelling of boolean literals. -OPT_BOOLEAN_KEYWORD: Final[int] # TRUE / FALSE -OPT_BOOLEAN_INTEGER: Final[int] # 1 / 0 +OPT_BOOL_FORMAT_KEYWORD: Final[int] # TRUE / FALSE +OPT_BOOL_FORMAT_INTEGER: Final[int] # 1 / 0 -# SchemaConfig.features: schema statement support. -OPT_FEATURES_SUPPORTED: Final[int] # Schemas are supported. Required for any schema statement. -OPT_FEATURES_IF_NOT_EXISTS: Final[int] # CREATE SCHEMA IF NOT EXISTS -OPT_FEATURES_EMBEDDED_DDL: Final[int] # DDL statements embedded in CREATE SCHEMA -OPT_FEATURES_IS_DATABASE: Final[int] # A schema means the same thing as a database. -OPT_FEATURES_DROP_CASCADE: Final[int] # DROP SCHEMA ... CASCADE -OPT_FEATURES_DROP_RESTRICT: Final[int] # DROP SCHEMA ... RESTRICT +# SchemaConfig.features: independent schema feature flags. +OPT_SCHEMA_FEATURES_SUPPORTED: Final[int] # Schemas are supported. Required for any schema statement. +OPT_SCHEMA_FEATURES_IF_NOT_EXISTS: Final[int] # CREATE SCHEMA IF NOT EXISTS +OPT_SCHEMA_FEATURES_EMBEDDED_DDL: Final[int] # DDL statements embedded in CREATE SCHEMA +OPT_SCHEMA_FEATURES_IS_DATABASE: Final[int] # A schema means the same thing as a database. +OPT_SCHEMA_FEATURES_DROP_CASCADE: Final[int] # DROP SCHEMA ... CASCADE +OPT_SCHEMA_FEATURES_DROP_RESTRICT: Final[int] # DROP SCHEMA ... RESTRICT # SchemaConfig.authorization: AUTHORIZATION owner clause. -OPT_AUTHORIZATION_UNSUPPORTED: Final[int] # Not allowed. -OPT_AUTHORIZATION_OPTIONAL: Final[int] # Allowed, but not required. +OPT_SCHEMA_AUTHORIZATION_UNSUPPORTED: Final[int] # Not allowed. +OPT_SCHEMA_AUTHORIZATION_OPTIONAL: Final[int] # Allowed, but not required. BigQuery: Final[Dialect] Databricks: Final[Dialect] diff --git a/scripts/check_dialect_stub.sh b/scripts/check_dialect_stub.sh deleted file mode 100644 index 40844cd..0000000 --- a/scripts/check_dialect_stub.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -rs="src/dialect/base.rs" -pyi="rapidquery/dialect.pyi" -missing=0 - -miss() { - echo "MISSING in stub: $1" - missing=1 -} - -# 1. Python classes: pyclass("rapidquery.dialect.Name") -grep -oE 'pyclass\("rapidquery\.dialect\.[A-Za-z]+"' "$rs" | sed 's/.*\.//; s/"//' | -while read -r c; do grep -q "^class $c\b" "$pyi" || miss "class $c"; done - -# 2. Constructor fields: "name:" lines inside pyo3_constructor! { ... } -awk '/pyo3_constructor!/{on=1} on&&/^}/{on=0} on' "$rs" | -grep -oE '^\s+[a-z_]+:' | tr -d ' :' | sort -u | -while read -r f; do grep -q "def $f(" "$pyi" || miss "field $f"; done - -# 3. Flags: "field: bits|enum Type { A, B }" -> OPT__ -tr '\n' ' ' < "$rs" | -grep -oE '[a-z_]+: (bits|enum) [A-Za-z]+ \{[^}]*\}' | -while read -r line; do - field=$(echo "${line%%:*}" | tr a-z A-Z) - variants=$(echo "$line" | sed 's/.*{//; s/}//; s/,/ /g') - for v in $variants; do - snake=$(echo "$v" | sed 's/\([a-z0-9]\)\([A-Z]\)/\1_\2/g' | tr a-z A-Z) - grep -q "^OPT_${field}_${snake}:" "$pyi" || miss "OPT_${field}_${snake}" - done -done - -exit $missing diff --git a/src/ddl/schema.rs b/src/ddl/schema.rs index 831f083..ba7f9d1 100644 --- a/src/ddl/schema.rs +++ b/src/ddl/schema.rs @@ -47,7 +47,7 @@ impl sql::WriteSql for PyCreateSchema { // ... [] [AUTHORIZATION ] match dialect.schema.authorization { - dialect::SchemaAuthorizationStyle::Unsupported => { + dialect::SchemaAuthorization::Unsupported => { if self.authorization.is_some() { return Err(rapidquery_macros::pyerr!( PyRuntimeError, @@ -57,7 +57,7 @@ impl sql::WriteSql for PyCreateSchema { self.name.write_sql(dialect, out)?; } - dialect::SchemaAuthorizationStyle::Optional => { + dialect::SchemaAuthorization::Optional => { self.name.write_sql(dialect, out)?; if let Some(x) = &self.authorization { @@ -76,6 +76,7 @@ impl sql::WriteSql for PyCreateSchema { )); }; + out.write(' '); tmpl.write_to_result(out, |_, out| dialect.string.write_string_literal(out, x.as_str()))?; } diff --git a/src/dialect/base.rs b/src/dialect/base.rs index 2c581e8..0c47245 100644 --- a/src/dialect/base.rs +++ b/src/dialect/base.rs @@ -24,9 +24,9 @@ pub struct LikeConfig { /// Does the dialect support `ILIKE`, or should we fall back? /// + /// - Unsupported /// - Standard: Native `ILIKE`. /// - UseLikeLower: Use `LIKE` with `LOWER()`. - /// - UseLike: Fall back to case-sensitive `LIKE`. #[define(enum { #[default] Unsupported, Standard, UseLikeLower })] pub ilike: ILikeStrategy, @@ -39,7 +39,7 @@ pub struct LikeConfig { /// - RegexpContains: `REGEXP_CONTAINS(expr, pattern)`. /// - RegexpMatches: `regexp_matches(expr, pattern)`. #[define(enum { #[default] Unsupported, Standard, Tilde, RegexpLike, RegexpContains, RegexpMatches })] - pub regex: RegexSpelling, + pub regex: LikeRegex, /// Whether the dialect supports the `SIMILAR TO` predicate. pub supports_similar_to: bool, @@ -50,7 +50,7 @@ rapidquery_macros::pyo3_constructor! { escape: bits LikeEscape { SCALAR, ANY } = empty, any: enum LikeAny { OrExpansion, ArrayConstructor, PatternList } = OrExpansion, ilike: enum ILikeStrategy { Unsupported, Standard, UseLikeLower } = Unsupported, - regex: enum RegexSpelling { + regex: enum LikeRegex { Unsupported, Standard, Tilde, RegexpLike, RegexpContains, RegexpMatches } = Unsupported, supports_similar_to: bool = false, @@ -84,14 +84,14 @@ pub struct BitwiseConfig { /// - Function: `xor(left, right)`. /// - BitFunction: `BITXOR(left, right)`. #[define(enum { #[default] Unsupported, Caret, Hash, Function, BitFunction })] - pub xor: BitwiseXorSpelling, + pub xor: BitwiseXor, } rapidquery_macros::pyo3_constructor! { BitwiseConfig => export_bitwise_config_options { operations: bits BitwiseOperations { AND, OR, NOT, SHL, SHR } = empty, syntax: enum BitwiseSyntax { Infix, BitFunction, ShiftFunction } = Infix, - xor: enum BitwiseXorSpelling { Unsupported, Caret, Hash, Function, BitFunction } = Unsupported, + xor: enum BitwiseXor { Unsupported, Caret, Hash, Function, BitFunction } = Unsupported, } } @@ -224,7 +224,7 @@ pub struct LiteralConfig { /// The way dialect accepts literal bit values e.g. `B'{}'` /// - /// Prefer using `StringConfig::write_bytes` instead of directly using this template. + /// Prefer using `StringConfig::write_bits` instead of directly using this template. pub bit: Option, /// The way dialect accepts literal bytes values e.g. `X'{hex}'` @@ -275,7 +275,7 @@ pub struct OperatorConfig { /// - Function: `MOD(left, right)`. /// - Infix: `left MOD right`. #[define(enum { #[default] Percent, Function, Infix })] - pub modulo: ModuloSpelling, + pub modulo: OperatorModulo, /// How `IS [NOT] DISTINCT FROM` is spelled. /// @@ -283,7 +283,7 @@ pub struct OperatorConfig { /// - Standard: `left IS [NOT] DISTINCT FROM right`. /// - NullSafeEqual: `<=>` is null-safe equal (`IS NOT DISTINCT FROM`). `IS DISTINCT FROM` is `NOT (left <=> right)`. #[define(enum { #[default] Unsupported, Standard, NullSafeEqual })] - pub is_distinct_from: IsDistinctFromSpelling, + pub is_distinct_from: IsDistinctFrom, /// How `TRIM` is spelled. /// @@ -291,7 +291,7 @@ pub struct OperatorConfig { /// - Function: `TRIM` / `LTRIM` / `RTRIM` `(expr[, chars])`. /// - SpacesOnly: `TRIM` / `LTRIM` / `RTRIM` `(expr)`. A character set raises. #[define(enum { #[default] SqlFrom, Function, SpacesOnly })] - pub trim: TrimSpelling, + pub trim: OperatorTrim, /// How `COLLATE` is spelled. /// @@ -299,15 +299,15 @@ pub struct OperatorConfig { /// - InfixIdent: `expr COLLATE ident`. /// - FunctionString: `COLLATE(expr, 'body')`. The identifier body is a string literal. #[define(enum { #[default] Unsupported, InfixIdent, FunctionString })] - pub collate: CollateSpelling, + pub collate: OperatorCollate, } rapidquery_macros::pyo3_constructor! { OperatorConfig => export_operator_config_options { - modulo: enum ModuloSpelling { Percent, Function, Infix } = Percent, - is_distinct_from: enum IsDistinctFromSpelling { Unsupported, Standard, NullSafeEqual } = Unsupported, - trim: enum TrimSpelling { SqlFrom, Function, SpacesOnly } = SqlFrom, - collate: enum CollateSpelling { Unsupported, InfixIdent, FunctionString } = Unsupported, + modulo: enum OperatorModulo { Percent, Function, Infix } = Percent, + is_distinct_from: enum IsDistinctFrom { Unsupported, Standard, NullSafeEqual } = Unsupported, + trim: enum OperatorTrim { SqlFrom, Function, SpacesOnly } = SqlFrom, + collate: enum OperatorCollate { Unsupported, InfixIdent, FunctionString } = Unsupported, } } @@ -331,7 +331,7 @@ pub struct SchemaConfig { /// - Unsupported: Means dialect does not support `AUTHORIZATION` for schema. /// - Optional: Means `AUTHORIZATION` clause is supported and it's optional. #[define(enum { #[default] Unsupported, Optional })] - pub authorization: SchemaAuthorizationStyle, + pub authorization: SchemaAuthorization, /// How schema location is expressed e.g. `LOCATION {}` (value is escaped and includes quotes itself). pub location: Option, @@ -342,7 +342,7 @@ rapidquery_macros::pyo3_constructor! { features: bits SchemaFeatures { SUPPORTED, IF_NOT_EXISTS, EMBEDDED_DDL, IS_DATABASE, DROP_CASCADE, DROP_RESTRICT } = empty, - authorization: enum SchemaAuthorizationStyle { Unsupported, Optional } = Unsupported, + authorization: enum SchemaAuthorization { Unsupported, Optional } = Unsupported, location: Option = None, } } diff --git a/src/dialect/escape.rs b/src/dialect/escape.rs index 33fc418..93e92bc 100644 --- a/src/dialect/escape.rs +++ b/src/dialect/escape.rs @@ -30,6 +30,7 @@ impl base::LiteralConfig { /// that is checked with `debug_assert!`. #[inline] pub fn write_bits(&self, writer: &mut W, bits: &str) -> Result<(), errors::FormatError> { + // FIXME: return an error for invalid bits debug_assert!( bits.chars().all(|c| c == '0' || c == '1'), "write_bits: invalid input: {bits}" @@ -59,6 +60,40 @@ impl base::LiteralConfig { } } +#[pyo3::pymethods] +impl base::LiteralConfig { + #[pyo3(signature = (value, /))] + fn serialize_boolean(&self, value: bool) -> String { + let mut buf = String::new(); + self.write_boolean(&mut buf, value); + buf + } + + #[pyo3(signature = (value, /))] + fn serialize_bits(&self, value: &str) -> pyo3::PyResult { + if value.is_empty() || !value.bytes().all(|b| b == b'0' || b == b'1') { + return rapidquery_macros::pyerr!( + Err(PyValueError), + "expected a string of '0'/'1' for bit serialization, got {:?}", + value + ); + } + + let mut buf = String::new(); + self.write_bits(&mut buf, value)?; + Ok(buf) + } + + #[pyo3(signature = (value, /))] + fn serialize_bytes(&self, value: pyo3::Bound<'_, pyo3::types::PyBytes>) -> String { + use pyo3::types::PyBytesMethods; + + let mut buf = String::new(); + self.write_bytes(&mut buf, value.as_bytes()); + buf + } +} + impl base::StringConfig { /// Escape `raw` and wrap it as a complete string literal. #[inline] @@ -67,6 +102,7 @@ impl base::StringConfig { writer: &mut W, raw: &str, ) -> Result<(), errors::FormatError> { + // FIXME: we can accept NUL on some dialects if raw.contains('\0') { return Err(errors::FormatError::NulInString); } @@ -95,6 +131,16 @@ impl base::StringConfig { } } +#[pyo3::pymethods] +impl base::StringConfig { + #[pyo3(signature = (value, /))] + fn serialize_string(&self, value: &str) -> pyo3::PyResult { + let mut buf = String::new(); + self.write_string_literal(&mut buf, value)?; + Ok(buf) + } +} + impl base::PyDialect { /// Escape `ident` and wrap it in this dialect's identifier delimiters. #[inline] diff --git a/src/expr/enum_/like.rs b/src/expr/enum_/like.rs index 68b55f6..2811e69 100644 --- a/src/expr/enum_/like.rs +++ b/src/expr/enum_/like.rs @@ -392,11 +392,11 @@ fn write_regex_call( impl WriteSql for RLike { fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match dialect.like.regex { - dialect::RegexSpelling::Unsupported => Err(rapidquery_macros::pyerr!( + dialect::LikeRegex::Unsupported => Err(rapidquery_macros::pyerr!( PyRuntimeError, "RLIKE is not supported by this dialect", )), - dialect::RegexSpelling::Standard => { + dialect::LikeRegex::Standard => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { out.write_str(" NOT RLIKE "); @@ -405,18 +405,18 @@ impl WriteSql for RLike { } self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) } - dialect::RegexSpelling::Tilde => { + dialect::LikeRegex::Tilde => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; out.write_str(if self.negated { " !~ " } else { " ~ " }); self.pattern.write_operand(dialect, out, Self::PRECEDENCE, true) } - dialect::RegexSpelling::RegexpLike => { + dialect::LikeRegex::RegexpLike => { write_regex_call(dialect, out, "REGEXP_LIKE", self.negated, &self.expr, &self.pattern) } - dialect::RegexSpelling::RegexpContains => { + dialect::LikeRegex::RegexpContains => { write_regex_call(dialect, out, "REGEXP_CONTAINS", self.negated, &self.expr, &self.pattern) } - dialect::RegexSpelling::RegexpMatches => { + dialect::LikeRegex::RegexpMatches => { write_regex_call(dialect, out, "regexp_matches", self.negated, &self.expr, &self.pattern) } } diff --git a/src/expr/enum_/nodes.rs b/src/expr/enum_/nodes.rs index 3aa8a8e..0b33353 100644 --- a/src/expr/enum_/nodes.rs +++ b/src/expr/enum_/nodes.rs @@ -307,22 +307,16 @@ impl WriteSql for Binary { Ok(()) } BinaryOp::BitXor => match dialect.bitwise.xor { - dialect::BitwiseXorSpelling::Unsupported => { - Err(unsupported("bitwise XOR is not supported by this dialect")) - } - dialect::BitwiseXorSpelling::Caret => write_infix(self, dialect, out, "^", false), - dialect::BitwiseXorSpelling::Hash => write_infix(self, dialect, out, "#", false), - dialect::BitwiseXorSpelling::Function => { - write_binary_call(dialect, out, "xor", &self.left, &self.right) - } - dialect::BitwiseXorSpelling::BitFunction => { - write_binary_call(dialect, out, "BITXOR", &self.left, &self.right) - } + dialect::BitwiseXor::Unsupported => Err(unsupported("bitwise XOR is not supported by this dialect")), + dialect::BitwiseXor::Caret => write_infix(self, dialect, out, "^", false), + dialect::BitwiseXor::Hash => write_infix(self, dialect, out, "#", false), + dialect::BitwiseXor::Function => write_binary_call(dialect, out, "xor", &self.left, &self.right), + dialect::BitwiseXor::BitFunction => write_binary_call(dialect, out, "BITXOR", &self.left, &self.right), }, BinaryOp::Mod => match dialect.operator.modulo { - dialect::ModuloSpelling::Percent => write_infix(self, dialect, out, "%", false), - dialect::ModuloSpelling::Function => write_binary_call(dialect, out, "MOD", &self.left, &self.right), - dialect::ModuloSpelling::Infix => write_infix(self, dialect, out, "MOD", false), + dialect::OperatorModulo::Percent => write_infix(self, dialect, out, "%", false), + dialect::OperatorModulo::Function => write_binary_call(dialect, out, "MOD", &self.left, &self.right), + dialect::OperatorModulo::Infix => write_infix(self, dialect, out, "MOD", false), }, BinaryOp::BitAnd => write_bit_op( self, @@ -465,10 +459,10 @@ impl IsDistinctFrom { impl WriteSql for IsDistinctFrom { fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match dialect.operator.is_distinct_from { - dialect::IsDistinctFromSpelling::Unsupported => { + dialect::IsDistinctFrom::Unsupported => { Err(unsupported("IS DISTINCT FROM is not supported by this dialect")) } - dialect::IsDistinctFromSpelling::Standard => { + dialect::IsDistinctFrom::Standard => { self.left.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { out.write_str(" IS NOT DISTINCT FROM "); @@ -477,7 +471,7 @@ impl WriteSql for IsDistinctFrom { } self.right.write_operand(dialect, out, Self::PRECEDENCE, true) } - dialect::IsDistinctFromSpelling::NullSafeEqual => { + dialect::IsDistinctFrom::NullSafeEqual => { // `<=>` is null-safe equal, i.e. `IS NOT DISTINCT FROM`. if !self.negated { out.write_str("NOT ("); @@ -789,7 +783,7 @@ fn write_trim_function(trim: &Trim, dialect: &dialect::PyDialect, impl WriteSql for Trim { fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match dialect.operator.trim { - dialect::TrimSpelling::SqlFrom => { + dialect::OperatorTrim::SqlFrom => { out.write_str("TRIM("); match (&self.where_, &self.characters) { (TrimWhere::Both, None) => self.expr.write_sql(dialect, out)?, @@ -814,8 +808,8 @@ impl WriteSql for Trim { out.write(')'); Ok(()) } - dialect::TrimSpelling::Function => write_trim_function(self, dialect, out), - dialect::TrimSpelling::SpacesOnly => { + dialect::OperatorTrim::Function => write_trim_function(self, dialect, out), + dialect::OperatorTrim::SpacesOnly => { if self.characters.is_some() { return Err(unsupported("trim characters are not supported by this dialect")); } @@ -864,13 +858,13 @@ impl Collate { impl WriteSql for Collate { fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { match dialect.operator.collate { - dialect::CollateSpelling::Unsupported => Err(unsupported("COLLATE is not supported by this dialect")), - dialect::CollateSpelling::InfixIdent => { + dialect::OperatorCollate::Unsupported => Err(unsupported("COLLATE is not supported by this dialect")), + dialect::OperatorCollate::InfixIdent => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; out.write_str(" COLLATE "); self.collation.write_sql(dialect, out) } - dialect::CollateSpelling::FunctionString => { + dialect::OperatorCollate::FunctionString => { let body = match self.collation.parts() { [super::IdentPart::Quote(body) | super::IdentPart::Unquote(body)] if !body.is_empty() => { body.as_str() diff --git a/src/serializers/functions.rs b/src/serializers/functions.rs index 4dd94c0..ac6f624 100644 --- a/src/serializers/functions.rs +++ b/src/serializers/functions.rs @@ -272,7 +272,7 @@ pub(super) unsafe fn write_time( let val: pyo3::Bound<'_, pyo3::types::PyTime> = pyo3::Bound::from_borrowed_ptr(py, ptr).cast_into()?; let time: chrono::NaiveTime = val.extract()?; - cfg.date + cfg.time .write_to(out, |_, out| out.write_str(&time.format("%H:%M:%S%.f").to_string())); Ok(()) } else { @@ -385,8 +385,8 @@ pub(super) unsafe fn write_macaddr( cfg: &dialect::LiteralConfig, out: &mut W, ) -> pyo3::PyResult<()> { - let Some(tmpl) = &cfg.inet else { - return Err(dialect::errors::FormatError::Unsupported("InetSerializer").into()); + let Some(tmpl) = &cfg.macaddr else { + return Err(dialect::errors::FormatError::Unsupported("MacAddressSerializer").into()); }; let text = pyobject_string(py, ptr)?; diff --git a/tests/test_ddl_schema.py b/tests/test_ddl_schema.py index 1206ac0..e176118 100644 --- a/tests/test_ddl_schema.py +++ b/tests/test_ddl_schema.py @@ -30,7 +30,11 @@ def test_supported(self): def test_is_database(self): stmt = rq.ddl.CreateSchema("name") - dl = rq.dialect.Dialect(schema_features=rq.dialect.OPT_SCHEMA_FEATURES_IS_DATABASE) + dl = rq.dialect.Dialect( + schema=rq.dialect.SchemaConfig( + features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED | rq.dialect.OPT_SCHEMA_FEATURES_IS_DATABASE + ) + ) assert stmt.to_sql(dl) == 'CREATE DATABASE "name"' def test_if_not_exists(self): @@ -40,19 +44,30 @@ def test_if_not_exists(self): assert stmt.to_sql(rq.dialect.PostgreSql) == 'CREATE SCHEMA IF NOT EXISTS "name"' # Should ignore IF NOT EXISTS when a dialect doesn't support it - assert stmt.to_sql(rq.dialect.Dialect()) == 'CREATE SCHEMA "name"' + dl = rq.dialect.Dialect(schema=rq.dialect.SchemaConfig(features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED)) + assert stmt.to_sql(dl) == 'CREATE SCHEMA "name"' def test_authorization(self): stmt = rq.ddl.CreateSchema("name", authorization="user") assert stmt.authorization == "user" # Unsupported - dl = rq.dialect.Dialect(schema_authorization_style=rq.dialect.OPT_SCHEMA_AUTHORIZATION_STYLE_UNSUPPORTED) + dl = rq.dialect.Dialect( + schema=rq.dialect.SchemaConfig( + features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED, + authorization=rq.dialect.OPT_SCHEMA_AUTHORIZATION_UNSUPPORTED, + ) + ) with pytest.raises(RuntimeError, match="supported"): _ = stmt.to_sql(dl) # Optional - dl = rq.dialect.Dialect(schema_authorization_style=rq.dialect.OPT_SCHEMA_AUTHORIZATION_STYLE_OPTIONAL) + dl = rq.dialect.Dialect( + schema=rq.dialect.SchemaConfig( + features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED, + authorization=rq.dialect.OPT_SCHEMA_AUTHORIZATION_OPTIONAL, + ) + ) assert stmt.to_sql(dl) == 'CREATE SCHEMA "name" AUTHORIZATION "user"' stmt = rq.ddl.CreateSchema("name", authorization=None) @@ -63,31 +78,23 @@ def test_location(self): assert stmt.location == "s3://depts/finance" # Unsupported - dl = rq.dialect.Dialect(schema_location_style=rq.dialect.OPT_SCHEMA_LOCATION_STYLE_UNSUPPORTED) + dl = rq.dialect.Dialect( + schema=rq.dialect.SchemaConfig( + features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED, + location=None, + ) + ) with pytest.raises(RuntimeError, match="supported"): _ = stmt.to_sql(dl) - # Location Keyword dl = rq.dialect.Dialect( - string_format=rq.dialect.OPT_STRING_FORMAT_SINGLE_QUOTE, - schema_location_style=rq.dialect.OPT_SCHEMA_LOCATION_STYLE_LOCATION_KEYWORD, + schema=rq.dialect.SchemaConfig( + features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED, + location="LOCATION {}", + ) ) assert stmt.to_sql(dl) == "CREATE SCHEMA \"name\" LOCATION 's3://depts/finance'" - # ManagedLocation Keyword - dl = rq.dialect.Dialect( - string_format=rq.dialect.OPT_STRING_FORMAT_SINGLE_QUOTE, - schema_location_style=rq.dialect.OPT_SCHEMA_LOCATION_STYLE_MANAGED_LOCATION_KEYWORD, - ) - assert stmt.to_sql(dl) == "CREATE SCHEMA \"name\" MANAGED LOCATION 's3://depts/finance'" - - # External Volume - dl = rq.dialect.Dialect( - string_format=rq.dialect.OPT_STRING_FORMAT_SINGLE_QUOTE, - schema_location_style=rq.dialect.OPT_SCHEMA_LOCATION_STYLE_EXTERNAL_VOLUME, - ) - assert stmt.to_sql(dl) == "CREATE SCHEMA \"name\" EXTERNAL_VOLUME = 's3://depts/finance'" - class TestDropSchema: def test_new(self): @@ -126,7 +133,8 @@ def test_if_exists(self): assert stmt.to_sql(rq.dialect.PostgreSql) == 'DROP SCHEMA IF EXISTS "name"' # Should ignore IF EXISTS when a dialect doesn't support it - assert stmt.to_sql(rq.dialect.Dialect()) == 'DROP SCHEMA "name"' + dl = rq.dialect.Dialect(schema=rq.dialect.SchemaConfig(features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED)) + assert stmt.to_sql(dl) == 'DROP SCHEMA "name"' def test_behavior(self): stmt = rq.ddl.DropSchema("name", behavior="CASCADE") @@ -138,7 +146,11 @@ def test_behavior(self): _ = stmt.to_sql(dl) # CASCADE Supported - dl = rq.dialect.Dialect(schema_features=rq.dialect.OPT_SCHEMA_FEATURES_DROP_CASCADE) + dl = rq.dialect.Dialect( + schema=rq.dialect.SchemaConfig( + features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED | rq.dialect.OPT_SCHEMA_FEATURES_DROP_CASCADE, + ) + ) assert stmt.to_sql(dl) == 'DROP SCHEMA "name" CASCADE' # CASCADE Unsupported @@ -150,7 +162,11 @@ def test_behavior(self): stmt = rq.ddl.DropSchema("name", behavior="RESTRICT") assert stmt.behavior == "RESTRICT" - dl = rq.dialect.Dialect(schema_features=rq.dialect.OPT_SCHEMA_FEATURES_DROP_RESTRICT) + dl = rq.dialect.Dialect( + schema=rq.dialect.SchemaConfig( + features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED | rq.dialect.OPT_SCHEMA_FEATURES_DROP_RESTRICT, + ) + ) assert stmt.to_sql(dl) == 'DROP SCHEMA "name" RESTRICT' # RESTRICT Unsupported diff --git a/tests/test_dialect.py b/tests/test_dialect.py index 29ebd41..785222b 100644 --- a/tests/test_dialect.py +++ b/tests/test_dialect.py @@ -1,91 +1,76 @@ import pytest -from rapidquery.dialect import ( - OPT_BIT_FORMAT_POSTFIX_BIG_B, - OPT_BITWISE_CALL_SYNTAX_INFIX, - OPT_BITWISE_OPERATIONS_AND, - OPT_BITWISE_OPERATIONS_NOT, - OPT_BITWISE_OPERATIONS_OR, - OPT_BITWISE_OPERATIONS_SHL, - OPT_BITWISE_OPERATIONS_SHR, - OPT_BITWISE_XOR_SPELLING_CARET, - OPT_BOOL_FORMAT_INTEGER, - OPT_BYTES_FORMAT_SLASH_X_BYTEA, - OPT_IDENTIFIER_ESCAPE_BACKSLASH, - OPT_IDENTIFIER_FORMAT_BACKTICK, - OPT_LIKE_ESCAPE_ANY, - OPT_LIKE_ESCAPE_SCALAR, - OPT_PLACEHOLDER_SYNTAX_NUMBERED, - OPT_PLACEHOLDER_SYNTAX_SIMPLE, - OPT_STRING_ESCAPE_BACKSLASH_HEX, - OPT_STRING_FORMAT_PREFIX_BIG_E, - Dialect, - PostgreSql, -) - - -def test_getters_round_trip(): - dialect = Dialect( - placeholder_character="$", - placeholder_syntax=OPT_PLACEHOLDER_SYNTAX_NUMBERED, - like_escape=OPT_LIKE_ESCAPE_SCALAR | OPT_LIKE_ESCAPE_ANY, - bool_format=OPT_BOOL_FORMAT_INTEGER, - cast_double_colon=True, - ) - assert dialect.placeholder_character == "$" - assert dialect.placeholder_syntax == OPT_PLACEHOLDER_SYNTAX_NUMBERED - assert dialect.like_escape == OPT_LIKE_ESCAPE_SCALAR | OPT_LIKE_ESCAPE_ANY - assert dialect.bool_format == OPT_BOOL_FORMAT_INTEGER - assert dialect.cast_double_colon is True - assert dialect.supports_similar_to is False - - default = Dialect() - assert default.placeholder_character == "?" - assert default.placeholder_syntax == OPT_PLACEHOLDER_SYNTAX_SIMPLE - assert default.cast_double_colon is False - assert default.bitwise_operations == ( - OPT_BITWISE_OPERATIONS_AND - | OPT_BITWISE_OPERATIONS_OR - | OPT_BITWISE_OPERATIONS_NOT - | OPT_BITWISE_OPERATIONS_SHL - | OPT_BITWISE_OPERATIONS_SHR - ) - assert default.bitwise_call_syntax == OPT_BITWISE_CALL_SYNTAX_INFIX - assert default.bitwise_xor_spelling == OPT_BITWISE_XOR_SPELLING_CARET - assert default.like_escape == 1 - assert default.tuple_arity == 3 +import rapidquery as rq - with pytest.raises(ValueError, match="bitwise_operations"): - Dialect(bitwise_operations=1 << 7) +def test_default_constructors(): + for cls in ( + rq.dialect.AggregateConfig, + rq.dialect.BitwiseConfig, + rq.dialect.CastConfig, + rq.dialect.Dialect, + rq.dialect.LikeConfig, + rq.dialect.LiteralConfig, + rq.dialect.OperatorConfig, + rq.dialect.SchemaConfig, + rq.dialect.StringConfig, + rq.dialect.TemporalConfig, + rq.dialect.TupleConfig, + ): + _ = cls() -def test_builtin_getters(): - assert PostgreSql.placeholder_character == "$" - assert PostgreSql.placeholder_syntax == OPT_PLACEHOLDER_SYNTAX_NUMBERED - assert PostgreSql.cast_double_colon is True - assert PostgreSql.like_escape == OPT_LIKE_ESCAPE_SCALAR +def test_enum_constructor(): + cfg = rq.dialect.AggregateConfig( + null_treatment=rq.dialect.OPT_NULL_TREATMENT_UNSUPPORTED, + function_filter=rq.dialect.OPT_FUNCTION_FILTER_UNSUPPORTED, + ) + assert cfg.null_treatment == rq.dialect.OPT_NULL_TREATMENT_UNSUPPORTED + assert cfg.function_filter == rq.dialect.OPT_FUNCTION_FILTER_UNSUPPORTED -# Edge cases are tested in Rust -def test_dialect_methods(): - default = Dialect( - bool_format=OPT_BOOL_FORMAT_INTEGER, - string_format=OPT_STRING_FORMAT_PREFIX_BIG_E, - string_escape=OPT_STRING_ESCAPE_BACKSLASH_HEX, - identifier_format=OPT_IDENTIFIER_FORMAT_BACKTICK, - identifier_escape=OPT_IDENTIFIER_ESCAPE_BACKSLASH, - bit_format=OPT_BIT_FORMAT_POSTFIX_BIG_B, - bytes_format=OPT_BYTES_FORMAT_SLASH_X_BYTEA, + cfg = rq.dialect.AggregateConfig( + null_treatment=rq.dialect.OPT_NULL_TREATMENT_INSIDE_PARENS, + function_filter=rq.dialect.OPT_FUNCTION_FILTER_FILTER_WHERE, ) - assert default.write_boolean(True) == "1" + assert cfg.null_treatment == rq.dialect.OPT_NULL_TREATMENT_INSIDE_PARENS + assert cfg.function_filter == rq.dialect.OPT_FUNCTION_FILTER_FILTER_WHERE + + # invalid inputs + with pytest.raises(ValueError): + _ = rq.dialect.AggregateConfig(null_treatment=9) - assert default.write_string_literal("love") == "E'love'" - assert default.write_identifier("lov`e") == "`lov\\`e`" +def test_bitflags_constructor(): + cfg = rq.dialect.SchemaConfig( + features=rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED | rq.dialect.OPT_SCHEMA_FEATURES_IF_NOT_EXISTS + ) - assert default.write_bits("10101") == "'10101'B" + assert cfg.features == rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED | rq.dialect.OPT_SCHEMA_FEATURES_IF_NOT_EXISTS + assert cfg.features & rq.dialect.OPT_SCHEMA_FEATURES_SUPPORTED > 0 + assert cfg.features & rq.dialect.OPT_SCHEMA_FEATURES_DROP_CASCADE == 0 + # invalid inputs with pytest.raises(ValueError): - _ = default.write_bits("123") + _ = rq.dialect.SchemaConfig(features=1 << 7) - assert default.write_bytes(b"love") == "'\\x6C6F7665'::bytea" + +def test_template_constructor(): + # We don't need to test them here + # because they are tested while creating named dialects + pass + + +# Edge cases are tested in Rust +def test_helper_methods(): + d = rq.dialect.StringConfig(template="E'{hex}'") + assert d.serialize_string("love") == "E'love'" + + d = rq.dialect.LiteralConfig( + boolean=rq.dialect.OPT_BOOL_FORMAT_INTEGER, + bit="'{}'HELLO WORLD", + bytes="WELCOME '{hex}'", + ) + assert d.serialize_boolean(True) == "1" + assert d.serialize_boolean(False) == "0" + assert d.serialize_bits("10101") == "'10101'HELLO WORLD" + assert d.serialize_bytes(b"love") == "WELCOME '6C6F7665'" diff --git a/tests/test_expr.py b/tests/test_expr.py index 3b42b59..8f7014d 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -529,9 +529,9 @@ def test_to_sql(self): assert params == (10,) concat = BinaryExpr(1, "||", BinaryExpr(2, "+", 3)) - assert concat.to_sql(PostgreSql) == "1 || (2 + 3)" - assert concat.to_sql(Sqlite) == "1 || (2 + 3)" - assert concat.to_sql(MsSql) == "1 + (2 + 3)" + assert concat.to_sql(PostgreSql) == "1 || 2 + 3" + assert concat.to_sql(Sqlite) == "1 || 2 + 3" + assert concat.to_sql(MsSql) == "1 + 2 + 3" assert concat.to_sql(MySql) == "CONCAT(1, 2 + 3)" assert BinaryExpr(BinaryExpr(1, "||", 2), "||", 3).to_sql(MySql) == "CONCAT(CONCAT(1, 2), 3)" diff --git a/tests/test_serializers.py b/tests/test_serializers.py index d3d8f9e..699886a 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -186,7 +186,7 @@ class TestBitSerializer: def test_follows_bit_format_when_supported(self, dialect: Dialect): expected = BIT_SQL.get(dialect) if expected is None: - with pytest.raises(NotImplementedError, match="does not support BitSerializer"): + with pytest.raises(RuntimeError, match="does not support BitSerializer"): BitSerializer().serialize(dialect, "10") return assert BitSerializer().serialize(dialect, "10") == expected @@ -293,7 +293,7 @@ def test_datetime_overflow(self): class TestInetSerializer: def test_canonical_cidr(self, dialect: Dialect): if dialect not in (PostgreSql, DuckDb): - with pytest.raises(NotImplementedError, match="does not support InetSerializer"): + with pytest.raises(RuntimeError, match="does not support InetSerializer"): InetSerializer().serialize(dialect, "192.168.1.1") return s = InetSerializer() @@ -319,7 +319,7 @@ def test_invalid_address(self): class TestMacAddressSerializer: def test_canonical_form(self, dialect: Dialect): if dialect != PostgreSql: - with pytest.raises(NotImplementedError, match="does not support MacAddressSerializer"): + with pytest.raises(RuntimeError, match="does not support MacAddressSerializer"): MacAddressSerializer().serialize(dialect, "01:23:45:67:89:ab") return s = MacAddressSerializer() @@ -345,7 +345,7 @@ def test_renders_array_constructor(self, dialect: Dialect): s = ArraySerializer(IntegerSerializer()) expected = ARRAY_SQL.get(dialect) if expected is None: - with pytest.raises(NotImplementedError, match="does not support ArraySerializer"): + with pytest.raises(RuntimeError, match="does not support ArraySerializer"): s.serialize(dialect, [1, 2, 3]) return assert s.serialize(dialect, [1, 2, 3]) == expected @@ -370,7 +370,7 @@ def test_rejects_non_sequence_and_bad_element(self): s.serialize(PG, [1, "x"]) def test_nested_unsupported_element(self): - with pytest.raises(NotImplementedError, match="does not support BitSerializer"): + with pytest.raises(RuntimeError, match="does not support BitSerializer"): ArraySerializer(BitSerializer()).serialize(BigQuery, ["10"]) @@ -385,13 +385,13 @@ def test_must_return_str(self): def test_unsupported_serializer_error_message(): - with pytest.raises(NotImplementedError, match="does not support BitSerializer"): + with pytest.raises(RuntimeError, match="does not support BitSerializer"): BitSerializer().serialize(Sqlite, "10") - with pytest.raises(NotImplementedError, match="does not support ArraySerializer"): + with pytest.raises(RuntimeError, match="does not support ArraySerializer"): ArraySerializer(IntegerSerializer()).serialize(MySql, [1, 2]) - with pytest.raises(NotImplementedError, match="does not support InetSerializer"): + with pytest.raises(RuntimeError, match="does not support InetSerializer"): InetSerializer().serialize(Sqlite, "192.168.1.1") - with pytest.raises(NotImplementedError, match="does not support MacAddressSerializer"): + with pytest.raises(RuntimeError, match="does not support MacAddressSerializer"): MacAddressSerializer().serialize(DuckDb, "01:23:45:67:89:ab") From 1c1ff11d80d7b3aede125531d200bd2ef6f74912 Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Mon, 26 Oct 2026 13:43:46 +0330 Subject: [PATCH 10/11] Update tests --- tests/test_dialect.py | 214 +++++++++++++- tests/test_ident.py | 2 - tests/test_serializers.py | 570 ++++++++++++++++++++++---------------- 3 files changed, 534 insertions(+), 252 deletions(-) diff --git a/tests/test_dialect.py b/tests/test_dialect.py index 785222b..8c93203 100644 --- a/tests/test_dialect.py +++ b/tests/test_dialect.py @@ -1,3 +1,5 @@ +from collections.abc import Callable, Sequence + import pytest import rapidquery as rq @@ -54,12 +56,6 @@ def test_bitflags_constructor(): _ = rq.dialect.SchemaConfig(features=1 << 7) -def test_template_constructor(): - # We don't need to test them here - # because they are tested while creating named dialects - pass - - # Edge cases are tested in Rust def test_helper_methods(): d = rq.dialect.StringConfig(template="E'{hex}'") @@ -74,3 +70,209 @@ def test_helper_methods(): assert d.serialize_boolean(False) == "0" assert d.serialize_bits("10101") == "'10101'HELLO WORLD" assert d.serialize_bytes(b"love") == "WELCOME '6C6F7665'" + + +class TemplateCase: + """Valid and invalid strings for one ``template(keys; min..=max)`` field. + + ``keys`` uses the Rust macro spelling. ``"left"`` is required, ``"hex?"`` is + optional, ``"_"`` is the positional ``{}``, and ``"_?"`` is an optional ``{}``. + ``maximum`` defaults to ``minimum``. + + Unknown ``{...}`` text is not a placeholder. A required key is reported only + when the placeholder count is already inside ``[minimum, maximum]``. + """ + + def __init__(self, keys: Sequence[str], minimum: int, maximum: int | None = None) -> None: + if minimum < 0: + raise ValueError("minimum must be >= 0") + self.minimum = minimum + self.maximum = minimum if maximum is None else maximum + if self.minimum > self.maximum: + raise ValueError("minimum is greater than maximum") + + # (key name, `{name}` text, required). `_` is the empty positional name. + parsed: list[tuple[str, str, bool]] = [] + for key in keys: + optional = key.endswith("?") + name = key[:-1] if optional else key + if name == "_": + name = "" + parsed.append((name, "{" + name + "}", not optional)) + if not parsed: + raise ValueError("expected at least one template key") + self._keys = parsed + + def valid(self) -> list[str]: + """Templates ``Template::parse`` accepts for this field.""" + + required = [placeholder for _, placeholder, is_required in self._keys if is_required] + optional = [placeholder for _, placeholder, is_required in self._keys if not is_required] + out: list[str] = [] + + def add(text: str) -> None: + if text not in out: + out.append(text) + + if not required and self.minimum == 0: + add("") + add("?") + + if not required and self.minimum <= 1 <= self.maximum: + for placeholder in optional: + add(placeholder) + + for count in (self.minimum, self.maximum): + built = self._build(count, required, optional) + if built is not None: + add(built) + + skeleton = next((text for text in out if text not in ("", "?")), "") + add(f"pre{skeleton}post{{not-a-key}}") + return out + + def invalid(self) -> list[tuple[str, str]]: + """``(template, ValueError match)`` pairs ``Template::parse`` rejects.""" + + cases = [("a" * 257, "template is too long")] + + if self.minimum > 0: + short = self.minimum - 1 + text = "plain" if short == 0 else self._keys[0][1] * short + cases.append((text, "placeholders are missing")) + + cases.append((self._keys[0][1] * (self.maximum + 1), "too many placeholders")) + + missing = self._missing_required() + if missing is not None: + cases.append(missing) + return cases + + def _build(self, count: int, required: list[str], optional: list[str]) -> str | None: + if count < len(required) or not self.minimum <= count <= self.maximum: + return None + parts = list(required) + if len(parts) == count: + return "".join(parts) + filler = required[0] if required else (optional[0] if optional else None) + if filler is None: + return None + parts.extend([filler] * (count - len(parts))) + return "".join(parts) + + def _missing_required(self) -> tuple[str, str] | None: + """A count-legal template that omits one required key, when that can happen.""" + + required = [(name, placeholder) for name, placeholder, is_required in self._keys if is_required] + if not required: + return None + + omit_name, omit_placeholder = required[-1] + keep = [ + placeholder for _, placeholder, is_required in self._keys if is_required and placeholder != omit_placeholder + ] + fillers = keep + [placeholder for _, placeholder, is_required in self._keys if not is_required] + target = max(self.minimum, len(keep)) + + if not fillers: + if self.minimum == 0 and not keep: + return ("plain", f"one of required keys not found: {omit_name}") + return None + if target > self.maximum: + return None + + parts = list(keep) + while len(parts) < target: + parts.append(fillers[0]) + + present = set(parts) + missing_name = next( + name for name, placeholder, is_required in self._keys if is_required and placeholder not in present + ) + return ("".join(parts), f"one of required keys not found: {missing_name}") + + +def _template_fields() -> list[tuple[str, Callable[[str], str], TemplateCase]]: + positional = TemplateCase(("_",), 1) + + def string_template(text: str) -> str: + return rq.dialect.StringConfig(template=text).template + + def string_concat(text: str) -> str: + return rq.dialect.StringConfig(concat=text).concat + + def try_cast(text: str) -> str: + rendered = rq.dialect.CastConfig(try_cast=text).try_cast + assert rendered is not None + return rendered + + def temporal(field: str) -> Callable[[str], str]: + def accept(text: str) -> str: + cfg = rq.dialect.TemporalConfig(**{field: text}) + rendered: str = getattr(cfg, field) + return rendered + + return accept + + def literal(field: str) -> Callable[[str], str]: + def accept(text: str) -> str: + cfg = rq.dialect.LiteralConfig(**{field: text}) + rendered = getattr(cfg, field) + assert rendered is not None + return str(rendered) + + return accept + + def location(text: str) -> str: + rendered = rq.dialect.SchemaConfig(location=text).location + assert rendered is not None + return rendered + + def placeholder(text: str) -> str: + return rq.dialect.Dialect(placeholder=text).placeholder + + def identifier(text: str) -> str: + return rq.dialect.Dialect(identifier=text).identifier + + string_keys = ("single?", "double?", "hex?", "hex_quote?", "mysql?", "unicode?", "unicode_quote?") + identifier_keys = ( + "double?", + "single?", + "bracket?", + "backtick?", + "slashdouble?", + "slashsingle?", + "slashbracket?", + "slashbacktick?", + ) + return [ + ("string.template", string_template, TemplateCase(string_keys, 1)), + ("string.concat", string_concat, TemplateCase(("left", "right"), 2)), + ("cast.try_cast", try_cast, TemplateCase(("expr", "type"), 2)), + ("temporal.date", temporal("date"), positional), + ("temporal.time", temporal("time"), positional), + ("temporal.datetime_naive", temporal("datetime_naive"), positional), + ("temporal.datetime_aware", temporal("datetime_aware"), positional), + ("literal.bit", literal("bit"), positional), + ("literal.inet", literal("inet"), positional), + ("literal.macaddr", literal("macaddr"), positional), + ("literal.uuid", literal("uuid"), positional), + ("literal.json", literal("json"), positional), + ("literal.array", literal("array"), positional), + ("literal.bytes", literal("bytes"), TemplateCase(("hex?", "slashx?"), 1)), + ("schema.location", location, positional), + ("dialect.placeholder", placeholder, TemplateCase(("_?",), 0, 1)), + ("dialect.identifier", identifier, TemplateCase(identifier_keys, 1)), + ] + + +@pytest.mark.parametrize( + ("accept", "case"), + [pytest.param(accept, case, id=name) for name, accept, case in _template_fields()], +) +def test_template_parameters(accept: Callable[[str], str], case: TemplateCase) -> None: + for text in case.valid(): + assert accept(text) == text + for text, match in case.invalid(): + with pytest.raises(ValueError, match=match): + accept(text) diff --git a/tests/test_ident.py b/tests/test_ident.py index e6e4ab7..04b6766 100644 --- a/tests/test_ident.py +++ b/tests/test_ident.py @@ -1,5 +1,3 @@ -import typing - import pytest from rapidquery import Asterisk, Ident, Unquote diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 699886a..0cf6c31 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -1,4 +1,4 @@ -"""Serializer type checks, canonicalization, and literal spelling.""" +"""Serializer type checks and literal spelling for a configured dialect.""" import datetime import enum @@ -10,20 +10,7 @@ import pytest -from rapidquery.dialect import ( - BigQuery, - Dialect, - DuckDb, - Hive, - MsSql, - MySql, - Oracle, - PostgreSql, - Snowflake, - Spark, - Sqlite, - Teradata, -) +from rapidquery.dialect import OPT_BOOL_FORMAT_INTEGER, Dialect, LiteralConfig, StringConfig, TemporalConfig from rapidquery.serializers import ( ArraySerializer, BitSerializer, @@ -43,23 +30,14 @@ UuidSerializer, ) -from .conftest import ALL_DIALECTS, USING_ORJSON - -PG = PostgreSql -BOOL_INT = (MsSql, MySql, Oracle, Sqlite) -BIT_SQL = { - PostgreSql: "B'10'", - DuckDb: "B'10'", - MySql: "b'10'", - Teradata: "'10'B", -} -ARRAY_SQL = { - PostgreSql: "ARRAY[1, 2, 3]", - BigQuery: "ARRAY[1, 2, 3]", - DuckDb: "[1, 2, 3]", - Spark: "array(1, 2, 3)", - Hive: "array(1, 2, 3)", -} +from .conftest import USING_ORJSON + +_UUID = "103596b1-db75-4ae0-907a-a4a9bc4dab85" +_DATE = datetime.date(2024, 1, 15) +_TIME = datetime.time(13, 45, 30) +_TIME_FRAC = datetime.time(1, 2, 3, 456789) +_NAIVE = datetime.datetime(2024, 1, 15, 10, 30, 0) +_OFFSET = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) class InvalidStrEnum(enum.Enum): @@ -71,337 +49,441 @@ class ExampleDataclass: x: int -def _custom_ok(dialect: Dialect, obj: typing.Any) -> str: - assert isinstance(dialect, Dialect) - return f"CUSTOM:{obj}" +def _literal(**kwargs: typing.Any) -> Dialect: + return Dialect(literal=LiteralConfig(**kwargs)) -def _custom_not_str(dialect: Dialect, obj: typing.Any) -> int: - return 12345 +def _temporal(**kwargs: str) -> Dialect: + return Dialect(temporal=TemporalConfig(**kwargs)) class TestIntegerSerializer: def test_values(self): - s = IntegerSerializer() - assert s.serialize(PG, 1) == "1" - assert s.serialize(PG, 18_446_744_073_709_551_615) == "18446744073709551615" - assert s.serialize(PG, -9_223_372_036_854_775_808) == "-9223372036854775808" - assert s.serialize(PG, True) == "1" - assert s.serialize(PG, False) == "0" + serializer = IntegerSerializer() + dialect = Dialect() + assert serializer.serialize(dialect, 1) == "1" + assert serializer.serialize(dialect, 18_446_744_073_709_551_615) == "18446744073709551615" + assert serializer.serialize(dialect, -9_223_372_036_854_775_808) == "-9223372036854775808" + assert serializer.serialize(dialect, True) == "1" + assert serializer.serialize(dialect, False) == "0" @pytest.mark.parametrize("value", [2.5, "invalid"]) def test_rejects_non_int(self, value: typing.Any): with pytest.raises(TypeError): - IntegerSerializer().serialize(PG, value) + IntegerSerializer().serialize(Dialect(), value) class TestFloatSerializer: def test_values(self): - s = FloatSerializer() - assert s.serialize(PG, 1.1) == "1.1" - assert s.serialize(PG, 1) == "1" - assert s.serialize(PG, 1.0) == "1.0" - assert s.serialize(PG, Decimal("1.3435")) == "1.3435" + serializer = FloatSerializer() + dialect = Dialect() + assert serializer.serialize(dialect, 1.1) == "1.1" + assert serializer.serialize(dialect, 1) == "1" + assert serializer.serialize(dialect, 1.0) == "1.0" + assert serializer.serialize(dialect, Decimal("1.3435")) == "1.3435" def test_rejects_non_numeric(self): with pytest.raises(TypeError): - FloatSerializer().serialize(PG, "invalid") + FloatSerializer().serialize(Dialect(), "invalid") @pytest.mark.parametrize("value", [float("nan"), float("inf"), float("-inf")]) def test_rejects_non_finite(self, value: float): with pytest.raises(ValueError): - FloatSerializer().serialize(PG, value) - - -@pytest.mark.parametrize("dialect", ALL_DIALECTS) -def test_boolean_spelling(dialect: Dialect): - s = BooleanSerializer() - if dialect in BOOL_INT: - assert s.serialize(dialect, True) == "1" - assert s.serialize(dialect, False) == "0" - else: - assert s.serialize(dialect, True) == "TRUE" - assert s.serialize(dialect, False) == "FALSE" + FloatSerializer().serialize(Dialect(), value) -@pytest.mark.parametrize("value", [1, 0, object()]) -def test_boolean_rejects_non_bool(value: typing.Any): - with pytest.raises(TypeError): - BooleanSerializer().serialize(PG, value) - +class TestBooleanSerializer: + def test_keyword(self): + serializer = BooleanSerializer() + dialect = Dialect() + assert serializer.serialize(dialect, True) == "TRUE" + assert serializer.serialize(dialect, False) == "FALSE" -@pytest.mark.parametrize("dialect", ALL_DIALECTS) -def test_string_obrien_and_nul(dialect: Dialect): - s = StringSerializer() - sql = s.serialize(dialect, "O'Brien") - assert "O'Brien" not in sql - assert "O''Brien" in sql or "O\\'Brien" in sql - with pytest.raises(ValueError, match="NUL"): - s.serialize(dialect, "a\0b") + def test_integer(self): + serializer = BooleanSerializer() + dialect = _literal(boolean=OPT_BOOL_FORMAT_INTEGER) + assert serializer.serialize(dialect, True) == "1" + assert serializer.serialize(dialect, False) == "0" - -@pytest.mark.parametrize("value", [InvalidStrEnum.Variant, 1, None, b"bytes"]) -def test_string_rejects_non_str(value: typing.Any): - with pytest.raises(TypeError): - StringSerializer().serialize(PG, value) + @pytest.mark.parametrize("value", [1, 0, object()]) + def test_rejects_non_bool(self, value: typing.Any): + with pytest.raises(TypeError): + BooleanSerializer().serialize(Dialect(), value) + + +# (template, value, sql). One template per escape key. `E` prefixes the hex key. +_STRING_CASES: list[tuple[str, str, str]] = [ + ("'{single}'", "hello", "'hello'"), + ("'{single}'", "O'Brien", "'O''Brien'"), + ("'{single}'", "a\\b", r"'a\b'"), + ("'{single}'", "a\nb", "'a\nb'"), + ("'{single}'", 'say "hi"', "'say \"hi\"'"), + ("'{single}'", "\t", "'\t'"), + ("'{single}'", "\x01", "'\x01'"), + ("'{single}'", "\x1a", "'\x1a'"), + ('"{double}"', "hello", '"hello"'), + ('"{double}"', "O'Brien", '"O\'Brien"'), + ('"{double}"', "a\\b", r'"a\b"'), + ('"{double}"', "a\nb", '"a\nb"'), + ('"{double}"', 'say "hi"', '"say ""hi"""'), + ('"{double}"', "\t", '"\t"'), + ('"{double}"', "\x01", '"\x01"'), + ('"{double}"', "\x1a", '"\x1a"'), + ("E'{hex}'", "hello", "E'hello'"), + ("E'{hex}'", "O'Brien", r"E'O\'Brien'"), + ("E'{hex}'", "a\\b", r"E'a\\b'"), + ("E'{hex}'", "a\nb", r"E'a\nb'"), + ("E'{hex}'", 'say "hi"', "E'say \"hi\"'"), + ("E'{hex}'", "\t", r"E'\t'"), + ("E'{hex}'", "\x01", r"E'\x01'"), + ("E'{hex}'", "\x1a", r"E'\x1A'"), + ('"{hex_quote}"', "hello", '"hello"'), + ('"{hex_quote}"', "O'Brien", r'"O\'Brien"'), + ('"{hex_quote}"', "a\\b", r'"a\\b"'), + ('"{hex_quote}"', "a\nb", r'"a\nb"'), + ('"{hex_quote}"', 'say "hi"', r'"say \"hi\""'), + ('"{hex_quote}"', "\t", r'"\t"'), + ('"{hex_quote}"', "\x01", r'"\x01"'), + ('"{hex_quote}"', "\x1a", r'"\x1A"'), + ("'{mysql}'", "hello", "'hello'"), + ("'{mysql}'", "O'Brien", r"'O\'Brien'"), + ("'{mysql}'", "a\\b", r"'a\\b'"), + ("'{mysql}'", "a\nb", r"'a\nb'"), + ("'{mysql}'", 'say "hi"', r"'say \"hi\"'"), + ("'{mysql}'", "\t", "'\t'"), + ("'{mysql}'", "\x01", "'\x01'"), + ("'{mysql}'", "\x1a", r"'\Z'"), + ("'{unicode}'", "hello", "'hello'"), + ("'{unicode}'", "O'Brien", r"'O\'Brien'"), + ("'{unicode}'", "a\\b", r"'a\\b'"), + ("'{unicode}'", "a\nb", r"'a\nb'"), + ("'{unicode}'", 'say "hi"', "'say \"hi\"'"), + ("'{unicode}'", "\t", r"'\t'"), + ("'{unicode}'", "\x01", r"'\u0001'"), + ("'{unicode}'", "\x1a", r"'\u001A'"), + ("'{unicode_quote}'", "hello", "'hello'"), + ("'{unicode_quote}'", "O'Brien", r"'O\'Brien'"), + ("'{unicode_quote}'", "a\\b", r"'a\\b'"), + ("'{unicode_quote}'", "a\nb", r"'a\nb'"), + ("'{unicode_quote}'", 'say "hi"', r"'say \"hi\"'"), + ("'{unicode_quote}'", "\t", r"'\t'"), + ("'{unicode_quote}'", "\x01", r"'\u0001'"), + ("'{unicode_quote}'", "\x1a", r"'\u001A'"), +] + + +class TestStringSerializer: + @pytest.mark.parametrize(("template", "value", "expected"), _STRING_CASES) + def test_escape_key(self, template: str, value: str, expected: str): + dialect = Dialect(string=StringConfig(template=template)) + assert StringSerializer().serialize(dialect, value) == expected + + @pytest.mark.parametrize("template", ["'{single}'", "E'{hex}'"]) + def test_rejects_nul(self, template: str): + dialect = Dialect(string=StringConfig(template=template)) + with pytest.raises(ValueError, match="NUL"): + StringSerializer().serialize(dialect, "a\0b") + + @pytest.mark.parametrize("value", [InvalidStrEnum.Variant, 1, None, b"bytes"]) + def test_rejects_non_str(self, value: typing.Any): + with pytest.raises(TypeError): + StringSerializer().serialize(Dialect(), value) class TestUuidSerializer: - _TEXT = "103596b1-db75-4ae0-907a-a4a9bc4dab85" - - def test_uuid_and_str(self): - s = UuidSerializer() - expected = f"E'{self._TEXT}'::uuid" - assert s.serialize(PG, self._TEXT) == expected - assert s.serialize(PG, uuid.UUID(self._TEXT)) == expected - assert s.serialize(DuckDb, self._TEXT) == f"UUID '{self._TEXT}'" - assert s.serialize(Sqlite, self._TEXT) == f"'{self._TEXT}'" + @pytest.mark.parametrize("template", ["'{}'", "E'{}'::uuid", "UUID '{}'", "N'{}'"]) + def test_str_and_uuid(self, template: str): + dialect = _literal(uuid=template) + expected = template.replace("{}", _UUID) + serializer = UuidSerializer() + assert serializer.serialize(dialect, _UUID) == expected + assert serializer.serialize(dialect, uuid.UUID(_UUID)) == expected + + def test_unsupported(self): + with pytest.raises(RuntimeError, match="does not support UuidSerializer"): + UuidSerializer().serialize(Dialect(), _UUID) def test_invalid_str(self): + dialect = _literal(uuid="'{}'") with pytest.raises(ValueError): - UuidSerializer().serialize(PG, "103596-db75-4ae0-907a-a4a9bc4d85") + UuidSerializer().serialize(dialect, "103596-db75-4ae0-907a-a4a9bc4d85") def test_rejects_non_uuid(self): + dialect = _literal(uuid="'{}'") with pytest.raises(TypeError): - UuidSerializer().serialize(PG, 6428739) - - -def test_bytes_pg_snowflake_duckdb(): - dead = b"\xde\xad" - assert BytesSerializer().serialize(PG, dead) == "'\\xDEAD'::bytea" - assert BytesSerializer().serialize(PG, b"") == "'\\x'::bytea" - assert BytesSerializer().serialize(Snowflake, dead) == "TO_BINARY('DEAD', 'HEX')" - assert BytesSerializer().serialize(Snowflake, b"") == "TO_BINARY('', 'HEX')" - assert BytesSerializer().serialize(DuckDb, dead) == "'\\xDE\\xAD'::BLOB" - assert BytesSerializer().serialize(DuckDb, b"") == "''::BLOB" + UuidSerializer().serialize(dialect, 6428739) + + +@pytest.mark.parametrize( + ("template", "dead", "empty"), + [ + ("X'{hex}'", "X'DEAD'", "X''"), + (r"'\x{hex}'::bytea", r"'\xDEAD'::bytea", r"'\x'::bytea"), + ("0x{hex}", "0xDEAD", "0x"), + ("UNHEX('{hex}')", "UNHEX('DEAD')", "UNHEX('')"), + ("HEXTORAW('{hex}')", "HEXTORAW('DEAD')", "HEXTORAW('')"), + ("TO_VARBYTE('{hex}', 'hex')", "TO_VARBYTE('DEAD', 'hex')", "TO_VARBYTE('', 'hex')"), + ("TO_BINARY('{hex}', 'HEX')", "TO_BINARY('DEAD', 'HEX')", "TO_BINARY('', 'HEX')"), + ("'{hex}'XBV", "'DEAD'XBV", "''XBV"), + ("B'{slashx}'", r"B'\xDE\xAD'", "B''"), + ("'{slashx}'::BLOB", r"'\xDE\xAD'::BLOB", "''::BLOB"), + ], +) +def test_bytes_template(template: str, dead: str, empty: str): + dialect = _literal(bytes=template) + serializer = BytesSerializer() + assert serializer.serialize(dialect, b"\xde\xad") == dead + assert serializer.serialize(dialect, b"") == empty @pytest.mark.parametrize("value", [bytearray(b"abc"), memoryview(b"abc"), "abc", 1, None]) def test_bytes_rejects_non_exact_bytes(value: typing.Any): with pytest.raises(TypeError): - BytesSerializer().serialize(PG, value) + BytesSerializer().serialize(Dialect(), value) class TestBitSerializer: - def test_follows_bit_format_when_supported(self, dialect: Dialect): - expected = BIT_SQL.get(dialect) - if expected is None: - with pytest.raises(RuntimeError, match="does not support BitSerializer"): - BitSerializer().serialize(dialect, "10") - return - assert BitSerializer().serialize(dialect, "10") == expected + @pytest.mark.parametrize( + ("template", "expected"), + [("B'{}'", "B'10'"), ("b'{}'", "b'10'"), ("'{}'B", "'10'B")], + ) + def test_embeds_bits(self, template: str, expected: str): + assert BitSerializer().serialize(_literal(bit=template), "10") == expected + + def test_unsupported(self): + with pytest.raises(RuntimeError, match="does not support BitSerializer"): + BitSerializer().serialize(Dialect(), "10") def test_rejects_non_bit_string(self): + dialect = _literal(bit="B'{}'") with pytest.raises(ValueError): - BitSerializer().serialize(PG, "102") + BitSerializer().serialize(dialect, "102") with pytest.raises(ValueError): - BitSerializer().serialize(PG, "") + BitSerializer().serialize(dialect, "") with pytest.raises(TypeError): - BitSerializer().serialize(PG, 10) + BitSerializer().serialize(dialect, 10) class TestJSONSerializer: - def test_dumps_then_json_format(self): - s = JSONSerializer() - assert s.serialize(PG, 10) == "E'10'" - assert s.serialize(PG, "10") == "E'\"10\"'" - assert s.serialize(BigQuery, {"a": 1}) == "JSON '{\"a\":1}'" - assert s.serialize(Snowflake, {"a": 1}) == "PARSE_JSON('{\"a\":1}')" - assert s.serialize(Sqlite, {"a": 1}) == "'{\"a\":1}'" + @pytest.mark.parametrize( + ("string_template", "json_template", "obj", "expected"), + [ + ("E'{hex}'", "{}", 10, "E'10'"), + ("E'{hex}'", "{}", "10", "E'\"10\"'"), + ("'{single}'", "{}", {"a": 1}, "'{\"a\":1}'"), + ("N'{single}'", "{}", {"a": 1}, "N'{\"a\":1}'"), + ("'{hex}'", "JSON {}", {"a": 1}, "JSON '{\"a\":1}'"), + ("'{hex}'", "PARSE_JSON({})", {"a": 1}, "PARSE_JSON('{\"a\":1}')"), + ("'{mysql}'", "{}", {"a": 1}, r"'{\"a\":1}'"), + ("'{unicode_quote}'", "{}", {"a": 1}, r"'{\"a\":1}'"), + ('"{hex_quote}"', "{}", {"a": 1}, r'"{\"a\":1}"'), + ("'{unicode}'", "{}", {"a": 1}, "'{\"a\":1}'"), + ], + ) + def test_dumps_then_wraps(self, string_template: str, json_template: str, obj: typing.Any, expected: str): + dialect = Dialect(string=StringConfig(template=string_template), literal=LiteralConfig(json=json_template)) + assert JSONSerializer().serialize(dialect, obj) == expected def test_orjson_only_values(self): - s = JSONSerializer() + dialect = Dialect(string=StringConfig(template="E'{hex}'")) + serializer = JSONSerializer() extras = [ ( datetime.datetime(2024, 10, 1, tzinfo=datetime.timezone.utc), "E'\"2024-10-01T00:00:00+00:00\"'", ), - ( - uuid.UUID("103596b1-db75-4ae0-907a-a4a9bc4dab85"), - "E'\"103596b1-db75-4ae0-907a-a4a9bc4dab85\"'", - ), + (uuid.UUID(_UUID), f"E'\"{_UUID}\"'"), (ExampleDataclass(x=1), "E'{\"x\":1}'"), ] for obj, expected in extras: if USING_ORJSON: - assert s.serialize(PG, obj) == expected + assert serializer.serialize(dialect, obj) == expected else: with pytest.raises(TypeError): - s.serialize(PG, obj) + serializer.serialize(dialect, obj) class TestDateTimeSerializers: - def test_date(self): - value = datetime.date(2024, 1, 15) - assert DateSerializer().serialize(PG, value) == "DATE E'2024-01-15'" - assert DateSerializer().serialize(Oracle, value) == "DATE '2024-01-15'" - assert DateSerializer().serialize(Sqlite, value) == "'2024-01-15'" + @pytest.mark.parametrize( + ("template", "expected"), + [ + ("DATE '{}'", "DATE '2024-01-15'"), + ("'{}'", "'2024-01-15'"), + ("DATE E'{}'", "DATE E'2024-01-15'"), + ("N'{}'", "N'2024-01-15'"), + ], + ) + def test_date(self, template: str, expected: str): + assert DateSerializer().serialize(_temporal(date=template), _DATE) == expected def test_date_rejects_datetime_and_str(self): with pytest.raises(TypeError): - DateSerializer().serialize(PG, datetime.datetime(2024, 1, 15, 10, 0)) + DateSerializer().serialize(Dialect(), datetime.datetime(2024, 1, 15, 10, 0)) with pytest.raises(TypeError): - DateSerializer().serialize(PG, "2024-01-15") - - def test_time(self): - s = TimeSerializer() - assert s.serialize(PG, datetime.time(13, 45, 30)) == "TIME E'13:45:30'" - assert s.serialize(PG, datetime.time(1, 2, 3, 456789)) == "TIME E'01:02:03.456789'" - assert s.serialize(Oracle, datetime.time(13, 45, 30)) == "'13:45:30'" + DateSerializer().serialize(Dialect(), "2024-01-15") + + @pytest.mark.parametrize( + ("template", "expected"), + [ + ("TIME '{}'", "TIME '13:45:30'"), + ("'{}'", "'13:45:30'"), + ("TIME E'{}'", "TIME E'13:45:30'"), + ("N'{}'", "N'13:45:30'"), + ], + ) + def test_time(self, template: str, expected: str): + assert TimeSerializer().serialize(_temporal(time=template), _TIME) == expected + + def test_time_fraction(self): + dialect = _temporal(time="TIME E'{}'") + assert TimeSerializer().serialize(dialect, _TIME_FRAC) == "TIME E'01:02:03.456789'" def test_time_rejects_date(self): with pytest.raises(TypeError): - TimeSerializer().serialize(PG, datetime.date(2024, 1, 1)) - - def test_datetime_naive_and_aware(self): - s = DatetimeSerializer() - naive = datetime.datetime(2024, 1, 15, 10, 30, 0) - utc = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc) - offset = datetime.datetime( - 2024, - 1, - 15, - 10, - 30, - 0, - tzinfo=datetime.timezone(datetime.timedelta(hours=-5)), - ) - assert s.serialize(PG, naive) == "TIMESTAMP E'2024-01-15 10:30:00'" - assert s.serialize(PG, utc) == "TIMESTAMPTZ E'2024-01-15 10:30:00+00:00'" - assert s.serialize(PG, offset) == "TIMESTAMPTZ E'2024-01-15 10:30:00-05:00'" - assert s.serialize(Oracle, naive) == "TIMESTAMP '2024-01-15 10:30:00'" - assert s.serialize(Oracle, offset) == "TIMESTAMP WITH TIMEZONE '2024-01-15 10:30:00-05:00'" - assert s.serialize(Sqlite, naive) == "'2024-01-15 10:30:00'" - assert s.serialize(Sqlite, offset) == "'2024-01-15 10:30:00-05:00'" + TimeSerializer().serialize(Dialect(), _DATE) + + @pytest.mark.parametrize( + ("template", "expected"), + [ + ("TIMESTAMP '{}'", "TIMESTAMP '2024-01-15 10:30:00'"), + ("'{}'", "'2024-01-15 10:30:00'"), + ("TIMESTAMP E'{}'", "TIMESTAMP E'2024-01-15 10:30:00'"), + ("N'{}'", "N'2024-01-15 10:30:00'"), + ], + ) + def test_datetime_naive(self, template: str, expected: str): + assert DatetimeSerializer().serialize(_temporal(datetime_naive=template), _NAIVE) == expected + + @pytest.mark.parametrize( + ("template", "expected"), + [ + ("TIMESTAMPTZ '{}'", "TIMESTAMPTZ '2024-01-15 10:30:00-05:00'"), + ("TIMESTAMP WITH TIMEZONE '{}'", "TIMESTAMP WITH TIMEZONE '2024-01-15 10:30:00-05:00'"), + ("TIMESTAMPTZ E'{}'", "TIMESTAMPTZ E'2024-01-15 10:30:00-05:00'"), + ("N'{}'", "N'2024-01-15 10:30:00-05:00'"), + ], + ) + def test_datetime_aware(self, template: str, expected: str): + assert DatetimeSerializer().serialize(_temporal(datetime_aware=template), _OFFSET) == expected def test_datetime_unix_timestamp(self): - s = DatetimeSerializer() - assert s.serialize(PG, 0) == "TIMESTAMPTZ E'1970-01-01 00:00:00+00:00'" - assert s.serialize(PG, 0.5) == "TIMESTAMPTZ E'1970-01-01 00:00:00.500+00:00'" + dialect = _temporal(datetime_aware="TIMESTAMPTZ E'{}'") + serializer = DatetimeSerializer() + assert serializer.serialize(dialect, 0) == "TIMESTAMPTZ E'1970-01-01 00:00:00+00:00'" + assert serializer.serialize(dialect, 0.5) == "TIMESTAMPTZ E'1970-01-01 00:00:00.500+00:00'" def test_datetime_rejects_date_and_str(self): with pytest.raises(TypeError): - DatetimeSerializer().serialize(PG, datetime.date(2024, 1, 15)) + DatetimeSerializer().serialize(Dialect(), _DATE) with pytest.raises(TypeError): - DatetimeSerializer().serialize(PG, "2024-01-15T10:30:00") + DatetimeSerializer().serialize(Dialect(), "2024-01-15T10:30:00") def test_datetime_overflow(self): with pytest.raises(OverflowError): - DatetimeSerializer().serialize(PG, 10**20) + DatetimeSerializer().serialize(Dialect(), 10**20) class TestInetSerializer: - def test_canonical_cidr(self, dialect: Dialect): - if dialect not in (PostgreSql, DuckDb): - with pytest.raises(RuntimeError, match="does not support InetSerializer"): - InetSerializer().serialize(dialect, "192.168.1.1") - return - s = InetSerializer() - cases = [ + @pytest.mark.parametrize("template", ["E'{}'::inet", "'{}'::inet"]) + @pytest.mark.parametrize( + ("obj", "inner"), + [ ("192.168.1.1", "192.168.1.1/32"), (ipaddress.ip_address("192.168.1.1"), "192.168.1.1/32"), ("192.168.1.0/24", "192.168.1.0/24"), ("::1", "::1/128"), ("2001:db8::/32", "2001:db8::/32"), - ] - for obj, inner in cases: - if dialect == PostgreSql: - expected = f"E'{inner}'::inet" - else: - expected = f"'{inner}'::inet" - assert s.serialize(dialect, obj) == expected + ], + ) + def test_canonical_cidr(self, template: str, obj: typing.Any, inner: str): + assert InetSerializer().serialize(_literal(inet=template), obj) == template.replace("{}", inner) + + def test_unsupported(self): + with pytest.raises(RuntimeError, match="does not support InetSerializer"): + InetSerializer().serialize(Dialect(), "192.168.1.1") def test_invalid_address(self): with pytest.raises(ValueError): - InetSerializer().serialize(PG, "not-an-ip") + InetSerializer().serialize(_literal(inet="'{}'::inet"), "not-an-ip") class TestMacAddressSerializer: - def test_canonical_form(self, dialect: Dialect): - if dialect != PostgreSql: - with pytest.raises(RuntimeError, match="does not support MacAddressSerializer"): - MacAddressSerializer().serialize(dialect, "01:23:45:67:89:ab") - return - s = MacAddressSerializer() + def test_canonical_form(self): + dialect = _literal(macaddr="E'{}'::macaddr") + serializer = MacAddressSerializer() expected = "E'01:23:45:67:89:AB'::macaddr" - assert s.serialize(dialect, "01:23:45:67:89:ab") == expected - assert s.serialize(dialect, "01-23-45-67-89-AB") == expected + assert serializer.serialize(dialect, "01:23:45:67:89:ab") == expected + assert serializer.serialize(dialect, "01-23-45-67-89-AB") == expected + + def test_unsupported(self): + with pytest.raises(RuntimeError, match="does not support MacAddressSerializer"): + MacAddressSerializer().serialize(Dialect(), "01:23:45:67:89:ab") def test_invalid_address(self): with pytest.raises(ValueError): - MacAddressSerializer().serialize(PG, "not-a-mac") + MacAddressSerializer().serialize(_literal(macaddr="E'{}'::macaddr"), "not-a-mac") class TestUnspecifiedSerializer: def test_uses_str(self): - s = UnspecifiedSerializer() - assert s.serialize(PG, 123) == "123" - assert s.serialize(PG, "hello") == "hello" - assert s.serialize(PG, None) == "None" + serializer = UnspecifiedSerializer() + dialect = Dialect() + assert serializer.serialize(dialect, 123) == "123" + assert serializer.serialize(dialect, "hello") == "hello" + assert serializer.serialize(dialect, None) == "None" class TestArraySerializer: - def test_renders_array_constructor(self, dialect: Dialect): - s = ArraySerializer(IntegerSerializer()) - expected = ARRAY_SQL.get(dialect) - if expected is None: - with pytest.raises(RuntimeError, match="does not support ArraySerializer"): - s.serialize(dialect, [1, 2, 3]) - return - assert s.serialize(dialect, [1, 2, 3]) == expected - assert s.serialize(dialect, (1, 2, 3)) == expected - empty = expected.replace("1, 2, 3", "") - assert s.serialize(dialect, []) == empty - nested = ArraySerializer(ArraySerializer(IntegerSerializer())) - if dialect in (PostgreSql, BigQuery): - assert nested.serialize(dialect, [[1, 2], [3]]) == "ARRAY[ARRAY[1, 2], ARRAY[3]]" - elif dialect == DuckDb: - assert nested.serialize(dialect, [[1, 2], [3]]) == "[[1, 2], [3]]" - elif dialect in (Spark, Hive): - assert nested.serialize(dialect, [[1, 2], [3]]) == "array(array(1, 2), array(3))" - else: - raise AssertionError(expected) + @pytest.mark.parametrize( + ("template", "expected", "empty", "nested"), + [ + ("ARRAY[{}]", "ARRAY[1, 2, 3]", "ARRAY[]", "ARRAY[ARRAY[1, 2], ARRAY[3]]"), + ("[{}]", "[1, 2, 3]", "[]", "[[1, 2], [3]]"), + ("array({})", "array(1, 2, 3)", "array()", "array(array(1, 2), array(3))"), + ], + ) + def test_renders_constructor(self, template: str, expected: str, empty: str, nested: str): + dialect = _literal(array=template) + serializer = ArraySerializer(IntegerSerializer()) + assert serializer.serialize(dialect, [1, 2, 3]) == expected + assert serializer.serialize(dialect, (1, 2, 3)) == expected + assert serializer.serialize(dialect, []) == empty + nested_serializer = ArraySerializer(ArraySerializer(IntegerSerializer())) + assert nested_serializer.serialize(dialect, [[1, 2], [3]]) == nested + + def test_unsupported(self): + with pytest.raises(RuntimeError, match="does not support ArraySerializer"): + ArraySerializer(IntegerSerializer()).serialize(Dialect(), [1, 2]) def test_rejects_non_sequence_and_bad_element(self): - s = ArraySerializer(IntegerSerializer()) + dialect = _literal(array="ARRAY[{}]") + serializer = ArraySerializer(IntegerSerializer()) with pytest.raises(TypeError): - s.serialize(PG, "not a list") + serializer.serialize(dialect, "not a list") with pytest.raises(TypeError): - s.serialize(PG, [1, "x"]) + serializer.serialize(dialect, [1, "x"]) def test_nested_unsupported_element(self): + dialect = _literal(array="ARRAY[{}]") with pytest.raises(RuntimeError, match="does not support BitSerializer"): - ArraySerializer(BitSerializer()).serialize(BigQuery, ["10"]) + ArraySerializer(BitSerializer()).serialize(dialect, ["10"]) class TestCustomSerializer: def test_receives_dialect_instance(self): - dialect = DuckDb - assert CustomSerializer(_custom_ok).serialize(dialect, 1) == "CUSTOM:1" + seen: list[Dialect] = [] - def test_must_return_str(self): - with pytest.raises(TypeError): - CustomSerializer(_custom_not_str).serialize(PG, "x") # pyright: ignore - - -def test_unsupported_serializer_error_message(): - with pytest.raises(RuntimeError, match="does not support BitSerializer"): - BitSerializer().serialize(Sqlite, "10") - with pytest.raises(RuntimeError, match="does not support ArraySerializer"): - ArraySerializer(IntegerSerializer()).serialize(MySql, [1, 2]) - with pytest.raises(RuntimeError, match="does not support InetSerializer"): - InetSerializer().serialize(Sqlite, "192.168.1.1") - with pytest.raises(RuntimeError, match="does not support MacAddressSerializer"): - MacAddressSerializer().serialize(DuckDb, "01:23:45:67:89:ab") + def render(dialect: Dialect, obj: typing.Any) -> str: + seen.append(dialect) + return f"CUSTOM:{obj}" + dialect = Dialect() + assert CustomSerializer(render).serialize(dialect, 1) == "CUSTOM:1" + assert seen == [dialect] -class TestLiteralGoldens: - def test_pg_string_uses_e_prefix(self): - assert StringSerializer().serialize(PG, "hello") == "E'hello'" - assert StringSerializer().serialize(PG, "a\nb") == "E'a\\nb'" - - def test_mssql_nvarchar_prefix(self): - assert StringSerializer().serialize(MsSql, "hi") == "N'hi'" + def test_must_return_str(self): + def render(dialect: Dialect, obj: typing.Any) -> int: + return 12345 - def test_pg_e_needed_for_backslash(self): - assert StringSerializer().serialize(PG, "a\\b") == "E'a\\\\b'" + with pytest.raises(TypeError): + CustomSerializer(render).serialize(Dialect(), "x") From beb7faff90b79dbcc936a90fa1e61aac3aae3f06 Mon Sep 17 00:00:00 2001 From: "A.Wolver.P" Date: Mon, 26 Oct 2026 14:26:53 +0330 Subject: [PATCH 11/11] Fix PyPy datetime capsule import --- Makefile | 3 +- pyproject.toml | 2 +- src/typeref.rs | 41 +++++-- uv.lock | 300 +++---------------------------------------------- 4 files changed, 49 insertions(+), 297 deletions(-) diff --git a/Makefile b/Makefile index 955df71..adb68e6 100644 --- a/Makefile +++ b/Makefile @@ -36,12 +36,13 @@ build-prod: format: uv run ruff check --fix . uv run ruff format . - cargo +nightly fmt + cargo +nightly fmt --all .PHONY: lint-python ## Lint python source files lint-python: uv run ruff check . uv run ruff format --check . + pyright rapidquery .PHONY: lint-rust ## Lint rust source files lint-rust: diff --git a/pyproject.toml b/pyproject.toml index d58703a..7e21aca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,8 +55,8 @@ dev = [ build = ["maturin == 1.15.0"] lint = [ - "mypy>=2.3.1", "pre-commit>=4.6.2", + "pyright>=1.1.414", "ruff>=0.16.9", ] test = [ diff --git a/src/typeref.rs b/src/typeref.rs index c45a99f..68c40fd 100644 --- a/src/typeref.rs +++ b/src/typeref.rs @@ -1,8 +1,5 @@ //! Interned `PyTypeObject*` cache. -// Dialect types -pub(crate) static mut DIALECT_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); - // Serializer types pub(crate) static mut SERIALIZER_TYPE: *mut pyo3::ffi::PyTypeObject = std::ptr::null_mut(); @@ -37,6 +34,7 @@ unsafe fn get_type_object_for(py: pyo3::Python) -> *mut pyo /// `PyImport_ImportModule` / dict lookup are not null-checked. A missing /// stdlib module currently yields a null pointer (and a later crash if /// dereferenced). Call only during module init, when the stdlib is present. +#[cfg_attr(feature = "nightly", optimize(size))] unsafe fn look_up_type_object( module_name: &std::ffi::CStr, member_name: &std::ffi::CStr, @@ -56,7 +54,34 @@ macro_rules! multiple_get_type_object_for { }; } +#[cfg(not(PyPy))] +#[cfg_attr(feature = "nightly", optimize(size))] +unsafe fn look_up_datetime() { + unsafe { + pyo3::ffi::PyDateTime_IMPORT(); + let datetime_capsule = + pyo3::ffi::PyCapsule_Import(c"datetime.datetime_CAPI".as_ptr(), 1).cast::(); + + debug_assert!(!datetime_capsule.is_null()); + + STD_DATETIME_TYPE = (*datetime_capsule).DateTimeType; + STD_DATE_TYPE = (*datetime_capsule).DateType; + STD_TIME_TYPE = (*datetime_capsule).TimeType; + } +} + +#[cfg(PyPy)] +#[cfg_attr(feature = "nightly", optimize(size))] +unsafe fn look_up_datetime() { + unsafe { + STD_DATETIME_TYPE = look_up_type_object(c"datetime", c"datetime"); + STD_DATE_TYPE = look_up_type_object(c"datetime", c"date"); + STD_TIME_TYPE = look_up_type_object(c"datetime", c"time"); + } +} + #[cold] +#[cfg_attr(feature = "nightly", optimize(size))] fn _initialize_typeref(py: pyo3::Python) { unsafe { // SAFETY: called once from [`initialize_typeref`] while the GIL is @@ -65,8 +90,6 @@ fn _initialize_typeref(py: pyo3::Python) { // are only read. multiple_get_type_object_for!( py, - // dialects - crate::dialect::PyDialect => DIALECT_TYPE, // serializers crate::serializers::PySerializer => SERIALIZER_TYPE, // exprs @@ -80,13 +103,7 @@ fn _initialize_typeref(py: pyo3::Python) { STD_DECIMAL_TYPE = look_up_type_object(c"decimal", c"Decimal"); STD_UUID_TYPE = look_up_type_object(c"uuid", c"UUID"); - pyo3::ffi::PyDateTime_IMPORT(); - let datetime_capsule = - pyo3::ffi::PyCapsule_Import(c"datetime.datetime_CAPI".as_ptr(), 1).cast::(); - - STD_DATETIME_TYPE = (*datetime_capsule).DateTimeType; - STD_DATE_TYPE = (*datetime_capsule).DateType; - STD_TIME_TYPE = (*datetime_capsule).TimeType; + look_up_datetime(); } } diff --git a/uv.lock b/uv.lock index 2059735..1298958 100644 --- a/uv.lock +++ b/uv.lock @@ -6,70 +6,6 @@ resolution-markers = [ "python_full_version < '3.15'", ] -[[package]] -name = "ast-serialize" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" }, - { url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" }, - { url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" }, - { url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" }, - { url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" }, - { url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" }, - { url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" }, - { url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" }, - { url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" }, - { url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" }, - { url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" }, - { url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" }, - { url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" }, - { url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" }, - { url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" }, - { url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" }, - { url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" }, - { url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" }, - { url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" }, - { url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" }, - { url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" }, - { url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" }, - { url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" }, - { url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" }, - { url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" }, - { url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" }, - { url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" }, - { url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" }, - { url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" }, - { url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" }, - { url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" }, - { url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" }, - { url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" }, -] - [[package]] name = "cfgv" version = "3.5.0" @@ -241,136 +177,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "librt" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/77/900294b3c9a81415323d81b3f3f271a7f5d7ed9d01e824fc38ffa49470b6/librt-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6", size = 148668, upload-time = "2026-09-29T00:44:20.282Z" }, - { url = "https://files.pythonhosted.org/packages/55/3b/c301e8cc47758e4518b93e926a0111080155cc79fa03651d202ac2025065/librt-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978", size = 153562, upload-time = "2026-09-29T00:44:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/62/b1/4d5b98114a58bf17e3832cd33400e40f2582f68c2fb7ce25b4468cb1afd3/librt-0.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd", size = 494337, upload-time = "2026-09-29T00:44:23.096Z" }, - { url = "https://files.pythonhosted.org/packages/88/36/14223c9b0dc38982eb4f399732c8cb27b19c7e027ae0faef5ca51d08d19f/librt-0.16.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144", size = 485440, upload-time = "2026-09-29T00:44:24.555Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/d7155e6800d0c33e297e529b217649f7361d706491ebf9d2fc0fc627309e/librt-0.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671", size = 498363, upload-time = "2026-09-29T00:44:26.03Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ce/d5f1fb9f913ac0979d6fc5db9fb9c0bf28cc3470988528957922f15ed40c/librt-0.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262", size = 515453, upload-time = "2026-09-29T00:44:27.566Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d4/ca0fca1a27c26080d1a25b2b31f271e532078a5719878c3daca0c3d4c539/librt-0.16.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a", size = 509537, upload-time = "2026-09-29T00:44:29.14Z" }, - { url = "https://files.pythonhosted.org/packages/ee/c0/26807bf2221a53a2c66f2e23a5c4d57f1c0eaa4afed7cb81d841cf3d1903/librt-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c", size = 532541, upload-time = "2026-09-29T00:44:30.745Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/c329d509907e1bc061abeec3796f94d255d43ba25d28c3854d2138ed5ab2/librt-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726", size = 537059, upload-time = "2026-09-29T00:44:32.102Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/4571b9c9e65d98e5503734166dce2fd3e216f526dc3c59a3773faa8a60e3/librt-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a", size = 536614, upload-time = "2026-09-29T00:44:33.653Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d3/a2ed8a292e4ab7889792a9c3f4ad5d1caf6557afce7f6b452e77f8ed20b7/librt-0.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1", size = 517199, upload-time = "2026-09-29T00:44:35.115Z" }, - { url = "https://files.pythonhosted.org/packages/9e/24/276e5218b4b7adb6258a42b7403cb1b7cef25616238d0c2152288a9d2f53/librt-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143", size = 558746, upload-time = "2026-09-29T00:44:36.656Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5c/9156df81a1726e1efc9567379def2b8777a6d56f464d2f1b760bf951b8af/librt-0.16.0-cp310-cp310-win32.whl", hash = "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba", size = 104424, upload-time = "2026-09-29T00:44:38.123Z" }, - { url = "https://files.pythonhosted.org/packages/0b/62/4fd172cb3f49ab2cf5ef17de519a93cfb935202c1270ba9d44c6a64178c3/librt-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155", size = 124267, upload-time = "2026-09-29T00:44:39.455Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" }, - { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" }, - { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" }, - { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" }, - { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" }, - { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" }, - { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" }, - { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" }, - { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" }, - { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" }, - { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, - { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, - { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, - { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, - { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, - { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, - { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, - { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, - { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, - { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, - { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" }, - { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" }, - { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" }, - { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" }, - { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" }, - { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" }, - { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" }, - { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" }, - { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" }, - { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" }, - { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" }, - { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" }, - { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" }, - { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" }, - { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" }, - { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, - { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, - { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, - { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, - { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, - { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, - { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, - { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, - { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, - { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, - { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, - { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, - { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, - { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, - { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, - { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, - { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, - { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, - { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, - { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, - { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, - { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, - { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, - { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, - { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, - { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, - { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, - { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, - { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, - { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, - { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, - { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, - { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, - { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, - { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, - { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, -] - [[package]] name = "maturin" version = "1.15.0" @@ -395,82 +201,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" }, ] -[[package]] -name = "mypy" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ast-serialize" }, - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/0a/78c9ac93933e2ecd51d807021f78c03ffe98640f33951a8be159f5fd55bc/mypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffda5244fd1ad71a1e54405e35f50d09b80c3978efa120f58bd1252ae32c62d2", size = 14408618, upload-time = "2026-10-01T20:39:46.766Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4f/5f5f4429fd5a4218ce6268164a66afeed75c7e2ce5522a4d163faaa5fe85/mypy-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:615b03922d40e186fd1df73156473db0ba525c639bb4fd1cf28e1694879c9b23", size = 14552749, upload-time = "2026-10-01T20:40:08.325Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/fa1c319cd51db7a6027b46117460d5a361c6bba0c0f0f0670a2dab8ce2a5/mypy-2.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13fa24f439c0e48a290a3922fa14ccd22f2762bae99d2142931b3e40a9055080", size = 15505846, upload-time = "2026-10-01T20:39:15.475Z" }, - { url = "https://files.pythonhosted.org/packages/e2/5a/14c02690776fb04e9fa60b81173cb9ae6315e0745fad3813b303737f2f0c/mypy-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3adef556a19eb3b630bf86a79c29d7da3d61e541472d0d897ca01b171c0abf8f", size = 15823059, upload-time = "2026-10-01T20:39:35.429Z" }, - { url = "https://files.pythonhosted.org/packages/76/d8/d29afa819f8cb765018d6e2773e390a41ea4592f75abe417135969646857/mypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:1dc0f64b0a92ae27a49d2175f0bacfa56e15bdc5b420cf92e0c1f79292219cbb", size = 11312927, upload-time = "2026-10-01T20:39:28.509Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c8/966dec4d80303855d824e6bf67fe1da812d88adfe4d75e24d8545dab4826/mypy-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a378fc15fb33e321f04652c166ce73eeb8833a97c3d218132844e938cd93220", size = 10879252, upload-time = "2026-10-01T20:38:41.545Z" }, - { url = "https://files.pythonhosted.org/packages/26/67/e53965e67d23d48e78dd603aa7525a8d77e7818c23954cc217ccde5cd5c0/mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd", size = 14315745, upload-time = "2026-10-01T20:39:25.03Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5d/a588ccd57625822d8484d7aff4e9c70f88d042a91c2ec84324a2bc14b3f3/mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74", size = 14465652, upload-time = "2026-10-01T20:38:57.19Z" }, - { url = "https://files.pythonhosted.org/packages/05/af/b4978a5566cb829ca563dc08e3ad8d944f116078786ad54ba3dff2bcb64d/mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1", size = 15419180, upload-time = "2026-10-01T20:40:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8c/52c71f31c54d0a749d832cc73b933c6e38c52d9b270d5503e031c73723dc/mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4", size = 15669212, upload-time = "2026-10-01T20:39:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/e8/55/ba63121494404fd7d3787ec176ccd02820d6ec125ac4e7c4c0b79f2cc6c7/mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b", size = 11299572, upload-time = "2026-10-01T20:40:03.627Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9c/385c81b5c0d3a2917f077c4da660d0493f9521ccfd4b058e33b860f72d02/mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd", size = 10869751, upload-time = "2026-10-01T20:40:25.483Z" }, - { url = "https://files.pythonhosted.org/packages/68/ed/e5d7cf4017e74a1c1e1c4058ce8f614fc1e3e7606564f47166e22bfc9f95/mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e", size = 14378593, upload-time = "2026-10-01T20:38:48.222Z" }, - { url = "https://files.pythonhosted.org/packages/30/7d/12d994886a922f0f1997becc9c6625198d61eeb5962558a886af7dd38d54/mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90", size = 14464697, upload-time = "2026-10-01T20:39:11.059Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9e/bcc9af755425ad17790bf11d73c2ea7592914cf309ee1340997670f9d57a/mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e", size = 15552257, upload-time = "2026-10-01T20:39:56.269Z" }, - { url = "https://files.pythonhosted.org/packages/af/0c/3343fc4525d6f00d75ad17a93a9f052d5163641cb8911840d4a12cb59ff2/mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1", size = 15800443, upload-time = "2026-10-01T20:38:54.815Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0e/69aac6b8159da7c53e6115a2be1bf48e85503402cadbf506dd708e8b2ad7/mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946", size = 11381574, upload-time = "2026-10-01T20:39:49.406Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d3/d32ce4feb5993eec09d2024bef16cedc9b93701b1446b86092f08b24491b/mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989", size = 10944729, upload-time = "2026-10-01T20:40:15.203Z" }, - { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" }, - { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" }, - { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" }, - { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" }, - { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" }, - { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" }, - { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" }, - { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" }, - { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" }, - { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" }, - { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" }, - { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" }, - { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" }, - { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" }, - { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" }, - { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" }, - { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" }, - { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" }, - { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" }, - { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" }, - { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" }, - { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" }, - { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" }, - { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "nodeenv" version = "1.11.0" @@ -561,15 +291,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] -[[package]] -name = "pathspec" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, -] - [[package]] name = "platformdirs" version = "4.12.3" @@ -613,6 +334,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] +[[package]] +name = "pyright" +version = "1.1.414" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -736,15 +470,15 @@ build = [ dev = [ { name = "hypothesis" }, { name = "maturin" }, - { name = "mypy" }, { name = "pre-commit" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, ] lint = [ - { name = "mypy" }, { name = "pre-commit" }, + { name = "pyright" }, { name = "ruff" }, ] test = [ @@ -762,15 +496,15 @@ build = [{ name = "maturin", specifier = "==1.15.0" }] dev = [ { name = "hypothesis", specifier = ">=6.0.0" }, { name = "maturin", specifier = "==1.15.0" }, - { name = "mypy", specifier = ">=2.3.1" }, { name = "pre-commit", specifier = ">=4.6.2" }, + { name = "pyright", specifier = ">=1.1.414" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ruff", specifier = ">=0.16.9" }, ] lint = [ - { name = "mypy", specifier = ">=2.3.1" }, { name = "pre-commit", specifier = ">=4.6.2" }, + { name = "pyright", specifier = ">=1.1.414" }, { name = "ruff", specifier = ">=0.16.9" }, ] test = [