#![forbid(unsafe_code)]
#![deny(unused_must_use)]
#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
#![warn(rust_2018_idioms)]
#![warn(unreachable_pub)]
#![warn(rustdoc::missing_crate_level_docs)]
#![warn(rustdoc::broken_intra_doc_links)]
#![allow(clippy::doc_markdown)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::items_after_statements)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::return_self_not_must_use)] #![allow(clippy::must_use_candidate)] #![allow(clippy::match_same_arms)] #![allow(clippy::format_push_string)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_sign_loss)] #![allow(clippy::cast_possible_wrap)] #![allow(clippy::indexing_slicing)] #![allow(clippy::unwrap_used)] #![allow(clippy::double_must_use)]
mod builder;
mod dialect;
mod pagination;
mod validate;
pub use builder::{
Aggregate, AggregateFunc, CompoundFilter, ComputedField, CursorDirection, DeleteBuilder,
Filter, FilterExpr, InsertBuilder, LogicalOp, Operator, ParseError, QueryBuilder, QueryResult,
SortDir, SortField, UpdateBuilder, Value, and, delete, insert, not, or, parse_filter, simple,
update,
};
pub use miniserde::json;
#[doc(hidden)]
pub use builder::{delete_sqlite, insert_sqlite, update_sqlite};
pub use dialect::{Dialect, Postgres, Sqlite};
pub use pagination::{Cursor, CursorError, IntoCursor, KeysetCondition, PageInfo};
pub use validate::{
FilterValidator, ValidationError, assert_valid_sql_expression, assert_valid_sql_identifier,
is_valid_sql_expression, is_valid_sql_identifier, merge_filters,
};
pub use mik_sql_macros::{sql_create, sql_delete, sql_read, sql_update};
pub use mik_sdk_macros::ids;
#[must_use]
pub fn postgres(table: &str) -> QueryBuilder<Postgres> {
QueryBuilder::new(Postgres, table)
}
#[must_use]
pub fn sqlite(table: &str) -> QueryBuilder<Sqlite> {
QueryBuilder::new(Sqlite, table)
}
pub mod prelude {
pub use crate::{
Aggregate, AggregateFunc, CompoundFilter, ComputedField, Cursor, CursorDirection,
CursorError, DeleteBuilder, Dialect, Filter, FilterExpr, FilterValidator, InsertBuilder,
IntoCursor, KeysetCondition, LogicalOp, Operator, PageInfo, ParseError, Postgres,
QueryBuilder, QueryResult, SortDir, SortField, Sqlite, UpdateBuilder, ValidationError,
Value, and, delete, insert, json, merge_filters, not, or, parse_filter, postgres, simple,
sqlite, update,
};
pub use mik_sdk_macros::ids;
pub use mik_sql_macros::{sql_create, sql_delete, sql_read, sql_update};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_select() {
let result = postgres("users").fields(&["id", "name", "email"]).build();
assert_eq!(result.sql, "SELECT id, name, email FROM users");
assert!(result.params.is_empty());
}
#[test]
fn test_select_with_filter() {
let result = postgres("users")
.fields(&["id", "name"])
.filter("active", Operator::Eq, Value::Bool(true))
.build();
assert_eq!(result.sql, "SELECT id, name FROM users WHERE active = $1");
assert_eq!(result.params.len(), 1);
}
#[test]
fn test_sqlite_dialect() {
let result = sqlite("users")
.fields(&["id", "name"])
.filter("active", Operator::Eq, Value::Bool(true))
.build();
assert_eq!(result.sql, "SELECT id, name FROM users WHERE active = ?1");
}
#[test]
fn test_cursor_pagination() {
let cursor = Cursor::new().int("id", 100);
let result = postgres("users")
.fields(&["id", "name"])
.sort("id", SortDir::Asc)
.after_cursor(cursor)
.limit(20)
.build();
assert_eq!(
result.sql,
"SELECT id, name FROM users WHERE id > $1 ORDER BY id ASC LIMIT 20"
);
}
#[test]
fn test_multi_field_cursor_mixed_sort_directions() {
let cursor = Cursor::new()
.string("created_at", "2024-01-15T10:00:00Z")
.int("id", 42);
let result = postgres("posts")
.fields(&["id", "title", "created_at"])
.sort("created_at", SortDir::Desc) .sort("id", SortDir::Asc) .after_cursor(cursor)
.limit(20)
.build();
assert!(result.sql.contains("ORDER BY created_at DESC, id ASC"));
assert_eq!(result.params.len(), 2);
}
#[test]
fn test_multi_field_cursor_all_desc() {
let cursor = Cursor::new()
.string("created_at", "2024-01-15T10:00:00Z")
.int("id", 42);
let result = postgres("posts")
.fields(&["id", "title"])
.sort("created_at", SortDir::Desc)
.sort("id", SortDir::Desc)
.after_cursor(cursor)
.limit(10)
.build();
assert!(result.sql.contains("ORDER BY created_at DESC, id DESC"));
}
#[test]
fn test_sqlite_between_operator() {
let result = sqlite("products")
.fields(&["id", "name", "price"])
.filter(
"price",
Operator::Between,
Value::Array(vec![Value::Float(10.0), Value::Float(100.0)]),
)
.build();
assert!(result.sql.contains("BETWEEN"));
assert!(result.sql.contains("?1"));
assert!(result.sql.contains("?2"));
assert_eq!(result.params.len(), 2);
}
#[test]
fn test_postgres_between_operator() {
let result = postgres("products")
.fields(&["id", "name", "price"])
.filter(
"price",
Operator::Between,
Value::Array(vec![Value::Int(10), Value::Int(100)]),
)
.build();
assert!(result.sql.contains("BETWEEN"));
assert!(result.sql.contains("$1"));
assert!(result.sql.contains("$2"));
assert_eq!(result.params.len(), 2);
}
#[test]
fn test_compound_filter_nested() {
use builder::{CompoundFilter, FilterExpr, simple};
let nested_or = CompoundFilter::or(vec![
simple("status", Operator::Eq, Value::String("active".into())),
simple("status", Operator::Eq, Value::String("pending".into())),
]);
let result = postgres("orders")
.fields(&["id", "status", "amount"])
.filter_expr(FilterExpr::Compound(CompoundFilter::and(vec![
simple("amount", Operator::Gte, Value::Int(100)),
FilterExpr::Compound(nested_or),
])))
.build();
assert!(result.sql.contains("AND"));
assert!(result.sql.contains("OR"));
assert_eq!(result.params.len(), 3);
}
#[test]
fn test_compound_filter_not() {
use builder::{CompoundFilter, FilterExpr, simple};
let result = postgres("users")
.fields(&["id", "name", "role"])
.filter_expr(FilterExpr::Compound(CompoundFilter::not(simple(
"role",
Operator::Eq,
Value::String("admin".into()),
))))
.build();
assert!(result.sql.contains("NOT"));
assert_eq!(result.params.len(), 1);
}
#[test]
fn test_empty_cursor_ignored() {
let cursor = Cursor::new();
let result = postgres("users")
.fields(&["id", "name"])
.sort("id", SortDir::Asc)
.after_cursor(cursor)
.limit(20)
.build();
assert_eq!(
result.sql,
"SELECT id, name FROM users ORDER BY id ASC LIMIT 20"
);
}
#[test]
fn test_cursor_extra_fields_ignored() {
let cursor = Cursor::new()
.string("extra_field", "should_be_ignored")
.int("another_extra", 999)
.int("id", 42);
let result = postgres("users")
.fields(&["id", "name"])
.sort("id", SortDir::Asc) .after_cursor(cursor)
.limit(20)
.build();
assert!(result.sql.contains("id > $1"));
assert_eq!(result.params.len(), 1);
}
#[test]
fn test_sqlite_in_clause_expansion() {
let result = sqlite("users")
.fields(&["id", "name"])
.filter(
"status",
Operator::In,
Value::Array(vec![
Value::String("active".into()),
Value::String("pending".into()),
Value::String("review".into()),
]),
)
.build();
assert!(result.sql.contains("IN (?1, ?2, ?3)"));
assert_eq!(result.params.len(), 3);
}
#[test]
fn test_postgres_in_clause_array() {
let result = postgres("users")
.fields(&["id", "name"])
.filter(
"status",
Operator::In,
Value::Array(vec![
Value::String("active".into()),
Value::String("pending".into()),
]),
)
.build();
assert!(result.sql.contains("= ANY($1)"));
assert_eq!(result.params.len(), 1); }
#[test]
fn test_between_with_exactly_two_values() {
let result = postgres("products")
.fields(&["id", "price"])
.filter(
"price",
Operator::Between,
Value::Array(vec![Value::Int(10), Value::Int(100)]),
)
.build();
assert!(result.sql.contains("BETWEEN $1 AND $2"));
assert_eq!(result.params.len(), 2);
}
#[test]
fn test_between_with_one_value_fallback() {
let result = postgres("products")
.fields(&["id", "price"])
.filter(
"price",
Operator::Between,
Value::Array(vec![Value::Int(10)]),
)
.build();
assert!(
result.sql.contains("1=0"),
"Should return impossible condition"
);
}
#[test]
fn test_between_with_three_values_fallback() {
let result = postgres("products")
.fields(&["id", "price"])
.filter(
"price",
Operator::Between,
Value::Array(vec![Value::Int(10), Value::Int(50), Value::Int(100)]),
)
.build();
assert!(
result.sql.contains("1=0"),
"Should return impossible condition"
);
}
#[test]
fn test_between_with_empty_array_fallback() {
let result = postgres("products")
.fields(&["id", "price"])
.filter("price", Operator::Between, Value::Array(vec![]))
.build();
assert!(
result.sql.contains("1=0"),
"Should return impossible condition"
);
}
#[test]
fn test_between_with_different_value_types() {
let result = postgres("orders")
.fields(&["id", "created_at"])
.filter(
"created_at",
Operator::Between,
Value::Array(vec![
Value::String("2024-01-01".into()),
Value::String("2024-12-31".into()),
]),
)
.build();
assert!(result.sql.contains("BETWEEN $1 AND $2"));
assert_eq!(result.params.len(), 2);
}
#[test]
fn test_between_sqlite_dialect() {
let result = sqlite("products")
.fields(&["id", "price"])
.filter(
"price",
Operator::Between,
Value::Array(vec![Value::Float(9.99), Value::Float(99.99)]),
)
.build();
assert!(result.sql.contains("BETWEEN ?1 AND ?2"));
assert_eq!(result.params.len(), 2);
}
}
#[cfg(test)]
mod api_contracts {
use static_assertions::assert_impl_all;
assert_impl_all!(crate::QueryResult: Clone, std::fmt::Debug, PartialEq);
assert_impl_all!(crate::Cursor: Clone, std::fmt::Debug, PartialEq);
assert_impl_all!(crate::PageInfo: Clone, std::fmt::Debug, PartialEq, Eq, Default);
assert_impl_all!(crate::Value: Clone, std::fmt::Debug, PartialEq);
assert_impl_all!(crate::Filter: Clone, std::fmt::Debug, PartialEq);
assert_impl_all!(crate::FilterExpr: Clone, std::fmt::Debug, PartialEq);
assert_impl_all!(crate::Operator: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::LogicalOp: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::SortDir: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::CursorDirection: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::AggregateFunc: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::CursorError: Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::ValidationError: Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::SortField: Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::Aggregate: Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::ComputedField: Clone, std::fmt::Debug, PartialEq, Eq);
assert_impl_all!(crate::KeysetCondition: Clone, std::fmt::Debug, PartialEq);
}