diff --git a/Makefile b/Makefile index bfa9410..0a2f633 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ test-python: test: test-rust test-python .PHONY: all ## Run the standard set of checks performed in CI -all: format lint build-dev test +all: format build-dev lint test .PHONY: clean ## Clear local caches and build artifacts clean: diff --git a/rapidquery-macros/cust_protocol.rs b/rapidquery-macros/cust_protocol.rs index 36ee4d1..7f94aed 100644 --- a/rapidquery-macros/cust_protocol.rs +++ b/rapidquery-macros/cust_protocol.rs @@ -97,7 +97,7 @@ pub fn expand(input: TokenStream) -> syn::Result { 1 => Some(pyo3::Bound::from_owned_ptr(#bound_object.py(), attribute)), 0 => None, _ => { - return Err(pyo3::PyErr::fetch(#bound_object.py())) + return Err(pyo3::PyErr::fetch(#bound_object.py()).into()) } } }; @@ -109,7 +109,7 @@ pub fn expand(input: TokenStream) -> syn::Result { if attribute.is_null() { if pyo3::ffi::PyErr_ExceptionMatches(pyo3::ffi::PyExc_AttributeError) != 1 { - return Err(pyo3::PyErr::fetch(#bound_object.py())); + return Err(pyo3::PyErr::fetch(#bound_object.py()).into()); } else { pyo3::ffi::PyErr_Clear(); None diff --git a/rapidquery-macros/lib.rs b/rapidquery-macros/lib.rs index 419118f..022977a 100644 --- a/rapidquery-macros/lib.rs +++ b/rapidquery-macros/lib.rs @@ -3,7 +3,6 @@ use proc_macro::TokenStream; mod cust_protocol; mod define_options; mod pyclass; -mod pyerr; mod pyo3_constructor; /// Defines a pyo3 class with a required module path. @@ -25,20 +24,6 @@ pub fn pyclass(attr: TokenStream, item: TokenStream) -> TokenStream { } } -/// Builds a [`PyErr`], or `Err(PyErr)` when the name is wrapped in `Err`. -/// -/// ```ignore -/// pyerr!(PyTypeError, "bad {}", x) -/// pyerr!(Err(PyValueError)) -/// ``` -#[proc_macro] -pub fn pyerr(input: TokenStream) -> TokenStream { - match pyerr::expand(input.into()) { - Ok(tokens) => tokens.into(), - Err(err) => err.to_compile_error().into(), - } -} - /// Builds a customization protocol checking. /// /// Note: you can access to attribute result using `protocol` variable. diff --git a/rapidquery-macros/pyerr.rs b/rapidquery-macros/pyerr.rs deleted file mode 100644 index a2d20b3..0000000 --- a/rapidquery-macros/pyerr.rs +++ /dev/null @@ -1,85 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::parse::Parse; -use syn::parse::ParseStream; - -struct PyErrInput { - return_err_enum: bool, - name: syn::Ident, - msg_or_fmt: Option, - format_args: Vec, -} - -impl Parse for PyErrInput { - /// Acceptable formats: - /// - `#name` - /// - `Err(#name)` - /// - `Err(#name), #msg` - /// - `Err(#name), #fmt, #args...` - /// - `#name, #msg` - /// - `#name, #fmt, #args...` - fn parse(input: ParseStream<'_>) -> syn::Result { - let ident: syn::Ident = input.parse()?; - let (wrap_in_err, name) = if ident == "Err" && input.peek(syn::token::Paren) { - let inner; - syn::parenthesized!(inner in input); - let name: syn::Ident = inner.parse()?; - inner.parse::()?; - (true, name) - } else { - (false, ident) - }; - - let mut slf = Self { - return_err_enum: wrap_in_err, - name, - msg_or_fmt: None, - format_args: vec![], - }; - - if input.is_empty() { - return Ok(slf); - } - - input.parse::()?; - - slf.msg_or_fmt = Some(input.parse()?); - - while input.peek(syn::token::Comma) { - input.parse::()?; - - if input.is_empty() { - break; - } - - slf.format_args.push(input.parse()?); - } - - Ok(slf) - } -} - -pub fn expand(input: TokenStream) -> syn::Result { - let PyErrInput { - return_err_enum, - name, - msg_or_fmt, - format_args, - } = syn::parse2(input)?; - - let constructed = match (msg_or_fmt, format_args.is_empty()) { - (Some(a), false) => { - quote! { ::pyo3::exceptions::#name::new_err(format!(#a, #(#format_args),*)) } - } - (Some(a), true) => { - quote! { ::pyo3::exceptions::#name::new_err(#a) } - } - (None, _) => quote! { ::pyo3::exceptions::#name::new_err(()) }, - }; - - if return_err_enum { - Ok(quote! { Err(#constructed) }) - } else { - Ok(constructed) - } -} diff --git a/rapidquery-macros/pyo3_constructor.rs b/rapidquery-macros/pyo3_constructor.rs index 1cd7f05..251f9e6 100644 --- a/rapidquery-macros/pyo3_constructor.rs +++ b/rapidquery-macros/pyo3_constructor.rs @@ -292,6 +292,14 @@ fn screaming_snake(s: &str) -> String { out } +/// Single-segment type name used for `OPT_*` constants. +fn enum_type_ident(ty: &Type) -> &Ident { + match ty { + Type::Path(type_name) => type_name.path.get_ident().expect("enum/bits type is a single ident"), + _ => panic!("enum/bits type is a single ident"), + } +} + /// Name of the exported module constant: `OPT__`. fn const_name(field: &Ident, item: &Ident) -> String { format!( @@ -362,31 +370,53 @@ impl Field { /// 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::Enum { ty, variants } => { + let type_ident = enum_type_ident(ty); + let names: Vec<_> = variants.iter().map(|v| const_name(type_ident, v)).collect(); + let values = 0..variants.len() as u8; + quote! { + match #value { + #( __x if __x == <#ty>::#variants as u8 => <#ty>::#variants, )* + __x => return ::core::result::Result::Err( + error::Error::InvalidEnumOptionField { + field: #name_str, + value: __x, + expected: error::format_options(&[#(#names),*], &[#(#values),*]), + }, + ), + } } - }, + } - Kind::Bits { ty, .. } => quote! { - <#ty>::from_bits(#value).ok_or_else(|| { - #value_error::new_err(format!("invalid bits {:#b} for `{}`", #value, #name_str)) - })? - }, + Kind::Bits { ty, flags } => { + let type_ident = enum_type_ident(ty); + let names: Vec<_> = flags.iter().map(|flag| const_name(type_ident, flag)).collect(); + quote! { + <#ty>::from_bits(#value).ok_or_else(|| { + error::Error::InvalidBitOptionField { + field: #name_str, + value: #value, + expected: error::format_options( + &[#(#names),*], + &[#(<#ty>::#flags.bits()),*], + ), + } + })? + } + } 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()))? + .map_err(|reason| error::Error::InvalidTemplate { + field: #name_str, + reason, + })? } } } @@ -605,7 +635,7 @@ pub fn expand(input: TokenStream) -> syn::Result { #[cold] fn #export_fn( module: &::pyo3::Bound<'_, ::pyo3::types::PyModule>, - ) -> ::pyo3::PyResult<()> { + ) -> pyo3::PyResult<()> { use ::pyo3::types::PyModuleMethods; #(#exports)* Ok(()) @@ -620,7 +650,7 @@ pub fn expand(input: TokenStream) -> syn::Result { clippy::fn_params_excessive_bools, clippy::cognitive_complexity )] - fn __new__(#(#params),*) -> ::pyo3::PyResult { + fn __new__(#(#params),*) -> error::Result { Ok(Self { #(#inits),* }) } diff --git a/src/data_type/mod.rs b/src/data_type/mod.rs index b6e1f78..940520f 100644 --- a/src/data_type/mod.rs +++ b/src/data_type/mod.rs @@ -1,3 +1,4 @@ +use crate::error; use crate::sql; #[cfg(test)] @@ -187,7 +188,7 @@ impl std::fmt::Display for DataTypeName { } impl sql::WriteSql for DataTypeName { - fn write_sql(&self, _dialect: &crate::dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, _dialect: &crate::dialect::PyDialect, out: &mut W) -> error::Result<()> { out.write_str(self.as_str()); Ok(()) } diff --git a/src/ddl/schema.rs b/src/ddl/schema.rs index ba7f9d1..ae1c08a 100644 --- a/src/ddl/schema.rs +++ b/src/ddl/schema.rs @@ -1,4 +1,5 @@ use crate::dialect; +use crate::error; use crate::expr::enum_::Ident; use crate::expr::extractors::IdentExtractor; use crate::internal::alias; @@ -24,12 +25,12 @@ pub struct PyCreateSchema { } impl sql::WriteSql for PyCreateSchema { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { if !dialect.schema.features.contains(dialect::SchemaFeatures::SUPPORTED) { - return Err(rapidquery_macros::pyerr!( - PyRuntimeError, - "CREATE SCHEMA statement is not supported by this dialect" - )); + return Err(error::Error::Unsupported { + feature: "CREATE SCHEMA is not supported by this dialect", + fix: "set schema.features to include OPT_SCHEMA_FEATURES_SUPPORTED", + }); } // CREATE {SCHEMA | DATABASE} @@ -49,10 +50,10 @@ impl sql::WriteSql for PyCreateSchema { match dialect.schema.authorization { dialect::SchemaAuthorization::Unsupported => { if self.authorization.is_some() { - return Err(rapidquery_macros::pyerr!( - PyRuntimeError, - "authorization is not supported in CREATE SCHEMA by this dialect" - )); + return Err(error::Error::Unsupported { + feature: "authorization is not supported in CREATE SCHEMA by this dialect", + fix: "set schema.authorization to OPT_SCHEMA_AUTHORIZATION_OPTIONAL", + }); } self.name.write_sql(dialect, out)?; @@ -70,10 +71,10 @@ impl sql::WriteSql for PyCreateSchema { if let Some(x) = &self.location { // ... {LOCATION | MANAGED LOCATION | EXTERNAL_VOLUME =} let Some(tmpl) = &dialect.schema.location else { - return Err(rapidquery_macros::pyerr!( - PyRuntimeError, - "location is not supported in CREATE SCHEMA by this dialect" - )); + return Err(error::Error::Unsupported { + feature: "location is not supported in CREATE SCHEMA by this dialect", + fix: "set schema.location to a template such as \" LOCATION {}\"", + }); }; out.write(' '); @@ -124,7 +125,7 @@ impl PyCreateSchema { } /// Render this expression as raw SQL (values inlined as literals). - fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> pyo3::PyResult { + fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> error::Result { sql::write_sql(dialect.py(), self, dialect.get()) } @@ -166,12 +167,12 @@ pub struct PyDropSchema { } impl sql::WriteSql for PyDropSchema { - fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> error::Result<()> { if !dialect.schema.features.contains(dialect::SchemaFeatures::SUPPORTED) { - return Err(rapidquery_macros::pyerr!( - PyRuntimeError, - "DROP SCHEMA statement is not supported by this dialect" - )); + return Err(error::Error::Unsupported { + feature: "DROP SCHEMA is not supported by this dialect", + fix: "set schema.features to include OPT_SCHEMA_FEATURES_SUPPORTED", + }); } // DROP {SCHEMA | DATABASE} @@ -195,20 +196,20 @@ impl sql::WriteSql for PyDropSchema { None => {} Some(DropSchemaBehavior::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" - )); + return Err(error::Error::Unsupported { + feature: "CASCADE is not supported in DROP SCHEMA by this dialect", + fix: "set schema.features to include OPT_SCHEMA_FEATURES_DROP_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" - )); + return Err(error::Error::Unsupported { + feature: "RESTRICT is not supported in DROP SCHEMA by this dialect", + fix: "set schema.features to include OPT_SCHEMA_FEATURES_DROP_RESTRICT", + }); } out.write_str(" RESTRICT"); @@ -227,7 +228,7 @@ impl PyDropSchema { name: IdentExtractor, if_exists: bool, behavior: Option<&str>, - ) -> pyo3::PyResult> { + ) -> error::Result> { use std::str::FromStr; let result = Self { @@ -237,7 +238,7 @@ impl PyDropSchema { None => None, Some(x) => Some( DropSchemaBehavior::from_str(x) - .map_err(|_| rapidquery_macros::pyerr!(PyValueError, "invalid value for behavior: {}", x))?, + .map_err(|_| error::Error::InvalidSchemaBehavior { value: x.to_owned() })?, ), }, }; @@ -260,7 +261,7 @@ impl PyDropSchema { } /// Render this expression as raw SQL (values inlined as literals). - fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> pyo3::PyResult { + fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> error::Result { sql::write_sql(dialect.py(), self, dialect.get()) } diff --git a/src/dialect/base.rs b/src/dialect/base.rs index 0d42fa4..6517404 100644 --- a/src/dialect/base.rs +++ b/src/dialect/base.rs @@ -1,5 +1,6 @@ #![allow(clippy::redundant_field_names)] +use crate::error; use crate::internal::template; #[rapidquery_macros::pyclass("rapidquery.dialect.LikeConfig", from_py_object, eq, hash)] diff --git a/src/dialect/errors.rs b/src/dialect/errors.rs deleted file mode 100644 index 34972ba..0000000 --- a/src/dialect/errors.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::fmt; - -// FIXME: Implement a unique error enum for project - -/// Formatting / serializing errors -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FormatError { - /// 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 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::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/src/dialect/escape.rs b/src/dialect/escape.rs index 93e92bc..e4af349 100644 --- a/src/dialect/escape.rs +++ b/src/dialect/escape.rs @@ -1,8 +1,8 @@ +use crate::error; use crate::internal::template; use crate::internal::writer; use super::base; -use super::errors; impl base::LiteralConfig { /// Write a boolean literal (`TRUE`/`FALSE` or `1`/`0`). @@ -29,7 +29,7 @@ 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 W, bits: &str) -> Result<(), errors::FormatError> { + pub fn write_bits(&self, writer: &mut W, bits: &str) -> error::Result<()> { // FIXME: return an error for invalid bits debug_assert!( bits.chars().all(|c| c == '0' || c == '1'), @@ -40,7 +40,10 @@ impl base::LiteralConfig { template.write_to(writer, |_, out| out.write_str(bits)); Ok(()) } else { - Err(errors::FormatError::Unsupported("BitSerializer")) + Err(error::Error::Unsupported { + feature: "bit strings are not supported by this dialect", + fix: "set literal.bit to a template such as \"B'{}'\"", + }) } } @@ -70,13 +73,11 @@ impl base::LiteralConfig { } #[pyo3(signature = (value, /))] - fn serialize_bits(&self, value: &str) -> pyo3::PyResult { + fn serialize_bits(&self, value: &str) -> error::Result { 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 - ); + return Err(error::Error::InvalidBitString { + value: value.to_owned(), + }); } let mut buf = String::new(); @@ -97,14 +98,10 @@ 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 W, - raw: &str, - ) -> Result<(), errors::FormatError> { + pub fn write_string_literal(&self, writer: &mut W, raw: &str) -> error::Result<()> { // FIXME: we can accept NUL on some dialects if raw.contains('\0') { - return Err(errors::FormatError::NulInString); + return Err(error::Error::NulInString); } self.template.write_to( @@ -134,7 +131,7 @@ impl base::StringConfig { #[pyo3::pymethods] impl base::StringConfig { #[pyo3(signature = (value, /))] - fn serialize_string(&self, value: &str) -> pyo3::PyResult { + fn serialize_string(&self, value: &str) -> error::Result { let mut buf = String::new(); self.write_string_literal(&mut buf, value)?; Ok(buf) @@ -354,6 +351,7 @@ fn write_backslash_unicode(writer: &mut W, string: &str, e mod tests { use crate::dialect::PyDialect; use crate::dialect::StringConfig; + use crate::error; use crate::internal::template::Template; const KEYS: &[(&str, bool)] = &[ @@ -430,7 +428,7 @@ mod tests { ("unicode_quote", false), ]; - fn render_string(tmpl: &str, raw: &str) -> Result { + fn render_string(tmpl: &str, raw: &str) -> error::Result { let c = StringConfig { template: Template::parse(tmpl, STRING_KEYS, 1, 1).unwrap(), ..Default::default() @@ -478,9 +476,9 @@ mod tests { #[test] fn string_rejects_nul() { - assert_eq!( + assert!(matches!( render_string("'{single}'", "a\0b"), - Err(crate::dialect::errors::FormatError::NulInString) - ); + Err(error::Error::NulInString) + )); } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 12f5d1a..d71c5bd 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1,5 +1,3 @@ -pub mod errors; - mod base; mod escape; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..5f0c67e --- /dev/null +++ b/src/error.rs @@ -0,0 +1,164 @@ +use crate::expr::enum_::IdentParseError; +use crate::internal::template::TemplateParseError; + +// Used by rapidquery_macros::pyo3_constructor +pub(crate) fn format_options(names: &[&str], values: &[u8]) -> String { + let parts: Vec = names + .iter() + .zip(values) + .map(|(name, value)| format!("{name} ({value})")) + .collect(); + + match parts.as_slice() { + [] => String::new(), + [one] => one.clone(), + [left, right] => format!("{left} or {right}"), + [head @ .., last] => format!("{}, or {last}", head.join(", ")), + } +} + +/// Project error. Convert it with `?` from a function that returns `Result`. +#[derive(Debug, derive_more::Display)] +pub enum Error { + /// A Python exception that already occurred (`PyErr::fetch`). Re-raised as-is. + #[display("{_0}")] + Python(pyo3::PyErr), + + /// `expected` is the requirement. `got` is the Python type name that arrived. + #[display("{expected}, got {got}")] + Expected { expected: &'static str, got: String }, + + /// `feature` says what this dialect cannot do. `fix` says which field to change. + #[display("{feature} (hint: {fix})")] + Unsupported { feature: &'static str, fix: &'static str }, + + #[display("invalid template for `{field}`: {reason}")] + InvalidTemplate { + field: &'static str, + reason: TemplateParseError, + }, + + #[display("{_0}")] + Ident(IdentParseError), + + #[display("string literal contains a NUL byte; a SQL string literal cannot carry NUL")] + NulInString, + + #[display("expected a string of '0' and '1' for bit serialization, got {value:?}")] + InvalidBitString { value: String }, + + #[display("cannot serialize non-finite float {value} as a numeric literal")] + NonFiniteFloat { value: f64 }, + + #[display("invalid uuid str: {detail}")] + InvalidUuid { detail: String }, + + #[display("invalid inet address {text:?}: {detail}")] + InvalidInet { text: String, detail: String }, + + #[display("invalid mac address {text:?}: {detail}")] + InvalidMac { text: String, detail: String }, + + #[display("LIKE ANY pattern list is empty; pass at least one pattern")] + EmptyLikeAny, + + #[display("unknown unary operator {operator:?}; expected NOT, +, -, or ~")] + UnknownUnaryOperator { operator: String }, + + #[display("unknown IS kind {kind:?}; expected NULL, TRUE, FALSE, or UNKNOWN")] + UnknownIsKind { kind: String }, + + #[display("unknown cast kind {kind:?}; expected CAST, TRY_CAST, or ::")] + UnknownCastKind { kind: String }, + + #[display("unknown TRIM where {kind:?}; expected BOTH, LEADING, or TRAILING")] + UnknownTrimWhere { kind: String }, + + #[display("invalid value {value} for `{field}`; expected {expected}")] + InvalidEnumOptionField { + field: &'static str, + value: u8, + expected: String, + }, + + #[display("invalid bits {value:#b} for `{field}`; expected a combination of {expected}")] + InvalidBitOptionField { + field: &'static str, + value: u8, + expected: String, + }, + + #[display("invalid value {value:?} for behavior; expected CASCADE or RESTRICT")] + InvalidSchemaBehavior { value: String }, + + #[display("custom expression has {placeholders} placeholders but {arguments} arguments")] + CustomPlaceholderCount { placeholders: usize, arguments: usize }, + + #[display( + "collation must be a single quoted or unquoted name when operator.collate is OPT_OPERATOR_COLLATE_FUNCTION_STRING" + )] + CollationNotSingleName, + + #[display("identifier index {index} is out of range (length {len})")] + IdentIndex { index: isize, len: usize }, + + #[display("integer is out of range for datetime serialization")] + DatetimeOutOfRange, + + #[display("timestamp {value} is out of range for datetime serialization")] + InvalidTimestamp { value: String }, +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Ident(err) => Some(err), + Self::InvalidTemplate { reason, .. } => Some(reason), + _ => None, + } + } +} + +impl From for Error { + fn from(err: IdentParseError) -> Self { + Self::Ident(err) + } +} + +impl From for Error { + fn from(reason: TemplateParseError) -> Self { + Self::InvalidTemplate { + field: "template", + reason, + } + } +} + +impl From for Error { + fn from(err: pyo3::PyErr) -> Self { + Self::Python(err) + } +} + +impl From> for Error { + fn from(err: pyo3::CastIntoError<'_>) -> Self { + pyo3::PyErr::from(err).into() + } +} + +impl From for pyo3::PyErr { + fn from(err: Error) -> Self { + match err { + Error::Python(err) => err, + err @ Error::Expected { .. } => pyo3::exceptions::PyTypeError::new_err(err.to_string()), + err @ Error::Unsupported { .. } => pyo3::exceptions::PyNotImplementedError::new_err(err.to_string()), + err @ Error::IdentIndex { .. } => pyo3::exceptions::PyIndexError::new_err(err.to_string()), + err @ (Error::DatetimeOutOfRange | Error::InvalidTimestamp { .. }) => { + pyo3::exceptions::PyOverflowError::new_err(err.to_string()) + } + err => pyo3::exceptions::PyValueError::new_err(err.to_string()), + } + } +} + +pub type Result = ::core::result::Result; diff --git a/src/expr/base.rs b/src/expr/base.rs index ae30d2f..0c58683 100644 --- a/src/expr/base.rs +++ b/src/expr/base.rs @@ -3,6 +3,7 @@ //! [`PyExpr`] holds [`ExprInner`]. Concrete nodes (`Ident`, `Binary`, …) are //! empty frozen subclasses. Shared methods live on the base. +use crate::error; use crate::internal::alias; use crate::internal::utils; use crate::sql; @@ -58,7 +59,7 @@ impl PyExpr { } /// `None`, exact `bool`, or exact `str` → an `IS` expression. -fn is_from_kind<'py>(expr: enum_::ExprInner, kind: &alias::BoundObject<'py>, negated: bool) -> pyo3::PyResult { +fn is_from_kind<'py>(expr: enum_::ExprInner, kind: &alias::BoundObject<'py>, negated: bool) -> error::Result { let ptr = kind.as_ptr(); // SAFETY: `ptr` is a live borrowed object and this runs with the GIL held. @@ -78,11 +79,10 @@ fn is_from_kind<'py>(expr: enum_::ExprInner, kind: &alias::BoundObject<'py>, neg return super::nodes::PyIsExpr::new_base(expr, text, negated); } - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected None, bool, or str, got {}", - utils::get_type_name(kind.py(), ptr) - ) + Err(error::Error::Expected { + expected: "expected None, bool, or str for an IS expression", + got: utils::get_type_name(kind.py(), ptr), + }) } } @@ -389,12 +389,8 @@ impl PyExpr { // ===== Is ===== /// e.g. `a IS [kind]` - fn is_<'py>( - &self, - py: pyo3::Python<'py>, - kind: alias::BoundObject<'py>, - ) -> pyo3::PyResult> { - is_from_kind(self.0.clone(), &kind, false).and_then(|x| x.into_subclasses(py)) + fn is_<'py>(&self, py: pyo3::Python<'py>, kind: alias::BoundObject<'py>) -> error::Result> { + is_from_kind(self.0.clone(), &kind, false).and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } /// e.g. `a IS NOT [kind]` @@ -402,8 +398,8 @@ impl PyExpr { &self, py: pyo3::Python<'py>, kind: alias::BoundObject<'py>, - ) -> pyo3::PyResult> { - is_from_kind(self.0.clone(), &kind, true).and_then(|x| x.into_subclasses(py)) + ) -> error::Result> { + is_from_kind(self.0.clone(), &kind, true).and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } // ===== IsDistinctFrom ===== @@ -450,11 +446,11 @@ impl PyExpr { &self, pattern: alias::BoundObject<'py>, escape: Option, - ) -> pyo3::PyResult> { + ) -> error::Result> { let py = pattern.py(); super::nodes::PyLikeExpr::new_base(self.0.clone(), pattern, false, true, escape) - .and_then(|x| x.into_subclasses(py)) + .and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } /// e.g. `a NOT LIKE 'a%'` @@ -463,11 +459,11 @@ impl PyExpr { &self, pattern: alias::BoundObject<'py>, escape: Option, - ) -> pyo3::PyResult> { + ) -> error::Result> { let py = pattern.py(); super::nodes::PyLikeExpr::new_base(self.0.clone(), pattern, true, true, escape) - .and_then(|x| x.into_subclasses(py)) + .and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } /// e.g. `a ILIKE 'a%'` @@ -476,11 +472,11 @@ impl PyExpr { &self, pattern: alias::BoundObject<'py>, escape: Option, - ) -> pyo3::PyResult> { + ) -> error::Result> { let py = pattern.py(); super::nodes::PyLikeExpr::new_base(self.0.clone(), pattern, false, false, escape) - .and_then(|x| x.into_subclasses(py)) + .and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } /// e.g. `a NOT ILIKE 'a%'` @@ -489,11 +485,11 @@ impl PyExpr { &self, pattern: alias::BoundObject<'py>, escape: Option, - ) -> pyo3::PyResult> { + ) -> error::Result> { let py = pattern.py(); super::nodes::PyLikeExpr::new_base(self.0.clone(), pattern, true, false, escape) - .and_then(|x| x.into_subclasses(py)) + .and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } // ===== SimilarTo / RLike ===== @@ -537,14 +533,15 @@ impl PyExpr { // ===== Cast ===== /// e.g. `CAST(a AS int)` - fn cast_as<'py>(&self, py: pyo3::Python<'py>, data_type_name: &str) -> pyo3::PyResult> { - super::nodes::PyCastExpr::new_base(self.0.clone(), data_type_name, "CAST").and_then(|x| x.into_subclasses(py)) + fn cast_as<'py>(&self, py: pyo3::Python<'py>, data_type_name: &str) -> error::Result> { + super::nodes::PyCastExpr::new_base(self.0.clone(), data_type_name, "CAST") + .and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } /// e.g. `TRY_CAST(a AS int)` - fn try_cast_as<'py>(&self, py: pyo3::Python<'py>, data_type_name: &str) -> pyo3::PyResult> { + fn try_cast_as<'py>(&self, py: pyo3::Python<'py>, data_type_name: &str) -> error::Result> { super::nodes::PyCastExpr::new_base(self.0.clone(), data_type_name, "TRY_CAST") - .and_then(|x| x.into_subclasses(py)) + .and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } // ===== Collate / Trim ===== @@ -565,16 +562,16 @@ impl PyExpr { py: pyo3::Python<'py>, r#where: &str, characters: Option, - ) -> pyo3::PyResult> { + ) -> error::Result> { super::nodes::PyTrimExpr::new_base(self.0.clone(), r#where, characters.map(|x| x.0)) - .and_then(|x| x.into_subclasses(py)) + .and_then(|x| x.into_subclasses(py).map_err(error::Error::from)) } /// Render this expression as a parameterized statement. fn prepare<'py>( &self, dialect: pyo3::Bound<'py, crate::dialect::PyDialect>, - ) -> pyo3::PyResult<(String, alias::BoundObject<'py>)> { + ) -> error::Result<(String, alias::BoundObject<'py>)> { let (sql, params) = sql::prepare(dialect.py(), &self.0, dialect.get())?; let params = unsafe { @@ -588,11 +585,11 @@ impl PyExpr { } /// Render this expression as raw SQL (values inlined as literals). - fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> pyo3::PyResult { + fn to_sql(&self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>) -> error::Result { sql::write_sql(dialect.py(), &self.0, dialect.get()) } - fn __repr__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + fn __repr__(&self, py: pyo3::Python<'_>) -> error::Result { Ok(self.0.python_repr(py)?.into_string()) } } diff --git a/src/expr/enum_/func.rs b/src/expr/enum_/func.rs index dcf458d..30da81a 100644 --- a/src/expr/enum_/func.rs +++ b/src/expr/enum_/func.rs @@ -1,5 +1,6 @@ use super::ExprInner; use super::ident::Ident; +use crate::error; use crate::dialect; use crate::internal::repr; @@ -66,7 +67,7 @@ pub struct Function { } impl Function { - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("Function"); fmt.positional(self.name.python_repr()); @@ -96,7 +97,7 @@ impl Function { } impl WriteSql for Function { - fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &crate::dialect::PyDialect, out: &mut W) -> error::Result<()> { // "name"([DISTINCT] args [{IGNORE|RESPECT} NULLS] [WHERE expr]) // [FILTER ([WHERE] expr)] [{IGNORE|RESPECT} NULLS] self.name.write_sql(dialect, out)?; @@ -111,16 +112,16 @@ impl WriteSql for Function { 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", - )); + return Err(error::Error::Unsupported { + feature: "null_treatment is not supported by this dialect", + fix: "set aggregate.null_treatment to OPT_NULL_TREATMENT_INSIDE_PARENS or OPT_NULL_TREATMENT_POSTFIX", + }); } 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", - )); + return Err(error::Error::Unsupported { + feature: "function filter is not supported by this dialect", + fix: "set aggregate.function_filter to OPT_FUNCTION_FILTER_FILTER, OPT_FUNCTION_FILTER_FILTER_WHERE, or OPT_FUNCTION_FILTER_INSIDE_WHERE", + }); } // Inside parens IGNORE/RESPECT NULLS diff --git a/src/expr/enum_/ident.rs b/src/expr/enum_/ident.rs index 586bb64..8c90cf4 100644 --- a/src/expr/enum_/ident.rs +++ b/src/expr/enum_/ident.rs @@ -1,6 +1,7 @@ //! SQL identifiers. use crate::dialect::PyDialect; +use crate::error; use crate::internal::repr; use crate::sql::SqlWriter; use crate::sql::WriteSql; @@ -21,10 +22,10 @@ impl IdentPart { /// Quoted part whose body is `s`. /// /// `"*"` is a quoted identifier named star, not [`IdentPart::Asterisk`]. - pub fn quoted(s: impl AsRef) -> Result { + pub fn quoted(s: impl AsRef) -> Result { let s = s.as_ref(); if s.is_empty() { - return Err(ParseIdentError::EmptyChunk); + return Err(IdentParseError::EmptyChunk); } Ok(Self::Quote(arcstr::ArcStr::from(s))) } @@ -32,13 +33,13 @@ impl IdentPart { /// Unquoted part whose body is `s`. /// /// Rejects anything that is not `[A-Za-z_][A-Za-z0-9_]*`. - pub fn unquoted(s: impl AsRef) -> Result { + pub fn unquoted(s: impl AsRef) -> Result { let s = s.as_ref(); if s.is_empty() { - return Err(ParseIdentError::EmptyChunk); + return Err(IdentParseError::EmptyChunk); } if !is_safe_unquoted(s) { - return Err(ParseIdentError::InvalidUnquoted); + return Err(IdentParseError::InvalidUnquoted(arcstr::ArcStr::from(s))); } Ok(Self::Unquote(arcstr::ArcStr::from(s))) } @@ -70,7 +71,7 @@ impl PartialEq<&str> for IdentPart { impl WriteSql for IdentPart { #[inline] - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> error::Result<()> { match self { Self::Quote(name) => { dialect.write_identifier(out, name.as_str()); @@ -103,8 +104,8 @@ impl WriteSql for IdentPart { pub struct Ident(smallvec::SmallVec<[IdentPart; 3]>); /// Why building an [`Ident`] failed. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ParseIdentError { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IdentParseError { /// No parts (empty string, empty constructor). Empty, /// A quoted or unquoted part was the empty string (`"a..b"`, `""` as a part). @@ -113,23 +114,26 @@ pub enum ParseIdentError { TooManyParts, /// `*` appeared before the last part (`*.id`). AsteriskNotLast, - /// Unquoted body was not a simple ASCII identifier. - InvalidUnquoted, + /// Unquoted body was not a simple ASCII identifier. Holds the rejected text. + InvalidUnquoted(arcstr::ArcStr), } -impl std::fmt::Display for ParseIdentError { +impl std::fmt::Display for IdentParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::Empty => "identifier is empty", - Self::EmptyChunk => "identifier has an empty part", - Self::TooManyParts => "identifier has too many parts", - Self::AsteriskNotLast => "asterisk must be the last identifier part", - Self::InvalidUnquoted => "unquoted identifier is not a simple ASCII name", - }) + match self { + Self::Empty => f.write_str("identifier is empty"), + Self::EmptyChunk => f.write_str("identifier has an empty part"), + Self::TooManyParts => write!(f, "identifier has too many parts; the maximum is {}", Ident::MAX_PARTS), + Self::AsteriskNotLast => f.write_str("asterisk must be the last identifier part"), + Self::InvalidUnquoted(name) => write!( + f, + "unquoted identifier {name:?} is not a simple ASCII name; expected [A-Za-z_][A-Za-z0-9_]*" + ), + } } } -impl std::error::Error for ParseIdentError {} +impl std::error::Error for IdentParseError {} /// `[A-Za-z_][A-Za-z0-9_]*`. Empty string is not safe. fn is_safe_unquoted(s: &str) -> bool { @@ -206,19 +210,19 @@ 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 { + pub fn from_parts(iter: impl IntoIterator) -> Result { let mut parts = smallvec::SmallVec::<[IdentPart; 3]>::new(); for part in iter { if let Some(IdentPart::Asterisk) = parts.last() { - return Err(ParseIdentError::AsteriskNotLast); + return Err(IdentParseError::AsteriskNotLast); } if parts.len() >= Self::MAX_PARTS { - return Err(ParseIdentError::TooManyParts); + return Err(IdentParseError::TooManyParts); } match &part { IdentPart::Quote(name) | IdentPart::Unquote(name) if name.is_empty() => { - return Err(ParseIdentError::EmptyChunk); + return Err(IdentParseError::EmptyChunk); } IdentPart::Quote(_) | IdentPart::Unquote(_) | IdentPart::Asterisk => {} } @@ -226,7 +230,7 @@ impl Ident { } if parts.is_empty() { - return Err(ParseIdentError::Empty); + return Err(IdentParseError::Empty); } Ok(Self(parts)) @@ -234,22 +238,22 @@ impl Ident { } impl std::str::FromStr for Ident { - type Err = ParseIdentError; + type Err = IdentParseError; #[cfg_attr(feature = "nightly", optimize(speed))] fn from_str(s: &str) -> Result { if s.is_empty() { - return Err(ParseIdentError::Empty); + return Err(IdentParseError::Empty); } let mut parts = smallvec::SmallVec::<[IdentPart; 3]>::new(); for chunk in s.splitn(Self::MAX_PARTS + 1, '.') { if chunk.is_empty() { - return Err(ParseIdentError::EmptyChunk); + return Err(IdentParseError::EmptyChunk); } if parts.len() >= Self::MAX_PARTS { - return Err(ParseIdentError::TooManyParts); + return Err(IdentParseError::TooManyParts); } // A bare `*` only makes sense as the final part (e.g. `table.*`). // If another part follows, demote the previous `*` to a quoted star. @@ -267,7 +271,7 @@ impl std::str::FromStr for Ident { impl WriteSql for Ident { #[inline] - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> error::Result<()> { write_separated(dialect, out, &self.0, ".") } } diff --git a/src/expr/enum_/like.rs b/src/expr/enum_/like.rs index 2811e69..a24b1b0 100644 --- a/src/expr/enum_/like.rs +++ b/src/expr/enum_/like.rs @@ -2,6 +2,7 @@ use super::ExprInner; use crate::dialect; +use crate::error; use crate::internal::repr; use crate::sql::SqlWriter; use crate::sql::WriteSql; @@ -11,7 +12,7 @@ pub(super) fn write_escape( dialect: &dialect::PyDialect, out: &mut W, escape: Option, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { let Some(c) = escape else { return Ok(()); }; @@ -33,7 +34,7 @@ pub enum LikePattern { } impl LikePattern { - fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { match self { Self::One(pattern) => pattern.python_repr(py), Self::Any(patterns) => { @@ -83,7 +84,7 @@ pub struct Like { impl Like { pub(crate) const PRECEDENCE: u8 = 5; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("LikeExpr"); fmt.positional(self.expr.python_repr(py)?) @@ -103,7 +104,7 @@ fn write_like_operand( expr: &ExprInner, wrap_lower: bool, paren_if_equal: bool, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if wrap_lower { out.write_str("LOWER("); expr.write_sql(dialect, out)?; @@ -132,7 +133,7 @@ fn write_like_pattern_list( out: &mut W, patterns: &[ExprInner], wrap_lower: bool, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if wrap_lower { for (i, pattern) in patterns.iter().enumerate() { if i > 0 { @@ -162,19 +163,22 @@ fn ensure_like_escape_is_supported( syntax: dialect::LikeEscape, escape: Option, native_any: bool, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if escape.is_none() || like_escape_allowed(syntax, native_any) { return Ok(()); } - Err(rapidquery_macros::pyerr!( - PyRuntimeError, - if native_any { - "LIKE ANY ESCAPE is not supported by this dialect" - } else { - "LIKE ESCAPE is not supported by this dialect" + Err(if native_any { + error::Error::Unsupported { + feature: "LIKE ANY ESCAPE is not supported by this dialect", + fix: "set like.escape to include OPT_LIKE_ESCAPE_ANY", + } + } else { + error::Error::Unsupported { + feature: "LIKE ESCAPE is not supported by this dialect", + fix: "set like.escape to include OPT_LIKE_ESCAPE_SCALAR", } - )) + }) } /// `[NOT] (x LIKE|ILIKE ANY (ARRAY?[…]) [ESCAPE char])` @@ -186,7 +190,7 @@ fn write_like_any_native( ilike: bool, wrap_lower: bool, array: bool, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if like.negated { out.write_str("NOT ("); } @@ -212,16 +216,16 @@ fn write_like_any_native( } impl WriteSql for Like { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { 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" - )); + return Err(error::Error::Unsupported { + feature: "ILIKE is not supported by this dialect", + fix: "set like.ilike to OPT_I_LIKE_STRATEGY_STANDARD or OPT_I_LIKE_STRATEGY_USE_LIKE_LOWER", + }); } dialect::ILikeStrategy::Standard => (true, false), dialect::ILikeStrategy::UseLikeLower => (false, true), @@ -296,7 +300,7 @@ pub struct SimilarTo { impl SimilarTo { pub(crate) const PRECEDENCE: u8 = 5; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("SimilarToExpr"); fmt.positional(self.expr.python_repr(py)?) @@ -309,12 +313,12 @@ impl SimilarTo { } impl WriteSql for SimilarTo { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { if !dialect.like.supports_similar_to { - return Err(rapidquery_macros::pyerr!( - PyRuntimeError, - "SIMILAR TO is not supported by this dialect" - )); + return Err(error::Error::Unsupported { + feature: "SIMILAR TO is not supported by this dialect", + fix: "set like.supports_similar_to to True", + }); } self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; @@ -356,7 +360,7 @@ pub struct RLike { impl RLike { pub(crate) const PRECEDENCE: u8 = 5; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("RLikeExpr"); fmt.positional(self.expr.python_repr(py)?) @@ -376,7 +380,7 @@ fn write_regex_call( negated: bool, expr: &ExprInner, pattern: &ExprInner, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if negated { out.write_str("NOT "); } @@ -390,12 +394,12 @@ fn write_regex_call( } impl WriteSql for RLike { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { match dialect.like.regex { - dialect::LikeRegex::Unsupported => Err(rapidquery_macros::pyerr!( - PyRuntimeError, - "RLIKE is not supported by this dialect", - )), + dialect::LikeRegex::Unsupported => Err(error::Error::Unsupported { + feature: "RLIKE is not supported by this dialect", + fix: "set like.regex to OPT_LIKE_REGEX_STANDARD, OPT_LIKE_REGEX_TILDE, OPT_LIKE_REGEX_REGEXP_LIKE, OPT_LIKE_REGEX_REGEXP_CONTAINS, or OPT_LIKE_REGEX_REGEXP_MATCHES", + }), dialect::LikeRegex::Standard => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { diff --git a/src/expr/enum_/mod.rs b/src/expr/enum_/mod.rs index 099246a..3abe658 100644 --- a/src/expr/enum_/mod.rs +++ b/src/expr/enum_/mod.rs @@ -13,6 +13,7 @@ pub use operator::*; pub use value::*; use crate::dialect::PyDialect; +use crate::error; use crate::internal::repr::ReprFormatter; use crate::sql::SqlWriter; use crate::sql::WriteSql; @@ -127,7 +128,7 @@ impl ExprInner { out: &mut W, parent_prec: u8, paren_if_equal: bool, - ) -> pyo3::PyResult<()> { + ) -> error::Result<()> { let child_prec = self.precedence(); let paren = child_prec > parent_prec || (paren_if_equal && child_prec == parent_prec); if paren { @@ -141,7 +142,7 @@ impl ExprInner { } #[inline] - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { match self { Self::Ident(x) => Ok(x.python_repr()), Self::Value(x) => x.python_repr(py), @@ -167,7 +168,7 @@ impl ExprInner { impl WriteSql for ExprInner { #[cfg_attr(feature = "nightly", optimize(speed))] - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> error::Result<()> { 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 0b33353..1026de4 100644 --- a/src/expr/enum_/nodes.rs +++ b/src/expr/enum_/nodes.rs @@ -7,14 +7,14 @@ use super::TrimWhere; use super::UnaryOp; use super::UnarySpacing; use crate::dialect; +use crate::error; use crate::internal::repr; use crate::sql::SqlWriter; use crate::sql::WriteSql; use crate::sql::write_separated; -#[inline] -fn unsupported(message: &'static str) -> pyo3::PyErr { - rapidquery_macros::pyerr!(PyRuntimeError, message) +fn unsupported(feature: &'static str, fix: &'static str) -> error::Error { + error::Error::Unsupported { feature, fix } } /// Row value. The dialect chooses `ROW`, `struct`, or parentheses. @@ -41,7 +41,7 @@ pub struct Tuple { impl Tuple { pub(crate) const PRECEDENCE: u8 = 0; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("TupleExpr"); for item in &self.items { @@ -54,20 +54,29 @@ impl Tuple { } impl WriteSql for Tuple { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { let arity = self.items.len(); 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")); + return Err(unsupported( + "empty tuple is not supported by this dialect", + "set tuple.arity to include OPT_TUPLE_ARITY_EMPTY", + )); } if arity == 1 && !dialect.tuple.arity.contains(dialect::TupleArity::ONE) { - return Err(unsupported("a tuple needs at least two elements on this dialect")); + return Err(unsupported( + "a tuple needs at least two elements on this dialect", + "set tuple.arity to include OPT_TUPLE_ARITY_ONE", + )); } } match dialect.tuple.syntax { dialect::TupleSyntax::Unsupported => { - return Err(unsupported("tuple values are not supported by this dialect")); + return Err(unsupported( + "tuple values are not supported by this dialect", + "set tuple.syntax to OPT_TUPLE_SYNTAX_ROW, OPT_TUPLE_SYNTAX_STRUCT, or OPT_TUPLE_SYNTAX_PARENTHESES", + )); } dialect::TupleSyntax::Row => { out.write_str("ROW("); @@ -110,7 +119,7 @@ pub struct Unary { } impl Unary { - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("UnaryExpr"); fmt.positional(self.op.as_sql()) @@ -122,10 +131,13 @@ impl Unary { } impl WriteSql for Unary { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { if self.op == UnaryOp::BitNot { if !dialect.bitwise.operations.contains(dialect::BitwiseOperations::NOT) { - return Err(unsupported("bitwise NOT is not supported by this dialect")); + return Err(unsupported( + "bitwise NOT is not supported by this dialect", + "set bitwise.operations to include OPT_BITWISE_OPERATIONS_NOT", + )); } match dialect.bitwise.syntax { @@ -185,7 +197,7 @@ pub struct Binary { } impl Binary { - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("BinaryExpr"); fmt.positional(self.left.python_repr(py)?) @@ -213,7 +225,7 @@ fn write_binary_call( name: &str, left: &ExprInner, right: &ExprInner, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { out.write_str(name); out.write('('); left.write_sql(dialect, out)?; @@ -239,10 +251,10 @@ fn write_bit_op( infix: &str, bit_fn: &str, shift_fn: &str, - missing: &'static str, -) -> pyo3::PyResult<()> { + missing: error::Error, +) -> error::Result<()> { if !dialect.bitwise.operations.contains(op) { - return Err(unsupported(missing)); + return Err(missing); } match dialect.bitwise.syntax { dialect::BitwiseSyntax::Infix => write_infix(bin, dialect, out, infix, false), @@ -264,7 +276,7 @@ fn write_infix( out: &mut W, spelling: &str, paren_arithmetic: bool, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { let prec = bin.op.precedence(); write_infix_side(dialect, out, &bin.left, prec, false, paren_arithmetic)?; out.write(' '); @@ -281,7 +293,7 @@ fn write_infix_side( prec: u8, paren_if_equal: bool, paren_arithmetic: bool, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if paren_arithmetic && matches!(expr, ExprInner::Binary(b) if is_arithmetic_binary_op(&b.op)) { out.write('('); expr.write_sql(dialect, out)?; @@ -292,7 +304,7 @@ fn write_infix_side( } impl WriteSql for Binary { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { match &self.op { BinaryOp::StringConcat => { _ = dialect.string.concat.write_to_result( @@ -307,7 +319,10 @@ impl WriteSql for Binary { Ok(()) } BinaryOp::BitXor => match dialect.bitwise.xor { - dialect::BitwiseXor::Unsupported => Err(unsupported("bitwise XOR is not supported by this dialect")), + dialect::BitwiseXor::Unsupported => Err(unsupported( + "bitwise XOR is not supported by this dialect", + "set bitwise.xor to OPT_BITWISE_XOR_CARET, OPT_BITWISE_XOR_HASH, OPT_BITWISE_XOR_FUNCTION, or OPT_BITWISE_XOR_BIT_FUNCTION", + )), 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), @@ -326,7 +341,10 @@ impl WriteSql for Binary { "&", "BITAND", "BITAND", - "bitwise AND is not supported by this dialect", + error::Error::Unsupported { + feature: "bitwise AND is not supported by this dialect", + fix: "set bitwise.operations to include OPT_BITWISE_OPERATIONS_AND", + }, ), BinaryOp::BitOr => write_bit_op( self, @@ -336,7 +354,10 @@ impl WriteSql for Binary { "|", "BITOR", "BITOR", - "bitwise OR is not supported by this dialect", + error::Error::Unsupported { + feature: "bitwise OR is not supported by this dialect", + fix: "set bitwise.operations to include OPT_BITWISE_OPERATIONS_OR", + }, ), BinaryOp::LShift => write_bit_op( self, @@ -346,7 +367,10 @@ impl WriteSql for Binary { "<<", "BITSHIFTLEFT", "SHIFTLEFT", - "bitwise shift is not supported by this dialect", + error::Error::Unsupported { + feature: "bitwise shift is not supported by this dialect", + fix: "set bitwise.operations to include OPT_BITWISE_OPERATIONS_SHL", + }, ), BinaryOp::RShift => write_bit_op( self, @@ -356,7 +380,10 @@ impl WriteSql for Binary { ">>", "BITSHIFTRIGHT", "SHIFTRIGHT", - "bitwise shift is not supported by this dialect", + error::Error::Unsupported { + feature: "bitwise shift is not supported by this dialect", + fix: "set bitwise.operations to include OPT_BITWISE_OPERATIONS_SHR", + }, ), _ => write_infix(self, dialect, out, self.op.as_sql(), false), } @@ -391,7 +418,7 @@ pub struct Is { impl Is { pub(crate) const PRECEDENCE: u8 = 5; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("IsExpr"); fmt.positional(self.expr.python_repr(py)?) @@ -404,7 +431,7 @@ impl Is { } impl WriteSql for Is { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { out.write_str(" IS NOT "); @@ -444,7 +471,7 @@ pub struct IsDistinctFrom { impl IsDistinctFrom { pub(crate) const PRECEDENCE: u8 = 5; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("IsDistinctFromExpr"); fmt.positional(self.left.python_repr(py)?) @@ -457,11 +484,12 @@ impl IsDistinctFrom { } impl WriteSql for IsDistinctFrom { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { match dialect.operator.is_distinct_from { - dialect::IsDistinctFrom::Unsupported => { - Err(unsupported("IS DISTINCT FROM is not supported by this dialect")) - } + dialect::IsDistinctFrom::Unsupported => Err(unsupported( + "IS DISTINCT FROM is not supported by this dialect", + "set operator.is_distinct_from to OPT_IS_DISTINCT_FROM_STANDARD or OPT_IS_DISTINCT_FROM_NULL_SAFE_EQUAL", + )), dialect::IsDistinctFrom::Standard => { self.left.write_operand(dialect, out, Self::PRECEDENCE, false)?; if self.negated { @@ -516,7 +544,7 @@ pub struct InList { impl InList { pub(crate) const PRECEDENCE: u8 = 5; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("InListExpr"); fmt.positional(self.expr.python_repr(py)?); @@ -534,7 +562,7 @@ impl InList { } impl WriteSql for InList { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { 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, self.negated); @@ -591,7 +619,7 @@ pub struct Between { impl Between { pub(crate) const PRECEDENCE: u8 = 5; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("BetweenExpr"); fmt.positional(self.expr.python_repr(py)?) .positional(self.low.python_repr(py)?) @@ -604,7 +632,7 @@ impl Between { } impl WriteSql for Between { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { // 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. @@ -655,7 +683,7 @@ pub struct Cast { impl Cast { pub(crate) const PRECEDENCE: u8 = 0; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("CastExpr"); fmt.positional(self.expr.python_repr(py)?) @@ -669,7 +697,7 @@ impl Cast { Ok(fmt) } - fn write_call(&self, dialect: &dialect::PyDialect, out: &mut W, name: &str) -> pyo3::PyResult<()> { + fn write_call(&self, dialect: &dialect::PyDialect, out: &mut W, name: &str) -> error::Result<()> { out.write_str(name); out.write('('); self.expr.write_sql(dialect, out)?; @@ -681,7 +709,7 @@ impl Cast { } impl WriteSql for Cast { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { match self.kind { CastKind::Cast => self.write_call(dialect, out, "CAST"), CastKind::TryCast => { @@ -696,12 +724,18 @@ impl WriteSql for Cast { }, ) } else { - Err(unsupported("try cast is not supported by this dialect")) + Err(unsupported( + "try cast is not supported by this dialect", + "set cast.try_cast to a template such as \"TRY_CAST({expr} AS {type})\"", + )) } } CastKind::DoubleColon => { if !dialect.cast.supports_double_colon { - return Err(unsupported("double-colon cast is not supported by this dialect")); + return Err(unsupported( + "double-colon cast is not supported by this dialect", + "set cast.supports_double_colon to True", + )); } out.write('('); self.expr.write_sql(dialect, out)?; @@ -742,7 +776,7 @@ pub struct Trim { impl Trim { pub(crate) const PRECEDENCE: u8 = 0; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let characters = self .characters .as_ref() @@ -763,7 +797,7 @@ impl Trim { } #[inline] -fn write_trim_function(trim: &Trim, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { +fn write_trim_function(trim: &Trim, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { let name = match trim.where_ { TrimWhere::Both => "TRIM", TrimWhere::Leading => "LTRIM", @@ -781,7 +815,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<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { match dialect.operator.trim { dialect::OperatorTrim::SqlFrom => { out.write_str("TRIM("); @@ -811,7 +845,10 @@ impl WriteSql for Trim { 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")); + return Err(unsupported( + "trim characters are not supported by this dialect", + "set operator.trim to OPT_OPERATOR_TRIM_SQL_FROM or OPT_OPERATOR_TRIM_FUNCTION", + )); } write_trim_function(self, dialect, out) } @@ -845,7 +882,7 @@ pub struct Collate { impl Collate { pub(crate) const PRECEDENCE: u8 = 0; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("CollateExpr"); fmt.positional(self.expr.python_repr(py)?) .positional(self.collation.python_repr()) @@ -856,9 +893,12 @@ impl Collate { } impl WriteSql for Collate { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { match dialect.operator.collate { - dialect::OperatorCollate::Unsupported => Err(unsupported("COLLATE is not supported by this dialect")), + dialect::OperatorCollate::Unsupported => Err(unsupported( + "COLLATE is not supported by this dialect", + "set operator.collate to OPT_OPERATOR_COLLATE_INFIX_IDENT or OPT_OPERATOR_COLLATE_FUNCTION_STRING", + )), dialect::OperatorCollate::InfixIdent => { self.expr.write_operand(dialect, out, Self::PRECEDENCE, false)?; out.write_str(" COLLATE "); @@ -870,7 +910,7 @@ impl WriteSql for Collate { body.as_str() } _ => { - return Err(unsupported("collation must be a single name")); + return Err(error::Error::CollationNotSingleName); } }; out.write_str("COLLATE("); @@ -909,7 +949,7 @@ pub struct Case { impl Case { pub(crate) const PRECEDENCE: u8 = 0; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let operand = self .operand .as_ref() @@ -935,7 +975,7 @@ impl Case { } impl WriteSql for Case { - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { out.write_str("CASE"); if let Some(operand) = &self.operand { out.write(' '); @@ -982,7 +1022,7 @@ pub struct Custom { impl Custom { pub(crate) const PRECEDENCE: u8 = 0; - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let mut fmt = repr::ReprFormatter::new_call("CustomExpr"); fmt.positional(self.sql.as_str()); @@ -998,7 +1038,7 @@ impl Custom { impl WriteSql for Custom { #[cfg_attr(feature = "nightly", optimize(speed))] - fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()> { let bytes = self.sql.as_bytes(); let mut i = 0; let mut start = 0; @@ -1016,12 +1056,10 @@ impl WriteSql for Custom { if i + 1 < bytes.len() && bytes[i + 1] == b'}' { out.write_str(&self.sql[start..i]); if used >= self.args.len() { - return rapidquery_macros::pyerr!( - Err(PyValueError), - "custom expression has {} placeholders but {} arguments", - used + 1, - self.args.len() - ); + return Err(error::Error::CustomPlaceholderCount { + placeholders: used + 1, + arguments: self.args.len(), + }); } self.args[used].write_sql(dialect, out)?; used += 1; @@ -1041,12 +1079,10 @@ impl WriteSql for Custom { out.write_str(&self.sql[start..]); if used != self.args.len() { - return rapidquery_macros::pyerr!( - Err(PyValueError), - "custom expression has {} placeholders but {} arguments", - used, - self.args.len() - ); + return Err(error::Error::CustomPlaceholderCount { + placeholders: used, + arguments: self.args.len(), + }); } Ok(()) } diff --git a/src/expr/enum_/value.rs b/src/expr/enum_/value.rs index ca9baad..9ee7be4 100644 --- a/src/expr/enum_/value.rs +++ b/src/expr/enum_/value.rs @@ -1,5 +1,6 @@ //! A Python object plus the [`Serializer`] used to emit it as SQL. +use crate::error; use pyo3::types::PyAnyMethods; use pyo3::types::PyStringMethods; @@ -36,11 +37,11 @@ impl Value { } #[inline] - pub fn infer_bound(bound: alias::BoundObject) -> pyo3::PyResult { + pub fn infer_bound(bound: alias::BoundObject) -> error::Result { SerializerType::infer_bound(&bound).map(|x| Self::new(bound.unbind(), x)) } - pub fn python_repr(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + pub fn python_repr(&self, py: pyo3::Python<'_>) -> error::Result { let obj_repr = self.object.bind(py).repr()?; let text = obj_repr.to_cow()?; @@ -55,7 +56,7 @@ impl Value { } impl WriteSql for Value { - fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> pyo3::PyResult<()> { + fn write_sql(&self, dialect: &PyDialect, out: &mut W) -> error::Result<()> { out.write_value(dialect, &self.serializer, &self.object) } } diff --git a/src/expr/extractors.rs b/src/expr/extractors.rs index 696aed7..56f6a84 100644 --- a/src/expr/extractors.rs +++ b/src/expr/extractors.rs @@ -1,3 +1,4 @@ +use crate::error; use std::str::FromStr; use pyo3::types::PyAnyMethods; @@ -58,7 +59,7 @@ pub struct IdentExtractor(pub Ident); fn extract_expr( obj: pyo3::prelude::Borrowed<'_, '_, pyo3::prelude::PyAny>, str_as_ident: bool, -) -> pyo3::PyResult { +) -> error::Result { let ptr = obj.as_ptr(); unsafe { if crate::typeref::check(ptr, crate::typeref::EXPR_TYPE) { @@ -68,8 +69,7 @@ fn extract_expr( if crate::typeref::check_exact(ptr, crate::typeref::UNQUOTE_TYPE) { let unquote = obj.cast_unchecked::(); - let part = IdentPart::unquoted(unquote.get().0.as_str()) - .map_err(|err| rapidquery_macros::pyerr!(PyValueError, "{}", err))?; + let part = IdentPart::unquoted(unquote.get().0.as_str()).map_err(error::Error::from)?; return Ok(ExprInner::Ident(Ident::from_one(part))); } @@ -83,7 +83,7 @@ fn extract_expr( // a bare `str` becomes [`ExprInner::Ident`] if `str_as_ident` if str_as_ident { - let inner = Ident::from_str(s).map_err(|err| rapidquery_macros::pyerr!(PyValueError, "{}", err))?; + let inner = Ident::from_str(s)?; return Ok(ExprInner::Ident(inner)); } @@ -116,11 +116,7 @@ fn extract_expr( return Ok(expr.get().0.clone()); }, mismatches: { - return rapidquery_macros::pyerr!( - Err(PyTypeError), - "__expr__ protocol attribute returns non-expr type: {}", - utils::get_type_name(obj.py(), protocol.as_ptr()) - ) + return Err(error::Error::Expected { expected: "__expr__ protocol attribute returned a non-expr", got: utils::get_type_name(obj.py(), protocol.as_ptr()) }); } } @@ -131,7 +127,7 @@ fn extract_expr( /// /// A Python `str` is never a sequence. An already-built `TupleExpr` is a /// single pattern; a raw `tuple`/`list` is ANY. -pub fn extract_like_pattern(obj: pyo3::prelude::Borrowed<'_, '_, pyo3::prelude::PyAny>) -> pyo3::PyResult { +pub fn extract_like_pattern(obj: pyo3::prelude::Borrowed<'_, '_, pyo3::prelude::PyAny>) -> error::Result { let ptr = obj.as_ptr(); unsafe { if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { @@ -149,13 +145,13 @@ pub fn extract_like_pattern(obj: pyo3::prelude::Borrowed<'_, '_, pyo3::prelude:: Ok(LikePattern::One(Box::new(extract_expr(obj, false)?))) } -fn extract_like_any<'py>(iter: impl IntoIterator>) -> pyo3::PyResult { +fn extract_like_any<'py>(iter: impl IntoIterator>) -> error::Result { let mut items = Vec::new(); for item in iter { items.push(item.extract::()?.0); } if items.is_empty() { - return rapidquery_macros::pyerr!(Err(PyValueError), "LIKE ANY pattern list is empty"); + return Err(error::Error::EmptyLikeAny); } Ok(LikePattern::Any(items)) } @@ -169,14 +165,14 @@ pub(super) enum ExtractedIdentArgument { #[inline] pub(super) fn extract_ident_argument( obj: pyo3::prelude::Borrowed<'_, '_, pyo3::prelude::PyAny>, -) -> pyo3::PyResult { +) -> error::Result { let ptr = obj.as_ptr(); unsafe { // SAFETY: `ptr` is a live borrowed `PyObject`. Type pointers are // interned in `initialize_typeref` before any extractor runs. if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { let s = utils::borrow_py_str_buffer(obj.py(), ptr)?; - let part = IdentPart::quoted(s).map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))?; + let part = IdentPart::quoted(s)?; return Ok(ExtractedIdentArgument::Part(part)); } @@ -194,8 +190,7 @@ pub(super) fn extract_ident_argument( if crate::typeref::check_exact(ptr, crate::typeref::UNQUOTE_TYPE) { let unquote = obj.cast_unchecked::(); - let part = IdentPart::unquoted(unquote.get().0.as_str()) - .map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))?; + let part = IdentPart::unquoted(unquote.get().0.as_str())?; return Ok(ExtractedIdentArgument::Part(part)); } @@ -221,23 +216,18 @@ pub(super) fn extract_ident_argument( } }, mismatches: { - return rapidquery_macros::pyerr!( - Err(PyTypeError), - "__ident__ protocol attribute returns non-ident type: {}", - utils::get_type_name(obj.py(), protocol.as_ptr()) - ) + return Err(error::Error::Expected { expected: "__ident__ protocol attribute returned a non-ident", got: utils::get_type_name(obj.py(), protocol.as_ptr()) }); } } - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected str, Ident, Unquote or Asterisk, got {}", - unsafe { crate::internal::utils::get_type_name(obj.py(), obj.as_ptr()) } - ) + Err(error::Error::Expected { + expected: "expected a str, Ident, Unquote, or Asterisk", + got: unsafe { crate::internal::utils::get_type_name(obj.py(), obj.as_ptr()) }, + }) } impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for LiteralExprExtractor { - type Error = pyo3::PyErr; + type Error = error::Error; #[inline] fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { @@ -246,7 +236,7 @@ impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for LiteralExprExtractor { } impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for IdentExtractor { - type Error = pyo3::PyErr; + type Error = error::Error; #[inline] fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { @@ -254,7 +244,7 @@ impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for IdentExtractor { // SAFETY: `ptr` is a live borrowed `PyObject`. if pyo3::ffi::PyUnicode_CheckExact(obj.as_ptr()) == 1 { let s = utils::borrow_py_str_buffer(obj.py(), obj.as_ptr())?; - let inner = Ident::from_str(s).map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))?; + let inner = Ident::from_str(s)?; return Ok(Self(inner)); } diff --git a/src/expr/func.rs b/src/expr/func.rs index 510af6c..8a9dddb 100644 --- a/src/expr/func.rs +++ b/src/expr/func.rs @@ -1,3 +1,4 @@ +use crate::error; use pyo3::types::PyAnyMethods; use pyo3::types::PyTupleMethods; @@ -28,12 +29,13 @@ impl PyFunction { } } - fn wrap(py: pyo3::Python<'_>, inner: super::enum_::Function) -> pyo3::PyResult { + fn wrap(py: pyo3::Python<'_>, inner: super::enum_::Function) -> error::Result { pyo3::Py::new( py, pyo3::PyClassInitializer::from(PyExpr(inner.into())).add_subclass(Self), ) .map(pyo3::Py::into_any) + .map_err(error::Error::from) } } @@ -45,7 +47,7 @@ impl PyFunction { name: IdentExtractor, args: alias::ArgsType<'_>, distinct: bool, - ) -> pyo3::PyResult> { + ) -> error::Result> { let mut extracted = Vec::with_capacity(args.len()); for item in args.iter() { extracted.push(item.extract::()?.0); @@ -62,21 +64,21 @@ impl PyFunction { } /// Return a copy whose filter predicate is `predicate`. - fn filter(slf: pyo3::PyRef<'_, Self>, predicate: LiteralExprExtractor) -> pyo3::PyResult { + fn filter(slf: pyo3::PyRef<'_, Self>, predicate: LiteralExprExtractor) -> error::Result { let mut inner = Self::inner(&slf).clone(); inner.filter = Some(Box::new(predicate.0)); Self::wrap(slf.py(), inner) } /// Return a copy that writes `IGNORE NULLS`. - fn ignore_nulls(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { + fn ignore_nulls(slf: pyo3::PyRef<'_, Self>) -> error::Result { let mut inner = Self::inner(&slf).clone(); inner.null_treatment = Some(NullTreatment::IgnoreNulls); Self::wrap(slf.py(), inner) } /// Return a copy that writes `RESPECT NULLS`. - fn respect_nulls(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult { + fn respect_nulls(slf: pyo3::PyRef<'_, Self>) -> error::Result { let mut inner = Self::inner(&slf).clone(); inner.null_treatment = Some(NullTreatment::RespectNulls); Self::wrap(slf.py(), inner) diff --git a/src/expr/ident.rs b/src/expr/ident.rs index 88aaef5..c700202 100644 --- a/src/expr/ident.rs +++ b/src/expr/ident.rs @@ -1,8 +1,9 @@ +use crate::error; use pyo3::types::PyTupleMethods; use super::enum_::Ident; +use super::enum_::IdentParseError; use super::enum_::IdentPart; -use super::enum_::ParseIdentError; use super::extractors::ExtractedIdentArgument; use super::extractors::extract_ident_argument; use crate::internal::alias; @@ -14,7 +15,7 @@ use crate::internal::repr; fn ident_part_to_pyobject<'py>( py: pyo3::Python<'py>, ident_part: &IdentPart, -) -> pyo3::PyResult> { +) -> error::Result> { match ident_part { IdentPart::Quote(s) => Ok(pyo3::types::PyString::new(py, s.as_str()).into_any()), IdentPart::Unquote(s) => Ok(pyo3::Bound::new(py, PyUnquote(arcstr::ArcStr::from(s.as_str())))?.into_any()), @@ -46,9 +47,9 @@ impl PyIdent { impl PyIdent { #[new] #[pyo3(signature = (*parts))] - fn __new__(parts: alias::ArgsType) -> pyo3::PyResult> { + fn __new__(parts: alias::ArgsType) -> error::Result> { let ident = match parts.len() { - 0 => return rapidquery_macros::pyerr!(Err(PyValueError), "{}", ParseIdentError::Empty), + 0 => return Err(error::Error::Ident(IdentParseError::Empty)), 1 => match extract_ident_argument(parts.get_item(0)?.as_borrowed())? { ExtractedIdentArgument::Ident(ident) => ident, ExtractedIdentArgument::Part(part) => Ident::from_one(part), @@ -62,7 +63,7 @@ impl PyIdent { } } - Ident::from_parts(acc).map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))? + Ident::from_parts(acc)? } }; @@ -76,7 +77,7 @@ impl PyIdent { Self::inner(&slf).len() } - fn __getitem__<'py>(slf: pyo3::PyRef<'py, Self>, index: isize) -> pyo3::PyResult> { + fn __getitem__<'py>(slf: pyo3::PyRef<'py, Self>, index: isize) -> error::Result> { let inner = Self::inner(&slf); let x = if index < 0 { @@ -87,12 +88,15 @@ impl PyIdent { match inner.parts().get(x as usize) { Some(part) => ident_part_to_pyobject(slf.py(), part), - None => rapidquery_macros::pyerr!(Err(PyIndexError), "index out of range"), + None => Err(error::Error::IdentIndex { + index, + len: inner.len(), + }), } } /// Copy the parts into a list. - fn to_list(slf: pyo3::PyRef<'_, Self>, py: pyo3::Python<'_>) -> pyo3::PyResult> { + fn to_list(slf: pyo3::PyRef<'_, Self>, py: pyo3::Python<'_>) -> error::Result> { Self::inner(&slf) .parts() .iter() @@ -110,9 +114,9 @@ pub struct PyUnquote(pub arcstr::ArcStr); impl PyUnquote { #[new] #[pyo3(signature = (name, /))] - fn __new__(name: &str) -> pyo3::PyResult { + fn __new__(name: &str) -> error::Result { // validate the `name` - IdentPart::unquoted(name).map_err(|e| rapidquery_macros::pyerr!(PyValueError, "{}", e))?; + IdentPart::unquoted(name)?; Ok(Self(arcstr::ArcStr::from(name))) } diff --git a/src/expr/nodes.rs b/src/expr/nodes.rs index 96129ba..38fdcba 100644 --- a/src/expr/nodes.rs +++ b/src/expr/nodes.rs @@ -1,3 +1,4 @@ +use crate::error; use pyo3::types::PyAnyMethods; use pyo3::types::PyTupleMethods; @@ -91,7 +92,7 @@ empty_node! { impl PyTupleExpr { #[new] #[pyo3(signature = (*items))] - fn __new__(items: alias::ArgsType<'_>) -> pyo3::PyResult> { + fn __new__(items: alias::ArgsType<'_>) -> error::Result> { use pyo3::types::PyTupleMethods; let mut exprs = Vec::with_capacity(items.len()); @@ -132,9 +133,11 @@ impl PyUnaryExpr { impl PyUnaryExpr { #[new] #[pyo3(signature = (operator, expr, /))] - fn __new__(operator: &str, expr: LiteralExprExtractor) -> pyo3::PyResult> { + fn __new__(operator: &str, expr: LiteralExprExtractor) -> error::Result> { let Some(op) = super::enum_::UnaryOp::parse(operator) else { - return rapidquery_macros::pyerr!(Err(PyValueError), "unknown unary operator {:?}", operator); + return Err(error::Error::UnknownUnaryOperator { + operator: operator.to_owned(), + }); }; let base = Self::new_base(op, expr.0); @@ -193,13 +196,9 @@ impl PyBinaryExpr { } impl PyIsExpr { - pub(super) fn new_base(expr: ExprInner, kind: &str, negated: bool) -> pyo3::PyResult { + pub(super) fn new_base(expr: ExprInner, kind: &str, negated: bool) -> error::Result { let Some(kind) = super::enum_::IsKind::parse(kind) else { - return rapidquery_macros::pyerr!( - Err(PyValueError), - "unknown IS kind {:?}; expected NULL, TRUE, FALSE, or UNKNOWN", - kind - ); + return Err(error::Error::UnknownIsKind { kind: kind.to_owned() }); }; let inner = super::enum_::Is { @@ -215,11 +214,7 @@ impl PyIsExpr { impl PyIsExpr { #[new] #[pyo3(signature = (expr, kind, /, *, negated = false))] - fn __new__( - expr: LiteralExprExtractor, - kind: &str, - negated: bool, - ) -> pyo3::PyResult> { + fn __new__(expr: LiteralExprExtractor, kind: &str, negated: bool) -> error::Result> { let base = Self::new_base(expr.0, kind, negated)?; Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) } @@ -304,7 +299,7 @@ impl PyInListExpr { expr: LiteralExprExtractor, list: alias::BoundObject<'_>, negated: bool, - ) -> pyo3::PyResult> { + ) -> error::Result> { let base = Self::new_base(expr.0, list, negated)?; Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) } @@ -390,7 +385,7 @@ impl PyLikeExpr { negated: bool, case_sensitive: bool, escape: Option, - ) -> pyo3::PyResult { + ) -> error::Result { let inner = super::enum_::Like { case_sensitive, negated, @@ -412,7 +407,7 @@ impl PyLikeExpr { negated: bool, case_sensitive: bool, escape: Option, - ) -> pyo3::PyResult> { + ) -> error::Result> { let base = Self::new_base(expr.0, pattern, negated, case_sensitive, escape)?; Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) } @@ -425,6 +420,7 @@ impl PyLikeExpr { fn get_pattern(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { let py = slf.py(); let inner = Self::inner(&slf); + match &inner.pattern { super::enum_::LikePattern::One(p) => super::base::into_subclasses(py, (**p).clone()), super::enum_::LikePattern::Any(ps) => unsafe { @@ -535,9 +531,9 @@ impl PyRLikeExpr { } impl PyCastExpr { - pub(super) fn new_base(expr: ExprInner, data_type_name: &str, kind: &str) -> pyo3::PyResult { + pub(super) fn new_base(expr: ExprInner, data_type_name: &str, kind: &str) -> error::Result { let Some(kind) = super::enum_::CastKind::parse(kind) else { - return rapidquery_macros::pyerr!(Err(PyValueError), "unknown cast kind {:?}", kind); + return Err(error::Error::UnknownCastKind { kind: kind.to_owned() }); }; let inner = super::enum_::Cast { @@ -557,7 +553,7 @@ impl PyCastExpr { expr: LiteralExprExtractor, data_type_name: &str, kind: &str, - ) -> pyo3::PyResult> { + ) -> error::Result> { let base = Self::new_base(expr.0, data_type_name, kind)?; Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) } @@ -579,13 +575,11 @@ impl PyCastExpr { } impl PyTrimExpr { - pub(super) fn new_base(expr: ExprInner, r#where: &str, characters: Option) -> pyo3::PyResult { + pub(super) fn new_base(expr: ExprInner, r#where: &str, characters: Option) -> error::Result { let Some(where_) = super::enum_::TrimWhere::parse(r#where) else { - return rapidquery_macros::pyerr!( - Err(PyValueError), - "unknown TRIM where {:?}; expected BOTH, LEADING, or TRAILING", - r#where - ); + return Err(error::Error::UnknownTrimWhere { + kind: r#where.to_owned(), + }); }; let inner = super::enum_::Trim { @@ -605,7 +599,7 @@ impl PyTrimExpr { expr: LiteralExprExtractor, r#where: &str, characters: Option, - ) -> pyo3::PyResult> { + ) -> error::Result> { let base = Self::new_base(expr.0, r#where, characters.map(|LiteralExprExtractor(e)| e))?; Ok(pyo3::PyClassInitializer::from(base).add_subclass(Self)) } @@ -662,7 +656,7 @@ impl PyCollateExpr { impl PyCaseExpr { #[new] #[pyo3(signature = (operand = None, /))] - fn __new__(operand: Option) -> pyo3::PyResult> { + fn __new__(operand: Option) -> error::Result> { let inner = super::enum_::Case { operand: operand.map(|LiteralExprExtractor(e)| Box::new(e)), when: vec![], @@ -675,7 +669,7 @@ impl PyCaseExpr { slf: pyo3::PyRef<'_, Self>, when: LiteralExprExtractor, then: LiteralExprExtractor, - ) -> pyo3::PyResult { + ) -> error::Result { let mut inner = Self::inner(&slf).clone(); inner.when.push((when.0, then.0)); @@ -684,6 +678,7 @@ impl PyCaseExpr { pyo3::PyClassInitializer::from(PyExpr(inner.into())).add_subclass(Self), ) .map(pyo3::Py::into_any) + .map_err(error::Error::from) } fn else_(slf: pyo3::PyRef<'_, Self>, result: LiteralExprExtractor) -> pyo3::PyResult { @@ -697,7 +692,7 @@ impl PyCaseExpr { .map(pyo3::Py::into_any) } - fn get_operand(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + fn get_operand(slf: pyo3::PyRef<'_, Self>) -> error::Result> { let inner = Self::inner(&slf); match inner.operand.as_ref() { @@ -709,7 +704,7 @@ impl PyCaseExpr { } } - fn get_when_list(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + fn get_when_list(slf: pyo3::PyRef<'_, Self>) -> error::Result> { let py = slf.py(); let inner = Self::inner(&slf); @@ -729,6 +724,7 @@ impl PyCaseExpr { fn get_else(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { let inner = Self::inner(&slf); + match inner.else_result.as_ref() { Some(expr) => super::base::into_subclasses(slf.py(), (**expr).clone()), None => Ok(utils::new_pynone(slf.py())), @@ -740,7 +736,7 @@ impl PyCaseExpr { impl PyCustomExpr { #[new] #[pyo3(signature = (sql, *args))] - fn __new__(sql: String, args: alias::ArgsType<'_>) -> pyo3::PyResult> { + fn __new__(sql: String, args: alias::ArgsType<'_>) -> error::Result> { let mut extracted = Vec::with_capacity(args.len()); for item in args.iter() { diff --git a/src/expr/value.rs b/src/expr/value.rs index cf50054..87242c2 100644 --- a/src/expr/value.rs +++ b/src/expr/value.rs @@ -1,3 +1,4 @@ +use crate::error; use crate::internal::alias; use crate::serializers::PySerializer; use crate::serializers::SerializerExtractor; @@ -32,7 +33,7 @@ impl PyValue { fn __new__( object: alias::BoundObject, serializer: Option, - ) -> pyo3::PyResult> { + ) -> error::Result> { // handle Value(Value(...), new_serializer) if unsafe { crate::typeref::check_exact(object.as_ptr(), crate::typeref::VALUE_TYPE) } { let expr = unsafe { object.cast_unchecked::() }; @@ -72,7 +73,7 @@ impl PyValue { } /// Serializer used by `to_sql`. - fn get_serializer(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + fn get_serializer(slf: pyo3::PyRef<'_, Self>) -> error::Result> { PySerializer(Self::inner(&slf).serializer.clone()).into_subclasses(slf.py()) } } diff --git a/src/internal/modules/json.rs b/src/internal/modules/json.rs index ae5dcb3..7449a89 100644 --- a/src/internal/modules/json.rs +++ b/src/internal/modules/json.rs @@ -3,6 +3,7 @@ //! The result is borrowed and passed to a callback so the SQL writer can //! escape it without an intermediate `String`. +use crate::error; use once_cell::sync::OnceCell; use pyo3::types::PyAnyMethods; use pyo3::types::PyModule; @@ -27,12 +28,12 @@ enum JsonDumper { static DUMPER: OnceCell = OnceCell::new(); -fn init_orjson(module: &pyo3::Bound<'_, PyModule>) -> pyo3::PyResult { +fn init_orjson(module: &pyo3::Bound<'_, PyModule>) -> error::Result { let dumps = module.getattr("dumps")?.unbind(); Ok(JsonDumper::Orjson { dumps }) } -fn init_stdlib(py: pyo3::Python<'_>) -> pyo3::PyResult { +fn init_stdlib(py: pyo3::Python<'_>) -> error::Result { let module = py.import("json")?; let dumps = module.getattr("dumps")?.unbind(); let kwnames = PyTuple::new(py, ["ensure_ascii", "allow_nan", "separators"])?.unbind(); @@ -47,20 +48,20 @@ fn init_stdlib(py: pyo3::Python<'_>) -> pyo3::PyResult { #[cfg(feature = "only-orjson")] #[cold] -fn init_import(py: pyo3::Python<'_>) -> pyo3::PyResult { +fn init_import(py: pyo3::Python<'_>) -> error::Result { let module = py.import("orjson")?; init_orjson(&module) } #[cfg(feature = "only-stdlib-json")] #[cold] -fn init_import(py: pyo3::Python<'_>) -> pyo3::PyResult { +fn init_import(py: pyo3::Python<'_>) -> error::Result { init_stdlib(py) } #[cfg(not(any(feature = "only-stdlib-json", feature = "only-orjson")))] #[cold] -fn init_import(py: pyo3::Python<'_>) -> pyo3::PyResult { +fn init_import(py: pyo3::Python<'_>) -> error::Result { if let Ok(module) = py.import("orjson") && let Ok(dumper) = init_orjson(&module) { @@ -79,8 +80,8 @@ fn init_import(py: pyo3::Python<'_>) -> pyo3::PyResult { unsafe fn visit_json_text( py: pyo3::Python<'_>, result: *mut pyo3::ffi::PyObject, - f: impl FnOnce(&str) -> pyo3::PyResult<()>, -) -> pyo3::PyResult<()> { + f: impl FnOnce(&str) -> error::Result<()>, +) -> error::Result<()> { if pyo3::ffi::PyBytes_CheckExact(result) == 1 { let buf = pyo3::ffi::PyBytes_AsString(result) as *const u8; let len = pyo3::ffi::PyBytes_Size(result) as usize; @@ -95,11 +96,10 @@ unsafe fn visit_json_text( return f(borrow_py_str_buffer(py, result)?); } - rapidquery_macros::pyerr!( - Err(PyTypeError), - "JSON dumps must return bytes or str, got {}", - crate::internal::utils::get_type_name(py, result) - ) + Err(error::Error::Expected { + expected: "expected JSON dumps to return bytes or str", + got: crate::internal::utils::get_type_name(py, result), + }) } /// JSON-encode `obj` and pass the text to `f` before dropping the dumps result. @@ -108,10 +108,7 @@ unsafe fn visit_json_text( /// `obj` must be a valid borrowed Python object. The GIL must be held. #[inline] #[cfg_attr(feature = "nightly", optimize(speed))] -pub unsafe fn dumps( - obj: pyo3::Bound<'_, pyo3::PyAny>, - f: impl FnOnce(&str) -> pyo3::PyResult<()>, -) -> pyo3::PyResult<()> { +pub unsafe fn dumps(obj: pyo3::Bound<'_, pyo3::PyAny>, f: impl FnOnce(&str) -> error::Result<()>) -> error::Result<()> { let py = obj.py(); let dumper = DUMPER.get_or_try_init(|| init_import(py))?; @@ -129,7 +126,7 @@ pub unsafe fn dumps( }; if result.is_null() { - return Err(pyo3::PyErr::fetch(py)); + return Err(pyo3::PyErr::fetch(py).into()); } let out = unsafe { visit_json_text(py, result, f) }; diff --git a/src/internal/template.rs b/src/internal/template.rs index 3e6b548..180cda8 100644 --- a/src/internal/template.rs +++ b/src/internal/template.rs @@ -92,7 +92,7 @@ pub enum TemplateParseError { RequiredKeyNotFound(&'static str), #[display("placeholders are missing (minimum {_0}, {_1} found)")] MissingPlaceholder(u8, usize), - #[display("too many placeholders (maximum is {_0} , {_1} found)")] + #[display("too many placeholders (maximum is {_0}, {_1} found)")] TooManyPlaceholder(u8, usize), } impl std::error::Error for TemplateParseError {} diff --git a/src/lib.rs b/src/lib.rs index 0091769..45bfd72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod data_type; pub mod dialect; +pub mod error; pub mod expr; pub mod internal; pub mod serializers; diff --git a/src/serializers/functions.rs b/src/serializers/functions.rs index ac6f624..d235e52 100644 --- a/src/serializers/functions.rs +++ b/src/serializers/functions.rs @@ -1,3 +1,4 @@ +use crate::error; use pyo3::types::PyAnyMethods; use pyo3::types::PyTzInfoAccess; @@ -27,13 +28,12 @@ pub(super) unsafe fn write_integer( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if pyo3::ffi::PyLong_Check(ptr) != 1 { - return rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected int for integer serialization, got {}", - utils::get_type_name(py, ptr) - ); + return Err(error::Error::Expected { + expected: "expected an int to serialize an integer", + got: utils::get_type_name(py, ptr), + }); } let mut overflow = 0i32; @@ -47,7 +47,7 @@ pub(super) unsafe fn write_integer( } if n == -1 && !pyo3::ffi::PyErr_Occurred().is_null() { - return Err(pyo3::PyErr::fetch(py)); + return Err(pyo3::PyErr::fetch(py).into()); } out.write_int(n); @@ -59,15 +59,11 @@ pub(super) unsafe fn write_float( py: pyo3::Python<'_>, ptr: *mut pyo3::ffi::PyObject, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if pyo3::ffi::PyFloat_CheckExact(ptr) == 1 { let v = pyo3::ffi::PyFloat_AsDouble(ptr); if !v.is_finite() { - return rapidquery_macros::pyerr!( - Err(PyValueError), - "cannot serialize non-finite float {} as a numeric literal", - v - ); + return Err(error::Error::NonFiniteFloat { value: v }); } out.write_float(v); @@ -81,11 +77,10 @@ pub(super) unsafe fn write_float( return Ok(()); } - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected float/int/Decimal for numeric serialization, got {}", - utils::get_type_name(py, ptr) - ) + Err(error::Error::Expected { + expected: "expected a float, int, or decimal.Decimal to serialize a number", + got: utils::get_type_name(py, ptr), + }) } #[inline] @@ -94,17 +89,16 @@ pub(super) unsafe fn write_bool( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if pyo3::ffi::PyBool_Check(ptr) == 1 { let value = pyo3::ffi::Py_True() == ptr; cfg.write_boolean(out, value); Ok(()) } else { - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected bool for boolean serialization, got {}", - utils::get_type_name(py, ptr) - ) + Err(error::Error::Expected { + expected: "expected a bool to serialize a boolean", + got: utils::get_type_name(py, ptr), + }) } } @@ -114,18 +108,15 @@ pub(super) unsafe fn write_str( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::StringConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if pyo3::ffi::PyUnicode_Check(ptr) == 1 { - return cfg - .write_string_literal(out, utils::borrow_py_str_buffer(py, ptr)?) - .map_err(Into::into); + return cfg.write_string_literal(out, utils::borrow_py_str_buffer(py, ptr)?); } - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected str for string serialization, got {}", - utils::get_type_name(py, ptr) - ) + Err(error::Error::Expected { + expected: "expected a str to serialize a string", + got: utils::get_type_name(py, ptr), + }) } #[inline] @@ -134,9 +125,12 @@ pub(super) unsafe fn write_uuid( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { let Some(tmpl) = &cfg.uuid else { - return Err(dialect::errors::FormatError::Unsupported("UuidSerializer").into()); + return Err(error::Error::Unsupported { + feature: "UUID values are not supported by this dialect", + fix: "set literal.uuid to a template such as \"'{}'\"", + }); }; let text = if crate::typeref::check_exact(ptr, crate::typeref::STD_UUID_TYPE) { @@ -144,15 +138,14 @@ pub(super) unsafe fn write_uuid( } else if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { let borrowed = utils::borrow_py_str_buffer(py, ptr)?; uuid::Uuid::try_parse(borrowed) - .map_err(|e| rapidquery_macros::pyerr!(PyValueError, "invalid uuid str: {}", e))? + .map_err(|e| error::Error::InvalidUuid { detail: e.to_string() })? .hyphenated() .to_string() } else { - return rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected uuid.UUID/str for UUID serialization, got {}", - utils::get_type_name(py, ptr) - ); + return Err(error::Error::Expected { + expected: "expected a uuid.UUID or str to serialize a UUID", + got: utils::get_type_name(py, ptr), + }); }; tmpl.write_to( @@ -169,7 +162,7 @@ pub(super) unsafe fn write_bytes( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if pyo3::ffi::PyBytes_CheckExact(ptr) == 1 { let buf = pyo3::ffi::PyBytes_AsString(ptr) as *const u8; let len = pyo3::ffi::PyBytes_Size(ptr) as usize; @@ -178,11 +171,10 @@ pub(super) unsafe fn write_bytes( cfg.write_bytes(out, std::slice::from_raw_parts(buf, len)); Ok(()) } else { - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected bytes for binary serialization, got {}", - utils::get_type_name(py, ptr) - ) + Err(error::Error::Expected { + expected: "expected bytes to serialize binary data", + got: utils::get_type_name(py, ptr), + }) } } @@ -191,29 +183,27 @@ pub(super) unsafe fn write_bit( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { if cfg.bit.is_none() { - return Err(dialect::errors::FormatError::Unsupported("BitSerializer").into()); + return Err(error::Error::Unsupported { + feature: "bit strings are not supported by this dialect", + fix: "set literal.bit to a template such as \"B'{}'\"", + }); } if pyo3::ffi::PyUnicode_CheckExact(ptr) == 1 { let raw = utils::borrow_py_str_buffer(py, ptr)?; if raw.is_empty() || !raw.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 {:?}", - raw - ); + return Err(error::Error::InvalidBitString { value: raw.to_owned() }); } - cfg.write_bits(out, raw).map_err(Into::into) + cfg.write_bits(out, raw) } else { - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected str of bits for bit serialization, got {}", - utils::get_type_name(py, ptr) - ) + Err(error::Error::Expected { + expected: "expected a str of '0' and '1' to serialize a bit string", + got: utils::get_type_name(py, ptr), + }) } } @@ -224,7 +214,7 @@ pub(super) unsafe fn write_json( literal_cfg: &dialect::LiteralConfig, string_cfg: &dialect::StringConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { crate::internal::modules::json_dumps( unsafe { pyo3::Bound::from_borrowed_ptr(py, ptr) }, // write result into `out` @@ -245,7 +235,7 @@ pub(super) unsafe fn write_date( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::TemporalConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { 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()?; @@ -254,11 +244,10 @@ pub(super) unsafe fn write_date( .write_to(out, |_, out| out.write_str(&date.format("%Y-%m-%d").to_string())); Ok(()) } else { - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected datetime.date for date serialization, got {}", - utils::get_type_name(py, ptr) - ) + Err(error::Error::Expected { + expected: "expected a datetime.date to serialize a date", + got: utils::get_type_name(py, ptr), + }) } } @@ -267,7 +256,7 @@ pub(super) unsafe fn write_time( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::TemporalConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { 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()?; @@ -276,11 +265,10 @@ pub(super) unsafe fn write_time( .write_to(out, |_, out| out.write_str(&time.format("%H:%M:%S%.f").to_string())); Ok(()) } else { - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected datetime.time for time serialization, got {}", - utils::get_type_name(py, ptr) - ) + Err(error::Error::Expected { + expected: "expected a datetime.time to serialize a time", + got: utils::get_type_name(py, ptr), + }) } } @@ -290,7 +278,7 @@ pub(super) unsafe fn write_datetime( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::TemporalConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { use chrono::TimeZone; if crate::typeref::check_exact(ptr, crate::typeref::STD_DATETIME_TYPE) { @@ -317,7 +305,7 @@ pub(super) unsafe fn write_datetime( let mut has_overflow: i32 = 0; let num = pyo3::ffi::PyLong_AsLongLongAndOverflow(ptr, &mut has_overflow); if has_overflow != 0 { - return rapidquery_macros::pyerr!(Err(PyOverflowError), "out of range"); + return Err(error::Error::DatetimeOutOfRange); } return match chrono::Utc.timestamp_opt(num, 0) { @@ -325,13 +313,9 @@ pub(super) unsafe fn write_datetime( cfg.datetime_aware.write_to(out, |_, out| { out.write_str(&datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").to_string()) }); - return Ok(()); + Ok(()) } - _ => rapidquery_macros::pyerr!( - Err(PyOverflowError), - "timestamp is invalid for datetime serialization: {}", - num - ), + _ => Err(error::Error::InvalidTimestamp { value: num.to_string() }), }; } @@ -342,21 +326,16 @@ pub(super) unsafe fn write_datetime( cfg.datetime_aware.write_to(out, |_, out| { out.write_str(&datetime.format("%Y-%m-%d %H:%M:%S%.f%:z").to_string()) }); - return Ok(()); + Ok(()) } - _ => rapidquery_macros::pyerr!( - Err(PyOverflowError), - "timestamp is invalid for datetime serialization: {}", - num - ), + _ => Err(error::Error::InvalidTimestamp { value: num.to_string() }), }; } - rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected datetime.datetime/int/float for datetime serialization, got {}", - utils::get_type_name(py, ptr) - ) + Err(error::Error::Expected { + expected: "expected a datetime.datetime, int, or float to serialize a datetime", + got: utils::get_type_name(py, ptr), + }) } pub(super) unsafe fn write_inet( @@ -364,14 +343,19 @@ pub(super) unsafe fn write_inet( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { let Some(tmpl) = &cfg.inet else { - return Err(dialect::errors::FormatError::Unsupported("InetSerializer").into()); + return Err(error::Error::Unsupported { + feature: "inet values are not supported by this dialect", + fix: "set literal.inet to a template such as \"'{}'::inet\"", + }); }; let text = pyobject_string(py, ptr)?; - let net = - ipnetwork::IpNetwork::from_str(&text).map_err(|e| rapidquery_macros::pyerr!(PyValueError, e.to_string()))?; + let net = ipnetwork::IpNetwork::from_str(&text).map_err(|e| error::Error::InvalidInet { + text: text.clone(), + detail: e.to_string(), + })?; let body = net.to_string(); @@ -384,14 +368,19 @@ pub(super) unsafe fn write_macaddr( ptr: *mut pyo3::ffi::PyObject, cfg: &dialect::LiteralConfig, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { let Some(tmpl) = &cfg.macaddr else { - return Err(dialect::errors::FormatError::Unsupported("MacAddressSerializer").into()); + return Err(error::Error::Unsupported { + feature: "macaddr values are not supported by this dialect", + fix: "set literal.macaddr to a template such as \"'{}'::macaddr\"", + }); }; 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 addr = mac_address::MacAddress::from_str(&text).map_err(|e| error::Error::InvalidMac { + text: text.clone(), + detail: e.to_string(), + })?; let body = addr.to_string(); @@ -405,9 +394,12 @@ pub(super) unsafe fn write_array( dialect: &dialect::PyDialect, element: &super::inner::SerializerType, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { let Some(tmpl) = &dialect.literal.array else { - return Err(dialect::errors::FormatError::Unsupported("ArraySerializer").into()); + return Err(error::Error::Unsupported { + feature: "arrays are not supported by this dialect", + fix: "set literal.array to a template such as \"ARRAY[{}]\"", + }); }; let (len, getter): ( @@ -418,11 +410,10 @@ pub(super) unsafe fn write_array( } else if pyo3::ffi::PyTuple_CheckExact(ptr) == 1 { (pyo3::ffi::PyTuple_Size(ptr), |p, i| pyo3::ffi::PyTuple_GetItem(p, i)) } else { - return rapidquery_macros::pyerr!( - Err(PyTypeError), - "expected list/tuple for array serialization, got {}", - utils::get_type_name(py, ptr) - ); + return Err(error::Error::Expected { + expected: "expected a list or tuple to serialize an array", + got: utils::get_type_name(py, ptr), + }); }; tmpl.write_to_result( @@ -434,7 +425,7 @@ pub(super) unsafe fn write_array( let item = getter(ptr, i); if item.is_null() { - return Err(pyo3::PyErr::fetch(py)); + return Err(pyo3::PyErr::fetch(py).into()); } let borrowed = alias::BoundObject::from_borrowed_ptr(py, item); element.serialize(dialect, &borrowed, out)?; @@ -451,16 +442,15 @@ pub(super) unsafe fn write_custom( function: &pyo3::Py, object: &alias::BoundObject<'_>, out: &mut W, -) -> pyo3::PyResult<()> { +) -> error::Result<()> { let _ = ptr; let output = function.call1(py, (dialect.clone(), object))?; if pyo3::ffi::PyUnicode_CheckExact(output.as_ptr()) != 1 { - return rapidquery_macros::pyerr!( - Err(PyTypeError), - "custom serializer must return str, got {}", - utils::get_type_name(py, output.as_ptr()) - ); + return Err(error::Error::Expected { + expected: "expected the custom serializer to return str", + got: utils::get_type_name(py, output.as_ptr()), + }); } out.write_str(utils::borrow_py_str_buffer(py, output.as_ptr())?); diff --git a/src/serializers/inner.rs b/src/serializers/inner.rs index 518f79e..54ee983 100644 --- a/src/serializers/inner.rs +++ b/src/serializers/inner.rs @@ -1,4 +1,5 @@ use crate::dialect::PyDialect; +use crate::error; use crate::internal::alias; use crate::internal::modules; use crate::internal::utils; @@ -71,7 +72,7 @@ impl std::hash::Hash for SerializerType { } impl SerializerType { - pub fn infer_bound(bound: &alias::BoundObject) -> pyo3::PyResult { + pub fn infer_bound(bound: &alias::BoundObject) -> error::Result { unsafe { let object_ptr = bound.as_ptr(); let object_type_ptr = pyo3::ffi::Py_TYPE(object_ptr); @@ -144,11 +145,7 @@ impl SerializerType { return Ok(serializer.get().0.clone()); }, mismatches: { - return rapidquery_macros::pyerr!( - Err(PyTypeError), - "__serializer__ protocol attribute returns non-serializer type: {}", - utils::get_type_name(bound.py(), protocol.as_ptr()) - ) + return Err(error::Error::Expected { expected: "__serializer__ protocol attribute returned a non-serializer", got: utils::get_type_name(bound.py(), protocol.as_ptr()) }); } } @@ -161,7 +158,7 @@ impl SerializerType { dialect: &PyDialect, object: &alias::BoundObject<'_>, out: &mut W, - ) -> pyo3::PyResult<()> { + ) -> error::Result<()> { let py = object.py(); let ptr = object.as_ptr(); diff --git a/src/serializers/mod.rs b/src/serializers/mod.rs index e6f0788..f763c9b 100644 --- a/src/serializers/mod.rs +++ b/src/serializers/mod.rs @@ -5,6 +5,7 @@ mod inner; pub use inner::SerializerType; +use crate::error; use crate::internal::alias; use crate::internal::repr; @@ -32,7 +33,7 @@ impl PySerializer { &self, dialect: pyo3::Bound<'_, crate::dialect::PyDialect>, object: alias::BoundObject<'_>, - ) -> pyo3::PyResult { + ) -> error::Result { let mut buf = String::new(); self.0.serialize(dialect.get(), &object, &mut buf)?; Ok(buf) @@ -96,7 +97,7 @@ macro_rules! impl_unit_serializers { pub fn into_subclasses<'py>( self, py: pyo3::Python<'py>, - ) -> pyo3::PyResult> { + ) -> error::Result> { match &self.0 { $( SerializerType::$variant {..} => { @@ -105,6 +106,7 @@ macro_rules! impl_unit_serializers { pyo3::PyClassInitializer::from(self).add_subclass($name), ) .map(|x| x.into_any()) + .map_err(error::Error::from) } )+ @@ -114,6 +116,7 @@ macro_rules! impl_unit_serializers { pyo3::PyClassInitializer::from(self).add_subclass(PyArraySerializer), ) .map(|x| x.into_any()) + .map_err(error::Error::from) }, SerializerType::Custom(_) => { pyo3::Bound::new( @@ -121,6 +124,7 @@ macro_rules! impl_unit_serializers { pyo3::PyClassInitializer::from(self).add_subclass(PyCustomSerializer), ) .map(|x| x.into_any()) + .map_err(error::Error::from) } } } @@ -192,7 +196,7 @@ impl PyArraySerializer { /// Returns: /// The `Serializer` passed to the constructor. #[getter] - fn get_element(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyResult> { + fn get_element(slf: pyo3::PyRef<'_, Self>) -> error::Result> { let base = slf.as_super(); match &base.0 { SerializerType::Array(element) => PySerializer((**element).clone()).into_subclasses(slf.py()), @@ -210,10 +214,13 @@ pub struct PyCustomSerializer; impl PyCustomSerializer { /// Build a serializer. `function(dialect, object)` must return `str`. #[new] - fn __new__(function: alias::PyObject) -> pyo3::PyResult> { + fn __new__(py: pyo3::Python<'_>, function: alias::PyObject) -> error::Result> { unsafe { if pyo3::ffi::PyCallable_Check(function.as_ptr()) != 1 { - return rapidquery_macros::pyerr!(Err(PyTypeError), "the 'function' parameter must be callable"); + return Err(error::Error::Expected { + expected: "expected a callable for the 'function' parameter", + got: crate::internal::utils::get_type_name(py, function.as_ptr()), + }); } } let inner = SerializerType::Custom(function); @@ -246,7 +253,7 @@ impl PyCustomSerializer { pub struct SerializerExtractor(pub SerializerType); impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for SerializerExtractor { - type Error = pyo3::PyErr; + type Error = error::Error; fn extract(obj: pyo3::prelude::Borrowed<'a, 'py, pyo3::prelude::PyAny>) -> Result { let ptr = obj.as_ptr(); @@ -260,8 +267,9 @@ impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for SerializerExtractor { } } - rapidquery_macros::pyerr!(Err(PyTypeError), "expected Serializer, got {}", unsafe { - crate::internal::utils::get_type_name(obj.py(), obj.as_ptr()) + Err(error::Error::Expected { + expected: "expected a Serializer", + got: unsafe { crate::internal::utils::get_type_name(obj.py(), obj.as_ptr()) }, }) } } diff --git a/src/sql.rs b/src/sql.rs index bd0185f..5489f2d 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -4,6 +4,7 @@ //! [`Parameterized`] binds them as placeholders. use crate::dialect; +use crate::error; use crate::internal::alias; use crate::internal::writer::FastWrite; use crate::serializers::SerializerType; @@ -23,7 +24,7 @@ pub trait SqlWriter: FastWrite { dialect: &dialect::PyDialect, serializer: &SerializerType, object: &alias::PyObject, - ) -> pyo3::PyResult<()>; + ) -> error::Result<()>; } /// SQL buffer that inlines values. It does not keep a parameter list. @@ -79,7 +80,7 @@ impl<'py> SqlWriter for Buffer<'py> { dialect: &dialect::PyDialect, serializer: &SerializerType, object: &alias::PyObject, - ) -> pyo3::PyResult<()> { + ) -> error::Result<()> { use pyo3::types::PyAnyMethods; let bound = object.bind(self.py); @@ -168,7 +169,7 @@ impl SqlWriter for Parameterized<'_> { dialect: &dialect::PyDialect, _serializer: &SerializerType, object: &alias::PyObject, - ) -> pyo3::PyResult<()> { + ) -> error::Result<()> { self.push_param(dialect, object.clone_ref(self.py)); Ok(()) } @@ -190,7 +191,7 @@ impl std::fmt::Write for Parameterized<'_> { /// 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) -> pyo3::PyResult<()>; + fn write_sql(&self, dialect: &dialect::PyDialect, out: &mut W) -> error::Result<()>; } /// Render `stmt` as raw SQL (values inlined as literals). @@ -198,7 +199,7 @@ pub fn write_sql<'py, T: WriteSql>( py: pyo3::Python<'py>, stmt: &T, dialect: &dialect::PyDialect, -) -> pyo3::PyResult { +) -> error::Result { let mut buf = Buffer::with_capacity(py, 256); stmt.write_sql(dialect, &mut buf)?; Ok(buf.into_string()) @@ -209,7 +210,7 @@ pub fn prepare<'py, T: WriteSql>( py: pyo3::Python<'py>, stmt: &T, dialect: &dialect::PyDialect, -) -> pyo3::PyResult<(String, PrepareParams)> { +) -> error::Result<(String, PrepareParams)> { let mut context = Parameterized::with_capacity(py, 256); stmt.write_sql(dialect, &mut context)?; Ok(context.into_parts()) @@ -225,7 +226,7 @@ pub fn write_separated<'a, T, W>( out: &mut W, items: impl IntoIterator, separator: &str, -) -> pyo3::PyResult<()> +) -> error::Result<()> where T: WriteSql + 'a, W: SqlWriter, diff --git a/tests/test_dialect.py b/tests/test_dialect.py index 8c93203..e6425fa 100644 --- a/tests/test_dialect.py +++ b/tests/test_dialect.py @@ -86,8 +86,10 @@ class TemplateCase: 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") @@ -98,9 +100,12 @@ def __init__(self, keys: Sequence[str], minimum: int, maximum: int | None = None 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]: diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 0cf6c31..4ed9203 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -200,7 +200,7 @@ def test_str_and_uuid(self, template: str): assert serializer.serialize(dialect, uuid.UUID(_UUID)) == expected def test_unsupported(self): - with pytest.raises(RuntimeError, match="does not support UuidSerializer"): + with pytest.raises(RuntimeError, match="literal.uuid"): UuidSerializer().serialize(Dialect(), _UUID) def test_invalid_str(self): @@ -251,7 +251,7 @@ 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"): + with pytest.raises(RuntimeError, match="literal.bit"): BitSerializer().serialize(Dialect(), "10") def test_rejects_non_bit_string(self): @@ -399,7 +399,7 @@ 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"): + with pytest.raises(RuntimeError, match="literal.inet"): InetSerializer().serialize(Dialect(), "192.168.1.1") def test_invalid_address(self): @@ -416,7 +416,7 @@ def test_canonical_form(self): assert serializer.serialize(dialect, "01-23-45-67-89-AB") == expected def test_unsupported(self): - with pytest.raises(RuntimeError, match="does not support MacAddressSerializer"): + with pytest.raises(RuntimeError, match="literal.macaddr"): MacAddressSerializer().serialize(Dialect(), "01:23:45:67:89:ab") def test_invalid_address(self): @@ -452,7 +452,7 @@ def test_renders_constructor(self, template: str, expected: str, empty: str, nes assert nested_serializer.serialize(dialect, [[1, 2], [3]]) == nested def test_unsupported(self): - with pytest.raises(RuntimeError, match="does not support ArraySerializer"): + with pytest.raises(RuntimeError, match="literal.array"): ArraySerializer(IntegerSerializer()).serialize(Dialect(), [1, 2]) def test_rejects_non_sequence_and_bad_element(self): @@ -465,7 +465,7 @@ def test_rejects_non_sequence_and_bad_element(self): def test_nested_unsupported_element(self): dialect = _literal(array="ARRAY[{}]") - with pytest.raises(RuntimeError, match="does not support BitSerializer"): + with pytest.raises(RuntimeError, match="literal.bit"): ArraySerializer(BitSerializer()).serialize(dialect, ["10"])