diff --git a/.gitignore b/.gitignore index daf7a07..8d01d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -231,4 +231,5 @@ ROADMAP*.md libryx_core* *.lock -tests/test_compiler.rs \ No newline at end of file +tests/test_compiler.rs +*.txt \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 39e83d2..b9a951c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,11 +4,13 @@ version = 4 [[package]] name = "Ryx" -version = "0.1.0" +version = "0.1.1" dependencies = [ + "criterion", "once_cell", "pyo3", "pyo3-async-runtimes", + "ryx-query", "serde", "serde_json", "sqlx", @@ -42,6 +44,18 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "async-channel" version = "1.9.0" @@ -274,6 +288,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.58" @@ -301,6 +321,58 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -346,6 +418,63 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "futures", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -361,6 +490,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -512,6 +647,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -641,6 +790,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -848,6 +1008,26 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1046,6 +1226,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "parking" version = "2.2.1" @@ -1146,6 +1332,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "polling" version = "3.11.0" @@ -1317,6 +1531,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1335,6 +1569,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1397,6 +1643,27 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "ryx-query" +version = "0.1.0" +dependencies = [ + "once_cell", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tracing", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1845,6 +2112,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" @@ -2046,6 +2323,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2113,6 +2400,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "1.6.1" @@ -2123,6 +2420,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index 0f05266..213363b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Ryx" -version = "0.1.0" +version = "0.1.1" edition = "2024" description = "Ryx ORM — a Django-style Python ORM powered by sqlx (Rust) via PyO3" license = "MIT OR Apache-2.0" @@ -32,6 +32,7 @@ mysql = ["sqlx/mysql"] sqlite = ["sqlx/sqlite"] [dependencies] +ryx-query = { path = "./ryx-query" } # ── PyO3 ────────────────────────────────────────────────────────────────────── # "extension-module" is required when building a cdylib for Python import. # Without it, PyO3 tries to link against libpython, which breaks on Linux/macOS @@ -85,4 +86,5 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } [dev-dependencies] # tokio test macro for async unit tests -tokio = { version = "1.40", features = ["full", "test-util"] } \ No newline at end of file +tokio = { version = "1.40", features = ["full", "test-util"] } +criterion = { version = "0.5", features = ["async_tokio"] } diff --git a/pyproject.toml b/pyproject.toml index 075dfc2..165e779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ build-backend = "maturin" [project] name = "ryx" -version = "0.1.2" +version = "0.1.3" description = "A Django-style Python ORM powered by sqlx (Rust) via PyO3." readme = "README.md" requires-python = ">=3.10" diff --git a/ryx-query/.DS_Store b/ryx-query/.DS_Store new file mode 100644 index 0000000..5db49a5 Binary files /dev/null and b/ryx-query/.DS_Store differ diff --git a/ryx-query/Cargo.toml b/ryx-query/Cargo.toml new file mode 100644 index 0000000..146b139 --- /dev/null +++ b/ryx-query/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ryx-query" +version = "0.1.0" +edition = "2024" +description = "Core query compilation and lookup logic for Ryx ORM" + +[dependencies] +sqlx = { version = "0.8.6", features = ["runtime-tokio", "macros", "chrono", "uuid", "json", "any"], default-features = false } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +once_cell = "1" +tracing = "0.1" + +[dev-dependencies] +criterion = { version = "0.5", features = ["async_tokio"] } + +[[bench]] +name = "query_bench" +harness = false diff --git a/ryx-query/benches/query_bench.rs b/ryx-query/benches/query_bench.rs new file mode 100644 index 0000000..3015f02 --- /dev/null +++ b/ryx-query/benches/query_bench.rs @@ -0,0 +1,103 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use ryx_query::ast::{QNode, SqlValue}; +use ryx_query::compiler::compile_q; +use ryx_query::lookups::init_registry; +use ryx_query::Backend; + +fn criterion_benchmark(c: &mut Criterion) { + // Note: Criterion uses a different API for grouping. + // The above functions were conceptual. Let's use the real Criterion API. + + init_registry(); + + let simple_q = QNode::Leaf { + field: "name".to_string(), + lookup: "exact".to_string(), + value: SqlValue::Text("test".to_string()), + negated: false, + }; + c.bench_function("compile_q_simple", |b| { + b.iter(|| { + let mut values = Vec::new(); + compile_q( + black_box(&simple_q), + &mut values, + black_box(Backend::PostgreSQL), + ) + }) + }); + + let date_q = QNode::Leaf { + field: "created_at".to_string(), + lookup: "year__gte".to_string(), + value: SqlValue::Int(2024), + negated: false, + }; + c.bench_function("compile_q_date_transform", |b| { + b.iter(|| { + let mut values = Vec::new(); + compile_q( + black_box(&date_q), + &mut values, + black_box(Backend::PostgreSQL), + ) + }) + }); + + let json_q = QNode::Leaf { + field: "data".to_string(), + lookup: "has_all".to_string(), + value: SqlValue::List(vec![ + SqlValue::Text("key1".to_string()), + SqlValue::Text("key2".to_string()), + SqlValue::Text("key3".to_string()), + ]), + negated: false, + }; + c.bench_function("compile_q_json_has_all", |b| { + b.iter(|| { + let mut values = Vec::new(); + compile_q( + black_box(&json_q), + &mut values, + black_box(Backend::PostgreSQL), + ) + }) + }); + + let complex_q = QNode::Or(vec![ + QNode::And(vec![ + QNode::Leaf { + field: "active".to_string(), + lookup: "exact".to_string(), + value: SqlValue::Bool(true), + negated: false, + }, + QNode::Leaf { + field: "views".to_string(), + lookup: "gte".to_string(), + value: SqlValue::Int(100), + negated: false, + }, + ]), + QNode::Leaf { + field: "featured".to_string(), + lookup: "exact".to_string(), + value: SqlValue::Bool(true), + negated: false, + }, + ]); + c.bench_function("compile_q_complex_tree", |b| { + b.iter(|| { + let mut values = Vec::new(); + compile_q( + black_box(&complex_q), + &mut values, + black_box(Backend::PostgreSQL), + ) + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/query/ast.rs b/ryx-query/src/ast.rs similarity index 99% rename from src/query/ast.rs rename to ryx-query/src/ast.rs index 0e39aef..13b00c9 100644 --- a/src/query/ast.rs +++ b/ryx-query/src/ast.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; -use crate::pool::Backend; +use crate::Backend; // ### // SqlValue — a Python-safe, DB-bindable value diff --git a/ryx-query/src/backend.rs b/ryx-query/src/backend.rs new file mode 100644 index 0000000..5cdcf8e --- /dev/null +++ b/ryx-query/src/backend.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +/// Database backend type. +/// Used for backend-specific SQL generation (e.g., DATE() vs strftime()). +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum Backend { + PostgreSQL, + MySQL, + SQLite, +} + +/// Detect the backend from a database URL. +pub fn detect_backend(url: &str) -> Backend { + let url_lower = url.to_lowercase(); + if url_lower.contains("postgres") { + Backend::PostgreSQL + } else if url_lower.contains("mysql") { + Backend::MySQL + } else if url_lower.contains("sqlite") { + Backend::SQLite + } else { + Backend::PostgreSQL // default + } +} diff --git a/src/query/compiler/compiler.rs b/ryx-query/src/compiler/compiler.rs similarity index 89% rename from src/query/compiler/compiler.rs rename to ryx-query/src/compiler/compiler.rs index 2768a11..98faa35 100644 --- a/src/query/compiler/compiler.rs +++ b/ryx-query/src/compiler/compiler.rs @@ -7,15 +7,15 @@ // See compiler/mod.rs for the module structure. // ### -use crate::errors::{RyxError, RyxResult}; -use crate::pool::Backend; -use crate::query::ast::{ +use crate::ast::{ AggFunc, AggregateExpr, FilterNode, JoinClause, JoinKind, QNode, QueryNode, QueryOperation, SortDirection, SqlValue, }; -use crate::query::lookups::date_lookups as date; -use crate::query::lookups::json_lookups as json; -use crate::query::lookups::{self, LookupContext}; +use crate::backend::Backend; +use crate::errors::{QueryError, QueryResult}; +use crate::lookups::date_lookups as date; +use crate::lookups::json_lookups as json; +use crate::lookups::{self, LookupContext}; pub use super::helpers::{apply_like_wrapping, qualified_col, split_qualified, KNOWN_TRANSFORMS}; @@ -27,7 +27,7 @@ pub struct CompiledQuery { pub values: Vec, } -pub fn compile(node: &QueryNode) -> RyxResult { +pub fn compile(node: &QueryNode) -> QueryResult { let mut values: Vec = Vec::new(); let sql = match &node.operation { QueryOperation::Select { columns } => { @@ -49,7 +49,7 @@ fn compile_select( node: &QueryNode, columns: Option<&[String]>, values: &mut Vec, -) -> RyxResult { +) -> QueryResult { let base_cols = match columns { None => "*".to_string(), Some(cols) => cols @@ -129,9 +129,9 @@ fn compile_select( Ok(sql) } -fn compile_aggregate(node: &QueryNode, values: &mut Vec) -> RyxResult { +fn compile_aggregate(node: &QueryNode, values: &mut Vec) -> QueryResult { if node.annotations.is_empty() { - return Err(RyxError::Internal( + return Err(QueryError::Internal( "aggregate() called with no aggregate expressions".into(), )); } @@ -153,7 +153,7 @@ fn compile_aggregate(node: &QueryNode, values: &mut Vec) -> RyxResult< Ok(sql) } -fn compile_count(node: &QueryNode, values: &mut Vec) -> RyxResult { +fn compile_count(node: &QueryNode, values: &mut Vec) -> QueryResult { let mut sql = format!("SELECT COUNT(*) FROM {}", helpers::quote_col(&node.table)); if !node.joins.is_empty() { sql.push(' '); @@ -168,7 +168,7 @@ fn compile_count(node: &QueryNode, values: &mut Vec) -> RyxResult) -> RyxResult { +fn compile_delete(node: &QueryNode, values: &mut Vec) -> QueryResult { let mut sql = format!("DELETE FROM {}", helpers::quote_col(&node.table)); let where_sql = compile_where_combined(&node.filters, node.q_filter.as_ref(), values, node.backend)?; @@ -183,9 +183,9 @@ fn compile_update( node: &QueryNode, assignments: &[(String, SqlValue)], values: &mut Vec, -) -> RyxResult { +) -> QueryResult { if assignments.is_empty() { - return Err(RyxError::Internal("UPDATE with no assignments".into())); + return Err(QueryError::Internal("UPDATE with no assignments".into())); } let set: Vec = assignments .iter() @@ -213,9 +213,9 @@ fn compile_insert( cols_vals: &[(String, SqlValue)], returning_id: bool, values: &mut Vec, -) -> RyxResult { +) -> QueryResult { if cols_vals.is_empty() { - return Err(RyxError::Internal("INSERT with no values".into())); + return Err(QueryError::Internal("INSERT with no values".into())); } let (cols, vals): (Vec<_>, Vec<_>) = cols_vals.iter().cloned().unzip(); values.extend(vals); @@ -318,7 +318,7 @@ pub fn compile_agg_cols(anns: &[AggregateExpr]) -> String { .join(", ") } -pub fn compile_order_by(clauses: &[crate::query::ast::OrderByClause]) -> String { +pub fn compile_order_by(clauses: &[crate::ast::OrderByClause]) -> String { clauses .iter() .map(|c| { @@ -337,7 +337,7 @@ fn compile_where_combined( q: Option<&QNode>, values: &mut Vec, backend: Backend, -) -> RyxResult { +) -> QueryResult { let flat = if filters.is_empty() { None } else { @@ -356,7 +356,7 @@ fn compile_where_combined( }) } -pub fn compile_q(q: &QNode, values: &mut Vec, backend: Backend) -> RyxResult { +pub fn compile_q(q: &QNode, values: &mut Vec, backend: Backend) -> QueryResult { match q { QNode::Leaf { field, @@ -368,14 +368,14 @@ pub fn compile_q(q: &QNode, values: &mut Vec, backend: Backend) -> Ryx let parts: Vec = children .iter() .map(|c| compile_q(c, values, backend)) - .collect::>()?; + .collect::>()?; Ok(format!("({})", parts.join(" AND "))) } QNode::Or(children) => { let parts: Vec = children .iter() .map(|c| compile_q(c, values, backend)) - .collect::>()?; + .collect::>()?; Ok(format!("({})", parts.join(" OR "))) } QNode::Not(child) => { @@ -389,11 +389,11 @@ fn compile_filters( filters: &[FilterNode], values: &mut Vec, backend: Backend, -) -> RyxResult { +) -> QueryResult { let parts: Vec = filters .iter() .map(|f| compile_single_filter(&f.field, &f.lookup, &f.value, f.negated, values, backend)) - .collect::>()?; + .collect::>()?; Ok(parts.join(" AND ")) } @@ -404,7 +404,7 @@ fn compile_single_filter( negated: bool, values: &mut Vec, backend: Backend, -) -> RyxResult { +) -> QueryResult { let (base_column, applied_transforms, json_key) = if field.contains("__") { let parts: Vec<&str> = field.split("__").collect(); @@ -533,7 +533,7 @@ fn compile_single_filter( if lookup == "range" { let (lo, hi) = match value { SqlValue::List(v) if v.len() == 2 => (v[0].clone(), v[1].clone()), - _ => return Err(RyxError::Internal("range needs exactly 2 values".into())), + _ => return Err(QueryError::Internal("range needs exactly 2 values".into())), }; values.push(lo); values.push(hi); @@ -557,24 +557,25 @@ fn compile_single_filter( if KNOWN_TRANSFORMS.contains(&lookup) { let transform_fn = match lookup { - "date" => date::date_transform as crate::query::lookups::LookupFn, - "year" => date::year_transform as crate::query::lookups::LookupFn, - "month" => date::month_transform as crate::query::lookups::LookupFn, - "day" => date::day_transform as crate::query::lookups::LookupFn, - "hour" => date::hour_transform as crate::query::lookups::LookupFn, - "minute" => date::minute_transform as crate::query::lookups::LookupFn, - "second" => date::second_transform as crate::query::lookups::LookupFn, - "week" => date::week_transform as crate::query::lookups::LookupFn, - "dow" => date::dow_transform as crate::query::lookups::LookupFn, - "quarter" => date::quarter_transform as crate::query::lookups::LookupFn, - "time" => date::time_transform as crate::query::lookups::LookupFn, - "iso_week" => date::iso_week_transform as crate::query::lookups::LookupFn, - "iso_dow" => date::iso_dow_transform as crate::query::lookups::LookupFn, - "key" => json::json_key_transform as crate::query::lookups::LookupFn, - "key_text" => json::json_key_text_transform as crate::query::lookups::LookupFn, - "json" => json::json_cast_transform as crate::query::lookups::LookupFn, + "date" => date::date_transform as crate::lookups::LookupFn, + "year" => date::year_transform as crate::lookups::LookupFn, + "month" => date::month_transform as crate::lookups::LookupFn, + "day" => date::day_transform as crate::lookups::LookupFn, + "hour" => date::hour_transform as crate::lookups::LookupFn, + "minute" => date::minute_transform as crate::lookups::LookupFn, + "second" => date::second_transform as crate::lookups::LookupFn, + "week" => date::week_transform as crate::lookups::LookupFn, + "dow" => date::dow_transform as crate::lookups::LookupFn, + "quarter" => date::quarter_transform as crate::lookups::LookupFn, + "time" => date::time_transform as crate::lookups::LookupFn, + "iso_week" => date::iso_week_transform as crate::lookups::LookupFn, + "iso_dow" => date::iso_dow_transform as crate::lookups::LookupFn, + "key" => json::json_key_transform as crate::lookups::LookupFn, + "key_text" => json::json_key_text_transform as crate::lookups::LookupFn, + "json" => json::json_cast_transform as crate::lookups::LookupFn, + _ => { - return Err(RyxError::UnknownLookup { + return Err(QueryError::UnknownLookup { field: field.to_string(), lookup: lookup.to_string(), }) @@ -597,7 +598,7 @@ fn compile_single_filter( #[cfg(test)] mod tests { use super::*; - use crate::query::ast::*; + use crate::ast::*; #[test] fn test_bare_select() { @@ -699,6 +700,6 @@ mod tests { } fn init_registry() { - crate::query::lookups::init_registry(); + crate::lookups::init_registry(); } } diff --git a/src/query/compiler/helpers.rs b/ryx-query/src/compiler/helpers.rs similarity index 98% rename from src/query/compiler/helpers.rs rename to ryx-query/src/compiler/helpers.rs index 9d039db..27d18a0 100644 --- a/src/query/compiler/helpers.rs +++ b/ryx-query/src/compiler/helpers.rs @@ -9,7 +9,7 @@ // - Other compilation utilities // ### -use crate::query::ast::SqlValue; +use crate::ast::SqlValue; /// Double-quote a simple identifier (column or table name). pub fn quote_col(s: &str) -> String { diff --git a/src/query/compiler/mod.rs b/ryx-query/src/compiler/mod.rs similarity index 100% rename from src/query/compiler/mod.rs rename to ryx-query/src/compiler/mod.rs diff --git a/ryx-query/src/errors.rs b/ryx-query/src/errors.rs new file mode 100644 index 0000000..940f1b6 --- /dev/null +++ b/ryx-query/src/errors.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum QueryError { + #[error("Unknown lookup: '{lookup}' on field '{field}'")] + UnknownLookup { field: String, lookup: String }, + + #[error("Unknown field '{field}' on model '{model}'")] + UnknownField { field: String, model: String }, + + #[error("Type mismatch for field '{field}': expected {expected}, got {got}")] + TypeMismatch { + field: String, + expected: String, + got: String, + }, + + #[error("Internal query error: {0}")] + Internal(String), +} + +pub type QueryResult = Result; diff --git a/ryx-query/src/lib.rs b/ryx-query/src/lib.rs new file mode 100644 index 0000000..302add8 --- /dev/null +++ b/ryx-query/src/lib.rs @@ -0,0 +1,8 @@ +pub mod ast; +pub mod backend; +pub mod compiler; +pub mod errors; +pub mod lookups; + +pub use backend::Backend; +pub use errors::{QueryError, QueryResult}; diff --git a/src/query/lookups/common_lookups.rs b/ryx-query/src/lookups/common_lookups.rs similarity index 95% rename from src/query/lookups/common_lookups.rs rename to ryx-query/src/lookups/common_lookups.rs index 880d2b1..ade130e 100644 --- a/src/query/lookups/common_lookups.rs +++ b/ryx-query/src/lookups/common_lookups.rs @@ -6,10 +6,10 @@ // Contains comparison and string lookups (exact, gt, contains, etc.) // ### -use crate::query::lookups::LookupContext; +use crate::lookups::LookupContext; -pub use crate::query::lookups::LookupFn; -pub use crate::query::lookups::PythonLookup; +pub use crate::lookups::LookupFn; +pub use crate::lookups::PythonLookup; /// `field__exact=value` → `field = ?` /// diff --git a/src/query/lookups/date_lookups.rs b/ryx-query/src/lookups/date_lookups.rs similarity index 98% rename from src/query/lookups/date_lookups.rs rename to ryx-query/src/lookups/date_lookups.rs index 323c4d8..bdd4bbf 100644 --- a/src/query/lookups/date_lookups.rs +++ b/ryx-query/src/lookups/date_lookups.rs @@ -7,10 +7,10 @@ // These are used for chained lookups like `created_at__year__gte=2024` // ### -use crate::pool::Backend; -use crate::query::lookups::LookupContext; +use crate::backend::Backend; +use crate::lookups::LookupContext; -pub use crate::query::lookups::LookupFn; +pub use crate::lookups::LookupFn; /// Apply a date/time field transformation. /// Returns SQL like "DATE(col)" or "EXTRACT(YEAR FROM col)" diff --git a/src/query/lookups/json_lookups.rs b/ryx-query/src/lookups/json_lookups.rs similarity index 92% rename from src/query/lookups/json_lookups.rs rename to ryx-query/src/lookups/json_lookups.rs index 5c5f591..be35f02 100644 --- a/src/query/lookups/json_lookups.rs +++ b/ryx-query/src/lookups/json_lookups.rs @@ -7,10 +7,10 @@ // These are used for chained lookups like `metadata__key__priority__exact="high"` // ### -use crate::pool::Backend; -use crate::query::lookups::LookupContext; +use crate::backend::Backend; +use crate::lookups::LookupContext; -pub use crate::query::lookups::LookupFn; +pub use crate::lookups::LookupFn; /// Apply a JSON field transformation. /// Returns SQL like `(col->>'key')` or `JSON_UNQUOTE(JSON_EXTRACT(col, '$.key'))` @@ -103,10 +103,7 @@ pub fn json_has_any(ctx: &LookupContext) -> String { match ctx.backend { Backend::PostgreSQL => format!("({} ?| ?)", ctx.column), Backend::MySQL => format!("JSON_CONTAINS_PATH({}, 'one', (?))", ctx.column), - Backend::SQLite => format!( - "json_extract({}, '$.' || ?) IS NOT NULL (?)", - ctx.column - ), // Template + Backend::SQLite => format!("json_extract({}, '$.' || ?) IS NOT NULL (?)", ctx.column), // Template } } @@ -115,10 +112,7 @@ pub fn json_has_all(ctx: &LookupContext) -> String { match ctx.backend { Backend::PostgreSQL => format!("({} ?& ?)", ctx.column), Backend::MySQL => format!("JSON_CONTAINS_PATH({}, 'all', (?))", ctx.column), - Backend::SQLite => format!( - "json_extract({}, '$.' || ?) IS NOT NULL (?)", - ctx.column - ), // Template + Backend::SQLite => format!("json_extract({}, '$.' || ?) IS NOT NULL (?)", ctx.column), // Template } } diff --git a/src/query/lookups/lookups.rs b/ryx-query/src/lookups/lookups.rs similarity index 91% rename from src/query/lookups/lookups.rs rename to ryx-query/src/lookups/lookups.rs index 2994375..c781ace 100644 --- a/src/query/lookups/lookups.rs +++ b/ryx-query/src/lookups/lookups.rs @@ -10,13 +10,14 @@ use std::collections::HashMap; use std::sync::{OnceLock, RwLock}; -use crate::errors::{RyxError, RyxResult}; -use crate::pool::Backend; +// Removed unused SqlValue import +use crate::backend::Backend; +use crate::errors::{QueryError, QueryResult}; // Re-export submodules -pub use crate::query::lookups::common_lookups; -pub use crate::query::lookups::date_lookups; -pub use crate::query::lookups::json_lookups; +pub use crate::lookups::common_lookups; +pub use crate::lookups::date_lookups; +pub use crate::lookups::json_lookups; // ### // Core types @@ -109,14 +110,17 @@ pub fn init_registry() { // Registry public API // ### -pub fn register_custom(name: impl Into, sql_template: impl Into) -> RyxResult<()> { +pub fn register_custom( + name: impl Into, + sql_template: impl Into, +) -> QueryResult<()> { let registry = REGISTRY .get() - .ok_or_else(|| RyxError::Internal("Lookup registry not initialized".into()))?; + .ok_or_else(|| QueryError::Internal("Lookup registry not initialized".into()))?; let mut guard = registry .write() - .map_err(|e| RyxError::Internal(format!("Registry lock poisoned: {e}")))?; + .map_err(|e| QueryError::Internal(format!("Registry lock poisoned: {e}")))?; guard.custom.insert( name.into(), @@ -128,14 +132,14 @@ pub fn register_custom(name: impl Into, sql_template: impl Into) Ok(()) } -fn resolve_simple(field: &str, lookup_name: &str, ctx: &LookupContext) -> RyxResult { +fn resolve_simple(field: &str, lookup_name: &str, ctx: &LookupContext) -> QueryResult { let registry = REGISTRY .get() - .ok_or_else(|| RyxError::Internal("Lookup registry not initialized".into()))?; + .ok_or_else(|| QueryError::Internal("Lookup registry not initialized".into()))?; let guard = registry .read() - .map_err(|e| RyxError::Internal(format!("Registry lock poisoned: {e}")))?; + .map_err(|e| QueryError::Internal(format!("Registry lock poisoned: {e}")))?; if let Some(custom) = guard.custom.get(lookup_name) { return Ok(custom.sql_template.replace("{col}", &ctx.column)); @@ -145,7 +149,7 @@ fn resolve_simple(field: &str, lookup_name: &str, ctx: &LookupContext) -> RyxRes return Ok(lookup_fn(ctx)); } - Err(RyxError::UnknownLookup { + Err(QueryError::UnknownLookup { field: field.to_string(), lookup: lookup_name.to_string(), }) @@ -153,20 +157,20 @@ fn resolve_simple(field: &str, lookup_name: &str, ctx: &LookupContext) -> RyxRes /// Returns the list of all registered lookup names (built-in + custom). /// Used by the Python layer for available_lookups(). -pub fn registered_lookups() -> RyxResult> { +pub fn registered_lookups() -> QueryResult> { let registry = REGISTRY .get() - .ok_or_else(|| RyxError::Internal("Lookup registry not initialized".into()))?; + .ok_or_else(|| QueryError::Internal("Lookup registry not initialized".into()))?; let guard = registry .read() - .map_err(|e| RyxError::Internal(format!("Registry lock poisoned: {e}")))?; + .map_err(|e| QueryError::Internal(format!("Registry lock poisoned: {e}")))?; let mut names: Vec = guard .builtin .keys() .copied() - .map(|k| k.to_string()) + .map(|k: &'static str| k.to_string()) .chain(guard.custom.keys().cloned()) .collect(); names.sort(); @@ -242,7 +246,7 @@ fn handle_sqlite_transform_lookup( _transform: &str, lookup_name: &str, ctx: &LookupContext, -) -> RyxResult { +) -> QueryResult { let is_numeric_comparison = matches!(lookup_name, "gt" | "gte" | "lt" | "lte" | "exact"); if is_numeric_comparison && ctx.column.contains("AS TEXT)") { @@ -270,7 +274,7 @@ fn add_sqlite_cast_for_transform(fragment: &str, lookup: &str) -> String { } } -pub fn resolve(field: &str, lookup_name: &str, ctx: &LookupContext) -> RyxResult { +pub fn resolve(field: &str, lookup_name: &str, ctx: &LookupContext) -> QueryResult { if !lookup_name.contains("__") { if ctx.json_key.is_some() { let mut column = format!("\"{}\"", field); @@ -381,7 +385,7 @@ pub fn apply_transform( column: &str, backend: Backend, key: Option<&str>, -) -> RyxResult { +) -> QueryResult { if let Some(sql) = date_lookups::apply_date_transform(name, column, backend) { return Ok(sql); } @@ -393,7 +397,7 @@ pub fn apply_transform( return Ok(format!("DATE({})", column)); } - Err(RyxError::UnknownLookup { + Err(QueryError::UnknownLookup { field: column.to_string(), lookup: name.to_string(), }) diff --git a/src/query/lookups/mod.rs b/ryx-query/src/lookups/mod.rs similarity index 100% rename from src/query/lookups/mod.rs rename to ryx-query/src/lookups/mod.rs index fc4fe4d..ca1b297 100644 --- a/src/query/lookups/mod.rs +++ b/ryx-query/src/lookups/mod.rs @@ -27,10 +27,10 @@ pub use lookups::LookupFn; pub use lookups::PythonLookup; // Re-export functions from lookups.rs +pub use lookups::all_lookups; +pub use lookups::all_transforms; pub use lookups::apply_transform; pub use lookups::init_registry; pub use lookups::register_custom; pub use lookups::registered_lookups; pub use lookups::resolve; -pub use lookups::all_lookups; -pub use lookups::all_transforms; diff --git a/src/errors.rs b/src/errors.rs index 208f332..a9b78f4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -23,6 +23,7 @@ use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; +use ryx_query::QueryError; use thiserror::Error; /// The master error type for the entire Ryx ORM. @@ -39,6 +40,10 @@ pub enum RyxError { #[error("Database error: {0}")] Database(#[from] sqlx::Error), + /// Errors from the query compiler. + #[error("Query error: {0}")] + Query(#[from] QueryError), + /// Raised when `.get()` or `.first()` finds no matching row. /// Mirrors Django's `Model.DoesNotExist`. #[error("No matching object found for the given query")] @@ -60,27 +65,6 @@ pub enum RyxError { #[error("Connection pool already initialized")] PoolAlreadyInitialized, - // Query building errors - /// Raised when the Python side passes an unrecognized lookup suffix. - /// Example: `filter(age__foobar=42)` where "foobar" is not a registered - /// lookup. We include the lookup name so the error is actionable. - #[error("Unknown lookup: '{lookup}' on field '{field}'")] - UnknownLookup { field: String, lookup: String }, - - /// Raised when a field name referenced in a filter/order_by doesn't exist - /// on the model's declared schema. - #[error("Unknown field '{field}' on model '{model}'")] - UnknownField { field: String, model: String }, - - /// Raised when a Python value cannot be converted to the expected SQL type. - /// Example: passing a string where an integer is expected. - #[error("Type mismatch for field '{field}': expected {expected}, got {got}")] - TypeMismatch { - field: String, - expected: String, - got: String, - }, - // Runtime / internal errors /// Catch-all for internal errors that shouldn't reach users but are /// wrapped here so we don't use `.unwrap()` anywhere in the codebase. @@ -107,13 +91,12 @@ pub enum RyxError { impl From for PyErr { fn from(err: RyxError) -> PyErr { match &err { - // User errors (bad field names, bad lookups, bad types) → - // ValueError so Python linters/type checkers can catch them - RyxError::UnknownLookup { .. } - | RyxError::UnknownField { .. } - | RyxError::TypeMismatch { .. } => PyValueError::new_err(err.to_string()), - - // Everything else → RuntimeError with full context message + RyxError::Query(qe) => match qe { + QueryError::UnknownLookup { .. } + | QueryError::UnknownField { .. } + | QueryError::TypeMismatch { .. } => PyValueError::new_err(qe.to_string()), + QueryError::Internal(_) => PyRuntimeError::new_err(qe.to_string()), + }, _ => PyRuntimeError::new_err(err.to_string()), } } diff --git a/src/executor.rs b/src/executor.rs index 6c0cbbf..585f293 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -45,7 +45,7 @@ use tracing::{debug, instrument}; use crate::errors::{RyxError, RyxResult}; use crate::pool; -use crate::query::{ast::SqlValue, compiler::CompiledQuery}; +use ryx_query::{ast::SqlValue, compiler::CompiledQuery}; use crate::transaction; // ### diff --git a/src/lib.rs b/src/lib.rs index 7b7ac49..0a7e04c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,16 +10,16 @@ use tokio::sync::Mutex as TokioMutex; pub mod errors; pub mod executor; pub mod pool; -pub mod query; pub mod transaction; +use crate::errors::RyxError; use crate::pool::PoolConfig; -use crate::query::ast::{ +use ryx_query::ast::{ AggFunc, AggregateExpr, FilterNode, JoinClause, JoinKind, OrderByClause, QNode, QueryNode, QueryOperation, SqlValue, }; -use crate::query::compiler; -use crate::query::lookups; +use ryx_query::compiler; +use ryx_query::lookups; use crate::transaction::TransactionHandle; // ### @@ -59,12 +59,12 @@ fn setup<'py>( #[pyfunction] fn register_lookup(name: String, sql_template: String) -> PyResult<()> { - lookups::register_custom(name, sql_template).map_err(PyErr::from) + lookups::register_custom(name, sql_template).map_err(RyxError::from).map_err(PyErr::from) } #[pyfunction] fn available_lookups() -> PyResult> { - lookups::registered_lookups().map_err(PyErr::from) + lookups::registered_lookups().map_err(RyxError::from).map_err(PyErr::from) } #[pyfunction] @@ -134,7 +134,7 @@ impl PyQueryBuilder { #[new] fn new(table: String) -> PyResult { // Get the backend from the pool at QueryBuilder creation time - let backend = pool::get_backend().unwrap_or(crate::pool::Backend::PostgreSQL); + let backend = pool::get_backend().unwrap_or(ryx_query::Backend::PostgreSQL); Ok(Self { node: QueryNode::select(table).with_backend(backend), @@ -254,7 +254,7 @@ impl PyQueryBuilder { // # Execution methods fn fetch_all<'py>(&self, py: Python<'py>) -> PyResult> { - let compiled = compiler::compile(&self.node).map_err(PyErr::from)?; + let compiled = compiler::compile(&self.node).map_err(RyxError::from)?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let rows = executor::fetch_all(compiled).await.map_err(PyErr::from)?; Python::attach(|py| Ok(decoded_rows_to_py(py, rows)?.unbind())) @@ -263,7 +263,7 @@ impl PyQueryBuilder { fn fetch_first<'py>(&self, py: Python<'py>) -> PyResult> { let node = self.node.clone().with_limit(1); - let compiled = compiler::compile(&node).map_err(PyErr::from)?; + let compiled = compiler::compile(&node).map_err(RyxError::from)?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let rows = executor::fetch_all(compiled).await.map_err(PyErr::from)?; Python::attach(|py| match rows.into_iter().next() { @@ -274,7 +274,7 @@ impl PyQueryBuilder { } fn fetch_get<'py>(&self, py: Python<'py>) -> PyResult> { - let compiled = compiler::compile(&self.node).map_err(PyErr::from)?; + let compiled = compiler::compile(&self.node).map_err(RyxError::from)?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let row = executor::fetch_one(compiled).await.map_err(PyErr::from)?; Python::attach(|py| Ok(decoded_row_to_py(py, row)?.into_any().unbind())) @@ -284,7 +284,7 @@ impl PyQueryBuilder { fn fetch_count<'py>(&self, py: Python<'py>) -> PyResult> { let mut count_node = self.node.clone(); count_node.operation = QueryOperation::Count; - let compiled = compiler::compile(&count_node).map_err(PyErr::from)?; + let compiled = compiler::compile(&count_node).map_err(RyxError::from)?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let count = executor::fetch_count(compiled).await.map_err(PyErr::from)?; Python::attach(|py| Ok(count.into_pyobject(py)?.unbind())) @@ -294,7 +294,7 @@ impl PyQueryBuilder { fn fetch_aggregate<'py>(&self, py: Python<'py>) -> PyResult> { let mut agg_node = self.node.clone(); agg_node.operation = QueryOperation::Aggregate; - let compiled = compiler::compile(&agg_node).map_err(PyErr::from)?; + let compiled = compiler::compile(&agg_node).map_err(RyxError::from)?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let rows = executor::fetch_all(compiled).await.map_err(PyErr::from)?; Python::attach(|py| match rows.into_iter().next() { @@ -307,7 +307,7 @@ impl PyQueryBuilder { fn execute_delete<'py>(&self, py: Python<'py>) -> PyResult> { let mut del_node = self.node.clone(); del_node.operation = QueryOperation::Delete; - let compiled = compiler::compile(&del_node).map_err(PyErr::from)?; + let compiled = compiler::compile(&del_node).map_err(RyxError::from)?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let res = executor::execute(compiled).await.map_err(PyErr::from)?; Python::attach(|py| Ok(res.rows_affected.into_pyobject(py)?.unbind())) @@ -328,7 +328,7 @@ impl PyQueryBuilder { upd_node.operation = QueryOperation::Update { assignments: rust_assignments, }; - let compiled = compiler::compile(&upd_node).map_err(PyErr::from)?; + let compiled = compiler::compile(&upd_node).map_err(RyxError::from)?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let res = executor::execute(compiled).await.map_err(PyErr::from)?; @@ -352,7 +352,7 @@ impl PyQueryBuilder { values: rust_values, returning_id, }; - let compiled = compiler::compile(&ins_node).map_err(PyErr::from)?; + let compiled = compiler::compile(&ins_node).map_err(RyxError::from)?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let res = executor::execute(compiled).await.map_err(PyErr::from)?; @@ -364,7 +364,7 @@ impl PyQueryBuilder { } fn compiled_sql(&self) -> PyResult { - Ok(compiler::compile(&self.node).map_err(PyErr::from)?.sql) + Ok(compiler::compile(&self.node).map_err(RyxError::from)?.sql) } } diff --git a/src/pool.rs b/src/pool.rs index d83ace7..38dd92d 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -34,32 +34,7 @@ use sqlx::{ use tracing::{debug, info}; use crate::errors::{RyxError, RyxResult}; - -// ### -// Backend enum -// ### -/// Database backend type. -/// Used for backend-specific SQL generation (e.g., DATE() vs strftime()). -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -pub enum Backend { - PostgreSQL, - MySQL, - SQLite, -} - -/// Detect the backend from a database URL. -pub fn detect_backend(url: &str) -> Backend { - let url_lower = url.to_lowercase(); - if url_lower.contains("postgres") { - Backend::PostgreSQL - } else if url_lower.contains("mysql") { - Backend::MySQL - } else if url_lower.contains("sqlite") { - Backend::SQLite - } else { - Backend::PostgreSQL // default - } -} +use ryx_query::Backend; // ### // Global singleton @@ -173,7 +148,7 @@ pub async fn initialize(database_url: &str, config: PoolConfig) -> RyxResult<()> .map_err(|_| RyxError::PoolAlreadyInitialized)?; // Set the backend type based on the URL - let backend = detect_backend(database_url); + let backend = ryx_query::backend::detect_backend(database_url); BACKEND.set(backend).ok(); info!("Ryx connection pool initialized successfully"); diff --git a/src/query/mod.rs b/src/query/mod.rs deleted file mode 100644 index 60df9c9..0000000 --- a/src/query/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -// -// ### -// Ryx — Query module -// -// This module contains everything related to building and compiling queries: -// - ast.rs : the query abstract syntax tree (data structures) -// - lookup.rs : the lookup registry (built-in + user-registered lookups) -// - compiler.rs : AST → SQL string + bound values -// ### - -pub mod ast; -pub mod compiler; -pub mod lookups; diff --git a/src/transaction.rs b/src/transaction.rs index 7fe5c02..d5740fd 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -35,8 +35,8 @@ use tracing::{debug, instrument}; use crate::errors::{RyxError, RyxResult}; use crate::pool; -use crate::query::ast::SqlValue; -use crate::query::compiler::CompiledQuery; +use ryx_query::ast::SqlValue; +use ryx_query::compiler::CompiledQuery; static ACTIVE_TX: OnceCell>>>>> = OnceCell::new(); @@ -137,7 +137,7 @@ impl TransactionHandle { /// /// The query is run on the transaction's connection (not the pool), so it /// participates in the current transaction boundary. - #[instrument(skip(self, query), fields(sql = %query.sql))] + // #[instrument(skip(self, query), fields(sql = %query.sql))] pub async fn execute_query(&self, query: CompiledQuery) -> RyxResult { let mut guard = self.inner.lock().await; let tx = guard.as_mut().ok_or_else(|| {