diff --git a/crates/rustango/src/sql/executor/mod.rs b/crates/rustango/src/sql/executor/mod.rs index c992554..ae18f46 100644 --- a/crates/rustango/src/sql/executor/mod.rs +++ b/crates/rustango/src/sql/executor/mod.rs @@ -1549,6 +1549,7 @@ impl UpdaterPool for UpdateBuilder { /// `PgArguments` and `SqlValue::Array` binding as a typed PG array, /// neither of which exists on MySQL / SQLite. The bi-directional /// counterparts are `bind_match_mysql!` + `bind_match_sqlite!`. +// Macros are made visible to sibling modules below via `pub(super) use`. #[cfg(feature = "postgres")] macro_rules! bind_match { ($q:expr, $value:expr) => { @@ -1701,6 +1702,15 @@ macro_rules! bind_match_sqlite { }; } +// Sibling-module access to the bind macros (values.rs and future +// extractions need them at `super::` scope). +#[cfg(feature = "postgres")] +pub(super) use bind_match; +#[cfg(feature = "mysql")] +pub(super) use bind_match_mysql; +#[cfg(feature = "sqlite")] +pub(super) use bind_match_sqlite; + #[cfg(feature = "postgres")] fn bind_query_as( q: QueryAs<'_, sqlx::Postgres, T, PgArguments>, @@ -1710,7 +1720,7 @@ fn bind_query_as( } #[cfg(feature = "postgres")] -fn bind_query( +pub(super) fn bind_query( q: Query<'_, sqlx::Postgres, PgArguments>, value: SqlValue, ) -> Query<'_, sqlx::Postgres, PgArguments> { @@ -2139,7 +2149,7 @@ use super::Pool; /// the Postgres-typed [`bind_query`] using the same polymorphic /// `bind_match!` body. #[cfg(feature = "mysql")] -fn bind_query_my( +pub(super) fn bind_query_my( q: sqlx::query::Query<'_, sqlx::MySql, sqlx::mysql::MySqlArguments>, value: SqlValue, ) -> sqlx::query::Query<'_, sqlx::MySql, sqlx::mysql::MySqlArguments> { @@ -2152,7 +2162,7 @@ fn bind_query_my( /// values go through the `json` feature into TEXT — both feature flags /// are pulled in by the runtime feature set when `sqlite` is on). #[cfg(feature = "sqlite")] -fn bind_query_sqlite<'a>( +pub(super) fn bind_query_sqlite<'a>( q: sqlx::query::Query<'a, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'a>>, value: SqlValue, ) -> sqlx::query::Query<'a, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'a>> { @@ -3003,376 +3013,15 @@ where // ==================================================================== // Pure projection — Django `.values()` / `.values_list()` (issue #22) +// Extracted to values.rs (#116 step 3). // ==================================================================== -/// Decode one per-dialect raw `SqlValue` from the i-th column of a row. -/// Same probe order as the aggregate-row decoder so the two paths agree -/// on how mixed types come back: scalars first, then jsonb / arrays -/// (PG only). NULLs (or unrecognized types) fall through to -/// [`SqlValue::Null`]. -#[cfg(feature = "postgres")] -fn pg_cell_to_sqlvalue(row: &PgRow, i: usize) -> SqlValue { - use sqlx::Row as _; - if let Ok(v) = row.try_get::(i) { - SqlValue::I64(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::I32(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::F64(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::Bool(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::String(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::Json(v) - } else { - SqlValue::Null - } -} - -#[cfg(feature = "mysql")] -fn my_cell_to_sqlvalue(row: &sqlx::mysql::MySqlRow, i: usize) -> SqlValue { - use sqlx::Row as _; - if let Ok(v) = row.try_get::(i) { - SqlValue::I64(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::I32(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::F64(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::Bool(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::String(v) - } else { - SqlValue::Null - } -} - -#[cfg(feature = "sqlite")] -fn sqlite_cell_to_sqlvalue(row: &sqlx::sqlite::SqliteRow, i: usize) -> SqlValue { - use sqlx::Row as _; - if let Ok(v) = row.try_get::(i) { - SqlValue::I64(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::I32(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::F64(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::Bool(v) - } else if let Ok(v) = row.try_get::(i) { - SqlValue::String(v) - } else { - SqlValue::Null - } -} - -/// Execute a [`SelectQuery`] (with `projection` set) and return each -/// row as a `HashMap` keyed by column name. -/// Backs [`crate::query::ValuesQuerySet::fetch`]. Issue #22. -/// -/// # Errors -/// SQL compilation or driver failure. -pub async fn fetch_values_dict( - pool: &Pool, - query: &SelectQuery, -) -> Result>, ExecError> { - let stmt = pool.dialect().compile_select(query)?; - match pool { - #[cfg(feature = "postgres")] - Pool::Postgres(pg) => { - let mut q: Query<'_, sqlx::Postgres, PgArguments> = sqlx::query(&stmt.sql); - for v in stmt.params { - q = bind_query(q, v); - } - let rows = q.fetch_all(pg).await?; - let mut out = Vec::with_capacity(rows.len()); - for row in &rows { - use sqlx::Column as _; - use sqlx::Row as _; - let mut map = std::collections::HashMap::new(); - for (i, col) in row.columns().iter().enumerate() { - map.insert(col.name().to_owned(), pg_cell_to_sqlvalue(row, i)); - } - out.push(map); - } - Ok(out) - } - #[cfg(feature = "mysql")] - Pool::Mysql(my) => { - let mut q: sqlx::query::Query<'_, sqlx::MySql, sqlx::mysql::MySqlArguments> = - sqlx::query(&stmt.sql); - for v in stmt.params { - q = bind_query_my(q, v); - } - let rows = q.fetch_all(my).await?; - let mut out = Vec::with_capacity(rows.len()); - for row in &rows { - use sqlx::Column as _; - use sqlx::Row as _; - let mut map = std::collections::HashMap::new(); - for (i, col) in row.columns().iter().enumerate() { - map.insert(col.name().to_owned(), my_cell_to_sqlvalue(row, i)); - } - out.push(map); - } - Ok(out) - } - #[cfg(feature = "sqlite")] - Pool::Sqlite(sq) => { - let mut q: sqlx::query::Query<'_, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'_>> = - sqlx::query(&stmt.sql); - for v in stmt.params { - q = bind_query_sqlite(q, v); - } - let rows = q.fetch_all(sq).await?; - let mut out = Vec::with_capacity(rows.len()); - for row in &rows { - use sqlx::Column as _; - use sqlx::Row as _; - let mut map = std::collections::HashMap::new(); - for (i, col) in row.columns().iter().enumerate() { - map.insert(col.name().to_owned(), sqlite_cell_to_sqlvalue(row, i)); - } - out.push(map); - } - Ok(out) - } - } -} - -/// Execute a [`SelectQuery`] (with `projection` set) and return each -/// row as a `Vec` ordered to match the projection's column -/// list. Backs [`crate::query::ValuesListQuerySet::fetch`]. -/// Issue #22. -/// -/// # Errors -/// SQL compilation or driver failure. -pub async fn fetch_values_list( - pool: &Pool, - query: &SelectQuery, -) -> Result>, ExecError> { - let stmt = pool.dialect().compile_select(query)?; - match pool { - #[cfg(feature = "postgres")] - Pool::Postgres(pg) => { - let mut q: Query<'_, sqlx::Postgres, PgArguments> = sqlx::query(&stmt.sql); - for v in stmt.params { - q = bind_query(q, v); - } - let rows = q.fetch_all(pg).await?; - let mut out = Vec::with_capacity(rows.len()); - for row in &rows { - use sqlx::Row as _; - let n = row.columns().len(); - let mut v = Vec::with_capacity(n); - for i in 0..n { - v.push(pg_cell_to_sqlvalue(row, i)); - } - out.push(v); - } - Ok(out) - } - #[cfg(feature = "mysql")] - Pool::Mysql(my) => { - let mut q: sqlx::query::Query<'_, sqlx::MySql, sqlx::mysql::MySqlArguments> = - sqlx::query(&stmt.sql); - for v in stmt.params { - q = bind_query_my(q, v); - } - let rows = q.fetch_all(my).await?; - let mut out = Vec::with_capacity(rows.len()); - for row in &rows { - use sqlx::Row as _; - let n = row.columns().len(); - let mut v = Vec::with_capacity(n); - for i in 0..n { - v.push(my_cell_to_sqlvalue(row, i)); - } - out.push(v); - } - Ok(out) - } - #[cfg(feature = "sqlite")] - Pool::Sqlite(sq) => { - let mut q: sqlx::query::Query<'_, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'_>> = - sqlx::query(&stmt.sql); - for v in stmt.params { - q = bind_query_sqlite(q, v); - } - let rows = q.fetch_all(sq).await?; - let mut out = Vec::with_capacity(rows.len()); - for row in &rows { - use sqlx::Row as _; - let n = row.columns().len(); - let mut v = Vec::with_capacity(n); - for i in 0..n { - v.push(sqlite_cell_to_sqlvalue(row, i)); - } - out.push(v); - } - Ok(out) - } - } -} - -/// Trait gate for the `.values_list_flat::(...)` typed-scalar path — -/// PG arm. Same shape as [`MaybePgFromRow`]: when the `postgres` -/// feature is on, this is `Decode + Type`; otherwise an -/// empty blanket-impl so non-PG builds compile. -#[cfg(feature = "postgres")] -pub trait MaybePgScalar: - for<'r> sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type -{ -} -#[cfg(feature = "postgres")] -impl MaybePgScalar for T where - T: for<'r> sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type -{ -} -#[cfg(not(feature = "postgres"))] -pub trait MaybePgScalar {} -#[cfg(not(feature = "postgres"))] -impl MaybePgScalar for T {} - -#[cfg(feature = "mysql")] -pub trait MaybeMyScalar: for<'r> sqlx::Decode<'r, sqlx::MySql> + sqlx::Type {} -#[cfg(feature = "mysql")] -impl MaybeMyScalar for T where T: for<'r> sqlx::Decode<'r, sqlx::MySql> + sqlx::Type {} -#[cfg(not(feature = "mysql"))] -pub trait MaybeMyScalar {} -#[cfg(not(feature = "mysql"))] -impl MaybeMyScalar for T {} - -#[cfg(feature = "sqlite")] -pub trait MaybeSqliteScalar: - for<'r> sqlx::Decode<'r, sqlx::Sqlite> + sqlx::Type -{ -} -#[cfg(feature = "sqlite")] -impl MaybeSqliteScalar for T where - T: for<'r> sqlx::Decode<'r, sqlx::Sqlite> + sqlx::Type -{ -} -#[cfg(not(feature = "sqlite"))] -pub trait MaybeSqliteScalar {} -#[cfg(not(feature = "sqlite"))] -impl MaybeSqliteScalar for T {} - -/// Execute a single-column [`SelectQuery`] and decode each row's only -/// cell into `U`. Backs [`crate::query::ValuesFlatQuerySet::fetch`]. -/// Issue #22 — Django's `.values_list('col', flat=True)`. -/// -/// # Errors -/// SQL compilation or driver failure, including a decode error if `U` -/// doesn't match the column's SQL type on the live database. -pub async fn fetch_values_flat(pool: &Pool, query: &SelectQuery) -> Result, ExecError> -where - U: MaybePgScalar + MaybeMyScalar + MaybeSqliteScalar + Send + Unpin, -{ - let stmt = pool.dialect().compile_select(query)?; - match pool { - #[cfg(feature = "postgres")] - Pool::Postgres(pg) => { - let mut q: sqlx::query::QueryScalar<'_, sqlx::Postgres, U, PgArguments> = - sqlx::query_scalar(&stmt.sql); - for v in stmt.params { - q = bind_query_scalar_pg(q, v); - } - Ok(q.fetch_all(pg).await?) - } - #[cfg(feature = "mysql")] - Pool::Mysql(my) => { - let mut q: sqlx::query::QueryScalar<'_, sqlx::MySql, U, sqlx::mysql::MySqlArguments> = - sqlx::query_scalar(&stmt.sql); - for v in stmt.params { - q = bind_query_scalar_my(q, v); - } - Ok(q.fetch_all(my).await?) - } - #[cfg(feature = "sqlite")] - Pool::Sqlite(sq) => { - let mut q: sqlx::query::QueryScalar< - '_, - sqlx::Sqlite, - U, - sqlx::sqlite::SqliteArguments<'_>, - > = sqlx::query_scalar(&stmt.sql); - for v in stmt.params { - q = bind_query_scalar_sqlite(q, v); - } - Ok(q.fetch_all(sq).await?) - } - } -} - -#[cfg(feature = "postgres")] -fn bind_query_scalar_pg( - q: sqlx::query::QueryScalar<'_, sqlx::Postgres, U, PgArguments>, - value: SqlValue, -) -> sqlx::query::QueryScalar<'_, sqlx::Postgres, U, PgArguments> { - bind_match!(q, value) -} - -#[cfg(feature = "mysql")] -fn bind_query_scalar_my( - q: sqlx::query::QueryScalar<'_, sqlx::MySql, U, sqlx::mysql::MySqlArguments>, - value: SqlValue, -) -> sqlx::query::QueryScalar<'_, sqlx::MySql, U, sqlx::mysql::MySqlArguments> { - bind_match_mysql!(q, value) -} - -#[cfg(feature = "sqlite")] -fn bind_query_scalar_sqlite<'a, U>( - q: sqlx::query::QueryScalar<'a, sqlx::Sqlite, U, sqlx::sqlite::SqliteArguments<'a>>, - value: SqlValue, -) -> sqlx::query::QueryScalar<'a, sqlx::Sqlite, U, sqlx::sqlite::SqliteArguments<'a>> { - bind_match_sqlite!(q, value) -} - -// Bridge methods on the values builders so callers chain `.fetch(&pool)`. - -impl crate::query::ValuesQuerySet { - /// Execute the projection and return rows as `Vec>`. - /// - /// # Errors - /// - [`ExecError::Query`] for SQL compilation failures (typo'd - /// column, etc.). - /// - [`ExecError::Sqlx`] for driver / network / decode failures. - pub async fn fetch( - self, - pool: &Pool, - ) -> Result>, ExecError> { - let q = self.compile()?; - fetch_values_dict(pool, &q).await - } -} - -impl crate::query::ValuesListQuerySet { - /// Execute the projection and return rows as `Vec>`. - /// - /// # Errors - /// As [`crate::query::ValuesQuerySet::fetch`]. - pub async fn fetch(self, pool: &Pool) -> Result>, ExecError> { - let q = self.compile()?; - fetch_values_list(pool, &q).await - } -} - -impl crate::query::ValuesFlatQuerySet { - /// Execute the single-column projection and decode each row's cell - /// into `U`. - /// - /// # Errors - /// As [`crate::query::ValuesQuerySet::fetch`], plus per-cell - /// type-mismatch errors if `U` doesn't match the column's SQL type. - pub async fn fetch(self, pool: &Pool) -> Result, ExecError> - where - U: MaybePgScalar + MaybeMyScalar + MaybeSqliteScalar + Send + Unpin, - { - let q = self.compile()?; - fetch_values_flat::(pool, &q).await - } -} +mod values; +#[allow(unused_imports)] +pub use values::{ + fetch_values_dict, fetch_values_flat, fetch_values_list, MaybeMyScalar, MaybePgScalar, + MaybeSqliteScalar, +}; /// Execute arbitrary SQL with bound `SqlValue` params and decode each /// row into `T` via `FromRow`. Bi-dialect counterpart of diff --git a/crates/rustango/src/sql/executor/values.rs b/crates/rustango/src/sql/executor/values.rs new file mode 100644 index 0000000..eb5acab --- /dev/null +++ b/crates/rustango/src/sql/executor/values.rs @@ -0,0 +1,397 @@ +//! Pure projection — Django `.values()` / `.values_list()` (issue #22). +//! +//! Extracted from `executor/mod.rs` as part of #116 step 3. Contains: +//! +//! - Per-dialect `cell_to_sqlvalue` helpers (PG / MySQL / SQLite). +//! - `fetch_values_dict` / `fetch_values_list` / `fetch_values_flat` +//! pool-level entry points. +//! - `MaybePgScalar` / `MaybeMyScalar` / `MaybeSqliteScalar` trait +//! gates for `values_list_flat::(...)`. +//! - `impl ValuesQuerySet` / `impl ValuesListQuerySet` / +//! `impl ValuesFlatQuerySet` bridge methods that let callers chain +//! `.fetch(&pool)`. + +#[cfg(feature = "postgres")] +use sqlx::postgres::{PgArguments, PgRow}; +#[cfg(feature = "postgres")] +use sqlx::query::Query; + +use super::ExecError; +use crate::core::{SelectQuery, SqlValue}; +use crate::sql::Pool; + +#[cfg(feature = "postgres")] +use super::{bind_match, bind_query}; +#[cfg(feature = "mysql")] +use super::{bind_match_mysql, bind_query_my}; +#[cfg(feature = "sqlite")] +use super::{bind_match_sqlite, bind_query_sqlite}; + +/// Decode one per-dialect raw `SqlValue` from the i-th column of a row. +/// Same probe order as the aggregate-row decoder so the two paths agree +/// on how mixed types come back: scalars first, then jsonb / arrays +/// (PG only). NULLs (or unrecognized types) fall through to +/// [`SqlValue::Null`]. +#[cfg(feature = "postgres")] +fn pg_cell_to_sqlvalue(row: &PgRow, i: usize) -> SqlValue { + use sqlx::Row as _; + if let Ok(v) = row.try_get::(i) { + SqlValue::I64(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::I32(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::F64(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::Bool(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::String(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::Json(v) + } else { + SqlValue::Null + } +} + +#[cfg(feature = "mysql")] +fn my_cell_to_sqlvalue(row: &sqlx::mysql::MySqlRow, i: usize) -> SqlValue { + use sqlx::Row as _; + if let Ok(v) = row.try_get::(i) { + SqlValue::I64(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::I32(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::F64(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::Bool(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::String(v) + } else { + SqlValue::Null + } +} + +#[cfg(feature = "sqlite")] +fn sqlite_cell_to_sqlvalue(row: &sqlx::sqlite::SqliteRow, i: usize) -> SqlValue { + use sqlx::Row as _; + if let Ok(v) = row.try_get::(i) { + SqlValue::I64(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::I32(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::F64(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::Bool(v) + } else if let Ok(v) = row.try_get::(i) { + SqlValue::String(v) + } else { + SqlValue::Null + } +} + +/// Execute a [`SelectQuery`] (with `projection` set) and return each +/// row as a `HashMap` keyed by column name. +/// Backs [`crate::query::ValuesQuerySet::fetch`]. Issue #22. +/// +/// # Errors +/// SQL compilation or driver failure. +pub async fn fetch_values_dict( + pool: &Pool, + query: &SelectQuery, +) -> Result>, ExecError> { + let stmt = pool.dialect().compile_select(query)?; + match pool { + #[cfg(feature = "postgres")] + Pool::Postgres(pg) => { + let mut q: Query<'_, sqlx::Postgres, PgArguments> = sqlx::query(&stmt.sql); + for v in stmt.params { + q = bind_query(q, v); + } + let rows = q.fetch_all(pg).await?; + let mut out = Vec::with_capacity(rows.len()); + for row in &rows { + use sqlx::Column as _; + use sqlx::Row as _; + let mut map = std::collections::HashMap::new(); + for (i, col) in row.columns().iter().enumerate() { + map.insert(col.name().to_owned(), pg_cell_to_sqlvalue(row, i)); + } + out.push(map); + } + Ok(out) + } + #[cfg(feature = "mysql")] + Pool::Mysql(my) => { + let mut q: sqlx::query::Query<'_, sqlx::MySql, sqlx::mysql::MySqlArguments> = + sqlx::query(&stmt.sql); + for v in stmt.params { + q = bind_query_my(q, v); + } + let rows = q.fetch_all(my).await?; + let mut out = Vec::with_capacity(rows.len()); + for row in &rows { + use sqlx::Column as _; + use sqlx::Row as _; + let mut map = std::collections::HashMap::new(); + for (i, col) in row.columns().iter().enumerate() { + map.insert(col.name().to_owned(), my_cell_to_sqlvalue(row, i)); + } + out.push(map); + } + Ok(out) + } + #[cfg(feature = "sqlite")] + Pool::Sqlite(sq) => { + let mut q: sqlx::query::Query<'_, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'_>> = + sqlx::query(&stmt.sql); + for v in stmt.params { + q = bind_query_sqlite(q, v); + } + let rows = q.fetch_all(sq).await?; + let mut out = Vec::with_capacity(rows.len()); + for row in &rows { + use sqlx::Column as _; + use sqlx::Row as _; + let mut map = std::collections::HashMap::new(); + for (i, col) in row.columns().iter().enumerate() { + map.insert(col.name().to_owned(), sqlite_cell_to_sqlvalue(row, i)); + } + out.push(map); + } + Ok(out) + } + } +} + +/// Execute a [`SelectQuery`] (with `projection` set) and return each +/// row as a `Vec` ordered to match the projection's column +/// list. Backs [`crate::query::ValuesListQuerySet::fetch`]. +/// Issue #22. +/// +/// # Errors +/// SQL compilation or driver failure. +pub async fn fetch_values_list( + pool: &Pool, + query: &SelectQuery, +) -> Result>, ExecError> { + let stmt = pool.dialect().compile_select(query)?; + match pool { + #[cfg(feature = "postgres")] + Pool::Postgres(pg) => { + let mut q: Query<'_, sqlx::Postgres, PgArguments> = sqlx::query(&stmt.sql); + for v in stmt.params { + q = bind_query(q, v); + } + let rows = q.fetch_all(pg).await?; + let mut out = Vec::with_capacity(rows.len()); + for row in &rows { + use sqlx::Row as _; + let n = row.columns().len(); + let mut v = Vec::with_capacity(n); + for i in 0..n { + v.push(pg_cell_to_sqlvalue(row, i)); + } + out.push(v); + } + Ok(out) + } + #[cfg(feature = "mysql")] + Pool::Mysql(my) => { + let mut q: sqlx::query::Query<'_, sqlx::MySql, sqlx::mysql::MySqlArguments> = + sqlx::query(&stmt.sql); + for v in stmt.params { + q = bind_query_my(q, v); + } + let rows = q.fetch_all(my).await?; + let mut out = Vec::with_capacity(rows.len()); + for row in &rows { + use sqlx::Row as _; + let n = row.columns().len(); + let mut v = Vec::with_capacity(n); + for i in 0..n { + v.push(my_cell_to_sqlvalue(row, i)); + } + out.push(v); + } + Ok(out) + } + #[cfg(feature = "sqlite")] + Pool::Sqlite(sq) => { + let mut q: sqlx::query::Query<'_, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'_>> = + sqlx::query(&stmt.sql); + for v in stmt.params { + q = bind_query_sqlite(q, v); + } + let rows = q.fetch_all(sq).await?; + let mut out = Vec::with_capacity(rows.len()); + for row in &rows { + use sqlx::Row as _; + let n = row.columns().len(); + let mut v = Vec::with_capacity(n); + for i in 0..n { + v.push(sqlite_cell_to_sqlvalue(row, i)); + } + out.push(v); + } + Ok(out) + } + } +} + +/// Trait gate for the `.values_list_flat::(...)` typed-scalar path — +/// PG arm. Same shape as [`super::MaybePgFromRow`]: when the `postgres` +/// feature is on, this is `Decode + Type`; otherwise an +/// empty blanket-impl so non-PG builds compile. +#[cfg(feature = "postgres")] +pub trait MaybePgScalar: + for<'r> sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type +{ +} +#[cfg(feature = "postgres")] +impl MaybePgScalar for T where + T: for<'r> sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type +{ +} +#[cfg(not(feature = "postgres"))] +pub trait MaybePgScalar {} +#[cfg(not(feature = "postgres"))] +impl MaybePgScalar for T {} + +#[cfg(feature = "mysql")] +pub trait MaybeMyScalar: for<'r> sqlx::Decode<'r, sqlx::MySql> + sqlx::Type {} +#[cfg(feature = "mysql")] +impl MaybeMyScalar for T where T: for<'r> sqlx::Decode<'r, sqlx::MySql> + sqlx::Type {} +#[cfg(not(feature = "mysql"))] +pub trait MaybeMyScalar {} +#[cfg(not(feature = "mysql"))] +impl MaybeMyScalar for T {} + +#[cfg(feature = "sqlite")] +pub trait MaybeSqliteScalar: + for<'r> sqlx::Decode<'r, sqlx::Sqlite> + sqlx::Type +{ +} +#[cfg(feature = "sqlite")] +impl MaybeSqliteScalar for T where + T: for<'r> sqlx::Decode<'r, sqlx::Sqlite> + sqlx::Type +{ +} +#[cfg(not(feature = "sqlite"))] +pub trait MaybeSqliteScalar {} +#[cfg(not(feature = "sqlite"))] +impl MaybeSqliteScalar for T {} + +/// Execute a single-column [`SelectQuery`] and decode each row's only +/// cell into `U`. Backs [`crate::query::ValuesFlatQuerySet::fetch`]. +/// Issue #22 — Django's `.values_list('col', flat=True)`. +/// +/// # Errors +/// SQL compilation or driver failure, including a decode error if `U` +/// doesn't match the column's SQL type on the live database. +pub async fn fetch_values_flat(pool: &Pool, query: &SelectQuery) -> Result, ExecError> +where + U: MaybePgScalar + MaybeMyScalar + MaybeSqliteScalar + Send + Unpin, +{ + let stmt = pool.dialect().compile_select(query)?; + match pool { + #[cfg(feature = "postgres")] + Pool::Postgres(pg) => { + let mut q: sqlx::query::QueryScalar<'_, sqlx::Postgres, U, PgArguments> = + sqlx::query_scalar(&stmt.sql); + for v in stmt.params { + q = bind_query_scalar_pg(q, v); + } + Ok(q.fetch_all(pg).await?) + } + #[cfg(feature = "mysql")] + Pool::Mysql(my) => { + let mut q: sqlx::query::QueryScalar<'_, sqlx::MySql, U, sqlx::mysql::MySqlArguments> = + sqlx::query_scalar(&stmt.sql); + for v in stmt.params { + q = bind_query_scalar_my(q, v); + } + Ok(q.fetch_all(my).await?) + } + #[cfg(feature = "sqlite")] + Pool::Sqlite(sq) => { + let mut q: sqlx::query::QueryScalar< + '_, + sqlx::Sqlite, + U, + sqlx::sqlite::SqliteArguments<'_>, + > = sqlx::query_scalar(&stmt.sql); + for v in stmt.params { + q = bind_query_scalar_sqlite(q, v); + } + Ok(q.fetch_all(sq).await?) + } + } +} + +#[cfg(feature = "postgres")] +fn bind_query_scalar_pg( + q: sqlx::query::QueryScalar<'_, sqlx::Postgres, U, PgArguments>, + value: SqlValue, +) -> sqlx::query::QueryScalar<'_, sqlx::Postgres, U, PgArguments> { + bind_match!(q, value) +} + +#[cfg(feature = "mysql")] +fn bind_query_scalar_my( + q: sqlx::query::QueryScalar<'_, sqlx::MySql, U, sqlx::mysql::MySqlArguments>, + value: SqlValue, +) -> sqlx::query::QueryScalar<'_, sqlx::MySql, U, sqlx::mysql::MySqlArguments> { + bind_match_mysql!(q, value) +} + +#[cfg(feature = "sqlite")] +fn bind_query_scalar_sqlite<'a, U>( + q: sqlx::query::QueryScalar<'a, sqlx::Sqlite, U, sqlx::sqlite::SqliteArguments<'a>>, + value: SqlValue, +) -> sqlx::query::QueryScalar<'a, sqlx::Sqlite, U, sqlx::sqlite::SqliteArguments<'a>> { + bind_match_sqlite!(q, value) +} + +// Bridge methods on the values builders so callers chain `.fetch(&pool)`. + +impl crate::query::ValuesQuerySet { + /// Execute the projection and return rows as `Vec>`. + /// + /// # Errors + /// - [`ExecError::Query`] for SQL compilation failures (typo'd + /// column, etc.). + /// - [`ExecError::Sqlx`] for driver / network / decode failures. + pub async fn fetch( + self, + pool: &Pool, + ) -> Result>, ExecError> { + let q = self.compile()?; + fetch_values_dict(pool, &q).await + } +} + +impl crate::query::ValuesListQuerySet { + /// Execute the projection and return rows as `Vec>`. + /// + /// # Errors + /// As [`crate::query::ValuesQuerySet::fetch`]. + pub async fn fetch(self, pool: &Pool) -> Result>, ExecError> { + let q = self.compile()?; + fetch_values_list(pool, &q).await + } +} + +impl crate::query::ValuesFlatQuerySet { + /// Execute the single-column projection and decode each row's cell + /// into `U`. + /// + /// # Errors + /// As [`crate::query::ValuesQuerySet::fetch`], plus per-cell + /// type-mismatch errors if `U` doesn't match the column's SQL type. + pub async fn fetch(self, pool: &Pool) -> Result, ExecError> + where + U: MaybePgScalar + MaybeMyScalar + MaybeSqliteScalar + Send + Unpin, + { + let q = self.compile()?; + fetch_values_flat::(pool, &q).await + } +}