mik_sql/lib.rs
1// =============================================================================
2// CRATE-LEVEL QUALITY LINTS (following Tokio/Serde standards)
3// =============================================================================
4#![forbid(unsafe_code)]
5#![deny(unused_must_use)]
6#![warn(missing_docs)]
7#![warn(missing_debug_implementations)]
8#![warn(rust_2018_idioms)]
9#![warn(unreachable_pub)]
10#![warn(rustdoc::missing_crate_level_docs)]
11#![warn(rustdoc::broken_intra_doc_links)]
12// =============================================================================
13// CLIPPY CONFIGURATION
14// =============================================================================
15// Pedantic lints that are too verbose to fix individually in a DSL-heavy crate
16#![allow(clippy::doc_markdown)] // Code items in docs - extensive doc changes needed
17#![allow(clippy::missing_errors_doc)] // # Errors sections - doc-heavy
18#![allow(clippy::missing_panics_doc)] // # Panics sections - doc-heavy
19#![allow(clippy::items_after_statements)] // const in functions - intentional for locality
20#![allow(clippy::module_name_repetitions)] // Type names matching module - acceptable
21#![allow(clippy::return_self_not_must_use)] // Builder pattern methods return Self by design
22#![allow(clippy::must_use_candidate)] // Builder methods - fluent API doesn't need must_use
23#![allow(clippy::match_same_arms)] // Intentional for clarity in some match expressions
24#![allow(clippy::format_push_string)] // String building style preference
25#![allow(clippy::cast_possible_truncation)] // Intentional in SQL context
26#![allow(clippy::cast_sign_loss)] // Intentional in SQL context
27#![allow(clippy::cast_possible_wrap)] // Intentional in SQL context
28// Internal builder code where bounds are checked before use
29#![allow(clippy::indexing_slicing)] // Bounds checked before indexing in builder logic
30#![allow(clippy::unwrap_used)] // Used after explicit length checks in compound filter builders
31#![allow(clippy::double_must_use)] // Functions returning must_use types can have their own docs
32
33//! # mik-sql - SQL Query Builder with Mongo-style Filters
34//!
35//! A standalone SQL query builder with intuitive macro syntax for CRUD operations.
36//! Supports Postgres and `SQLite` dialects.
37//!
38//! ## Quick Start
39//!
40//! ```
41//! # use mik_sql::prelude::*;
42//! // SELECT - Read data with filters and ordering
43//! let result = postgres("users")
44//! .fields(&["id", "name", "email"])
45//! .filter("active", Operator::Eq, Value::Bool(true))
46//! .sort("name", SortDir::Asc)
47//! .limit(10)
48//! .build();
49//!
50//! assert!(result.sql.contains("SELECT id, name, email FROM users"));
51//! assert!(result.sql.contains("ORDER BY name ASC"));
52//! ```
53//!
54//! ## `SQLite` Dialect
55//!
56//! Use `sqlite()` for `SQLite` syntax (?1, ?2 instead of $1, $2):
57//!
58//! ```
59//! # use mik_sql::prelude::*;
60//! let result = sqlite("users")
61//! .fields(&["id", "name"])
62//! .filter("active", Operator::Eq, Value::Bool(true))
63//! .build();
64//!
65//! assert!(result.sql.contains("?1")); // SQLite placeholder
66//! ```
67//!
68//! ## Supported Operators
69//!
70//! | Operator | SQL | Example |
71//! |----------|-----|---------|
72//! | `$eq` | `=` | `"status": { "$eq": "active" }` |
73//! | `$ne` | `!=` | `"status": { "$ne": "deleted" }` |
74//! | `$gt` | `>` | `"age": { "$gt": 18 }` |
75//! | `$gte` | `>=` | `"age": { "$gte": 21 }` |
76//! | `$lt` | `<` | `"price": { "$lt": 100 }` |
77//! | `$lte` | `<=` | `"price": { "$lte": 50 }` |
78//! | `$in` | `IN` | `"status": { "$in": ["a", "b"] }` |
79//! | `$nin` | `NOT IN` | `"status": { "$nin": ["x"] }` |
80//! | `$like` | `LIKE` | `"name": { "$like": "%test%" }` |
81//! | `$ilike` | `ILIKE` | `"name": { "$ilike": "%test%" }` |
82//! | `$starts_with` | `LIKE $1 \|\| '%'` | `"name": { "$starts_with": "John" }` |
83//! | `$ends_with` | `LIKE '%' \|\| $1` | `"email": { "$ends_with": "@example.com" }` |
84//! | `$contains` | `LIKE '%' \|\| $1 \|\| '%'` | `"bio": { "$contains": "developer" }` |
85//! | `$between` | `BETWEEN $1 AND $2` | `"age": { "$between": [18, 65] }` |
86//!
87//! ## Cursor Pagination
88//!
89//! ```
90//! # use mik_sql::prelude::*;
91//! // Create a cursor for pagination
92//! let cursor = Cursor::new()
93//! .string("created_at", "2024-01-15T10:00:00Z")
94//! .int("id", 42);
95//!
96//! let result = postgres("posts")
97//! .fields(&["id", "title", "created_at"])
98//! .filter("published", Operator::Eq, Value::Bool(true))
99//! .sort("created_at", SortDir::Desc)
100//! .sort("id", SortDir::Asc)
101//! .after_cursor(cursor)
102//! .limit(20)
103//! .build();
104//!
105//! assert!(result.sql.contains("ORDER BY created_at DESC, id ASC"));
106//! ```
107
108mod builder;
109mod dialect;
110mod pagination;
111mod validate;
112
113pub use builder::{
114 Aggregate, AggregateFunc, CompoundFilter, ComputedField, CursorDirection, DeleteBuilder,
115 Filter, FilterExpr, InsertBuilder, LogicalOp, Operator, ParseError, QueryBuilder, QueryResult,
116 SortDir, SortField, UpdateBuilder, Value, and, delete, insert, not, or, parse_filter, simple,
117 update,
118};
119
120/// Re-export miniserde's json module for runtime filter parsing.
121///
122/// Use this to parse JSON strings into values for `FilterExpr::from_json()`.
123///
124/// # Example
125///
126/// ```
127/// use mik_sql::{json, FilterExpr};
128///
129/// let json_str = r#"{"name": {"$eq": "Alice"}}"#;
130/// let value: miniserde::json::Value = json::from_str(json_str).unwrap();
131/// let filter = FilterExpr::from_json(&value).unwrap();
132/// ```
133pub use miniserde::json;
134
135// Internal functions used by macros - not part of public API
136#[doc(hidden)]
137pub use builder::{delete_sqlite, insert_sqlite, update_sqlite};
138pub use dialect::{Dialect, Postgres, Sqlite};
139pub use pagination::{Cursor, CursorError, IntoCursor, KeysetCondition, PageInfo};
140pub use validate::{
141 FilterValidator, ValidationError, assert_valid_sql_expression, assert_valid_sql_identifier,
142 is_valid_sql_expression, is_valid_sql_identifier, merge_filters,
143};
144
145// Re-export SQL macros from mik-sql-macros
146pub use mik_sql_macros::{sql_create, sql_delete, sql_read, sql_update};
147
148// Re-export ids! from mik-sdk-macros (consolidated location)
149pub use mik_sdk_macros::ids;
150
151/// Build a query for Postgres.
152///
153/// Convenience function that creates a `QueryBuilder` with Postgres dialect.
154#[must_use]
155pub fn postgres(table: &str) -> QueryBuilder<Postgres> {
156 QueryBuilder::new(Postgres, table)
157}
158
159/// Build a query for `SQLite`.
160///
161/// Convenience function that creates a `QueryBuilder` with `SQLite` dialect.
162#[must_use]
163pub fn sqlite(table: &str) -> QueryBuilder<Sqlite> {
164 QueryBuilder::new(Sqlite, table)
165}
166
167/// Prelude module for convenient imports.
168///
169/// ```
170/// use mik_sql::prelude::*;
171/// // Now Cursor, PageInfo, postgres(), sqlite(), etc. are available
172/// let result = postgres("users").fields(&["id"]).build();
173/// assert!(result.sql.contains("SELECT id FROM users"));
174/// ```
175pub mod prelude {
176 pub use crate::{
177 Aggregate, AggregateFunc, CompoundFilter, ComputedField, Cursor, CursorDirection,
178 CursorError, DeleteBuilder, Dialect, Filter, FilterExpr, FilterValidator, InsertBuilder,
179 IntoCursor, KeysetCondition, LogicalOp, Operator, PageInfo, ParseError, Postgres,
180 QueryBuilder, QueryResult, SortDir, SortField, Sqlite, UpdateBuilder, ValidationError,
181 Value, and, delete, insert, json, merge_filters, not, or, parse_filter, postgres, simple,
182 sqlite, update,
183 };
184
185 // Re-export macros
186 pub use mik_sdk_macros::ids;
187 pub use mik_sql_macros::{sql_create, sql_delete, sql_read, sql_update};
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_simple_select() {
196 let result = postgres("users").fields(&["id", "name", "email"]).build();
197
198 assert_eq!(result.sql, "SELECT id, name, email FROM users");
199 assert!(result.params.is_empty());
200 }
201
202 #[test]
203 fn test_select_with_filter() {
204 let result = postgres("users")
205 .fields(&["id", "name"])
206 .filter("active", Operator::Eq, Value::Bool(true))
207 .build();
208
209 assert_eq!(result.sql, "SELECT id, name FROM users WHERE active = $1");
210 assert_eq!(result.params.len(), 1);
211 }
212
213 #[test]
214 fn test_sqlite_dialect() {
215 let result = sqlite("users")
216 .fields(&["id", "name"])
217 .filter("active", Operator::Eq, Value::Bool(true))
218 .build();
219
220 assert_eq!(result.sql, "SELECT id, name FROM users WHERE active = ?1");
221 }
222
223 #[test]
224 fn test_cursor_pagination() {
225 let cursor = Cursor::new().int("id", 100);
226
227 let result = postgres("users")
228 .fields(&["id", "name"])
229 .sort("id", SortDir::Asc)
230 .after_cursor(cursor)
231 .limit(20)
232 .build();
233
234 assert_eq!(
235 result.sql,
236 "SELECT id, name FROM users WHERE id > $1 ORDER BY id ASC LIMIT 20"
237 );
238 }
239
240 // ═══════════════════════════════════════════════════════════════════════════
241 // EDGE CASE TESTS
242 // ═══════════════════════════════════════════════════════════════════════════
243
244 #[test]
245 fn test_multi_field_cursor_mixed_sort_directions() {
246 // Edge case: Multi-field cursor with mixed ASC/DESC sort directions
247 // DESC fields use < operator, ASC fields use > operator
248 let cursor = Cursor::new()
249 .string("created_at", "2024-01-15T10:00:00Z")
250 .int("id", 42);
251
252 let result = postgres("posts")
253 .fields(&["id", "title", "created_at"])
254 .sort("created_at", SortDir::Desc) // Newest first
255 .sort("id", SortDir::Asc) // Then by ID ascending
256 .after_cursor(cursor)
257 .limit(20)
258 .build();
259
260 // Multi-field keyset uses tuple comparison: (created_at, id) < ($1, $2)
261 // For DESC primary, ASC secondary: items after cursor have (smaller created_at) OR (same created_at AND larger id)
262 assert!(result.sql.contains("ORDER BY created_at DESC, id ASC"));
263 assert_eq!(result.params.len(), 2);
264 }
265
266 #[test]
267 fn test_multi_field_cursor_all_desc() {
268 // Edge case: All fields descending
269 let cursor = Cursor::new()
270 .string("created_at", "2024-01-15T10:00:00Z")
271 .int("id", 42);
272
273 let result = postgres("posts")
274 .fields(&["id", "title"])
275 .sort("created_at", SortDir::Desc)
276 .sort("id", SortDir::Desc)
277 .after_cursor(cursor)
278 .limit(10)
279 .build();
280
281 assert!(result.sql.contains("ORDER BY created_at DESC, id DESC"));
282 }
283
284 #[test]
285 fn test_sqlite_between_operator() {
286 // Edge case: SQLite BETWEEN handling with expanded parameters
287 let result = sqlite("products")
288 .fields(&["id", "name", "price"])
289 .filter(
290 "price",
291 Operator::Between,
292 Value::Array(vec![Value::Float(10.0), Value::Float(100.0)]),
293 )
294 .build();
295
296 // SQLite BETWEEN uses ?1 AND ?2 placeholders
297 assert!(result.sql.contains("BETWEEN"));
298 assert!(result.sql.contains("?1"));
299 assert!(result.sql.contains("?2"));
300 assert_eq!(result.params.len(), 2);
301 }
302
303 #[test]
304 fn test_postgres_between_operator() {
305 // Postgres BETWEEN for comparison
306 let result = postgres("products")
307 .fields(&["id", "name", "price"])
308 .filter(
309 "price",
310 Operator::Between,
311 Value::Array(vec![Value::Int(10), Value::Int(100)]),
312 )
313 .build();
314
315 assert!(result.sql.contains("BETWEEN"));
316 assert!(result.sql.contains("$1"));
317 assert!(result.sql.contains("$2"));
318 assert_eq!(result.params.len(), 2);
319 }
320
321 #[test]
322 fn test_compound_filter_nested() {
323 use builder::{CompoundFilter, FilterExpr, simple};
324
325 // Edge case: Nested compound filters (AND containing OR)
326 let nested_or = CompoundFilter::or(vec![
327 simple("status", Operator::Eq, Value::String("active".into())),
328 simple("status", Operator::Eq, Value::String("pending".into())),
329 ]);
330
331 let result = postgres("orders")
332 .fields(&["id", "status", "amount"])
333 .filter_expr(FilterExpr::Compound(CompoundFilter::and(vec![
334 simple("amount", Operator::Gte, Value::Int(100)),
335 FilterExpr::Compound(nested_or),
336 ])))
337 .build();
338
339 // Should produce: amount >= $1 AND (status = $2 OR status = $3)
340 assert!(result.sql.contains("AND"));
341 assert!(result.sql.contains("OR"));
342 assert_eq!(result.params.len(), 3);
343 }
344
345 #[test]
346 fn test_compound_filter_not() {
347 use builder::{CompoundFilter, FilterExpr, simple};
348
349 // Edge case: NOT compound filter
350 let result = postgres("users")
351 .fields(&["id", "name", "role"])
352 .filter_expr(FilterExpr::Compound(CompoundFilter::not(simple(
353 "role",
354 Operator::Eq,
355 Value::String("admin".into()),
356 ))))
357 .build();
358
359 assert!(result.sql.contains("NOT"));
360 assert_eq!(result.params.len(), 1);
361 }
362
363 #[test]
364 fn test_empty_cursor_ignored() {
365 // Edge case: Empty cursor should not add any keyset conditions
366 let cursor = Cursor::new();
367
368 let result = postgres("users")
369 .fields(&["id", "name"])
370 .sort("id", SortDir::Asc)
371 .after_cursor(cursor)
372 .limit(20)
373 .build();
374
375 // Should not have WHERE clause from empty cursor
376 assert_eq!(
377 result.sql,
378 "SELECT id, name FROM users ORDER BY id ASC LIMIT 20"
379 );
380 }
381
382 #[test]
383 fn test_cursor_extra_fields_ignored() {
384 // Edge case: Cursor contains fields that are NOT in the sort specification.
385 // These extra fields should be silently ignored - only sort fields matter.
386 let cursor = Cursor::new()
387 .string("extra_field", "should_be_ignored")
388 .int("another_extra", 999)
389 .int("id", 42); // Only this matches sort
390
391 let result = postgres("users")
392 .fields(&["id", "name"])
393 .sort("id", SortDir::Asc) // Only sort by id
394 .after_cursor(cursor)
395 .limit(20)
396 .build();
397
398 // Should only use 'id' from cursor, ignore extra_field and another_extra
399 assert!(result.sql.contains("id > $1"));
400 assert_eq!(result.params.len(), 1);
401 }
402
403 #[test]
404 fn test_sqlite_in_clause_expansion() {
405 // Edge case: SQLite IN clause expands to multiple placeholders
406 let result = sqlite("users")
407 .fields(&["id", "name"])
408 .filter(
409 "status",
410 Operator::In,
411 Value::Array(vec![
412 Value::String("active".into()),
413 Value::String("pending".into()),
414 Value::String("review".into()),
415 ]),
416 )
417 .build();
418
419 // SQLite: status IN (?1, ?2, ?3)
420 assert!(result.sql.contains("IN (?1, ?2, ?3)"));
421 assert_eq!(result.params.len(), 3);
422 }
423
424 #[test]
425 fn test_postgres_in_clause_array() {
426 // Postgres uses ANY with array parameter
427 let result = postgres("users")
428 .fields(&["id", "name"])
429 .filter(
430 "status",
431 Operator::In,
432 Value::Array(vec![
433 Value::String("active".into()),
434 Value::String("pending".into()),
435 ]),
436 )
437 .build();
438
439 // Postgres: status = ANY($1)
440 assert!(result.sql.contains("= ANY($1)"));
441 assert_eq!(result.params.len(), 1); // Single array param
442 }
443
444 // =========================================================================
445 // BETWEEN OPERATOR EDGE CASE TESTS
446 // =========================================================================
447
448 #[test]
449 fn test_between_with_exactly_two_values() {
450 // BETWEEN with exactly 2 values should work
451 let result = postgres("products")
452 .fields(&["id", "price"])
453 .filter(
454 "price",
455 Operator::Between,
456 Value::Array(vec![Value::Int(10), Value::Int(100)]),
457 )
458 .build();
459
460 assert!(result.sql.contains("BETWEEN $1 AND $2"));
461 assert_eq!(result.params.len(), 2);
462 }
463
464 #[test]
465 fn test_between_with_one_value_fallback() {
466 // BETWEEN with 1 value returns impossible condition (consistent behavior)
467 let result = postgres("products")
468 .fields(&["id", "price"])
469 .filter(
470 "price",
471 Operator::Between,
472 Value::Array(vec![Value::Int(10)]),
473 )
474 .build();
475 assert!(
476 result.sql.contains("1=0"),
477 "Should return impossible condition"
478 );
479 }
480
481 #[test]
482 fn test_between_with_three_values_fallback() {
483 // BETWEEN with 3 values returns impossible condition (consistent behavior)
484 let result = postgres("products")
485 .fields(&["id", "price"])
486 .filter(
487 "price",
488 Operator::Between,
489 Value::Array(vec![Value::Int(10), Value::Int(50), Value::Int(100)]),
490 )
491 .build();
492 assert!(
493 result.sql.contains("1=0"),
494 "Should return impossible condition"
495 );
496 }
497
498 #[test]
499 fn test_between_with_empty_array_fallback() {
500 // BETWEEN with empty array returns impossible condition (consistent behavior)
501 let result = postgres("products")
502 .fields(&["id", "price"])
503 .filter("price", Operator::Between, Value::Array(vec![]))
504 .build();
505 assert!(
506 result.sql.contains("1=0"),
507 "Should return impossible condition"
508 );
509 }
510
511 #[test]
512 fn test_between_with_different_value_types() {
513 // BETWEEN with different value types (strings for date range)
514 let result = postgres("orders")
515 .fields(&["id", "created_at"])
516 .filter(
517 "created_at",
518 Operator::Between,
519 Value::Array(vec![
520 Value::String("2024-01-01".into()),
521 Value::String("2024-12-31".into()),
522 ]),
523 )
524 .build();
525
526 assert!(result.sql.contains("BETWEEN $1 AND $2"));
527 assert_eq!(result.params.len(), 2);
528 }
529
530 #[test]
531 fn test_between_sqlite_dialect() {
532 // BETWEEN with SQLite dialect
533 let result = sqlite("products")
534 .fields(&["id", "price"])
535 .filter(
536 "price",
537 Operator::Between,
538 Value::Array(vec![Value::Float(9.99), Value::Float(99.99)]),
539 )
540 .build();
541
542 assert!(result.sql.contains("BETWEEN ?1 AND ?2"));
543 assert_eq!(result.params.len(), 2);
544 }
545}
546
547// ============================================================================
548// API Contract Tests (compile-time assertions)
549// ============================================================================
550
551#[cfg(test)]
552mod api_contracts {
553 use static_assertions::assert_impl_all;
554
555 // ========================================================================
556 // Query Builder types
557 // ========================================================================
558
559 // QueryResult is Clone, Debug, PartialEq
560 assert_impl_all!(crate::QueryResult: Clone, std::fmt::Debug, PartialEq);
561
562 // Cursor is Clone, Debug, PartialEq
563 assert_impl_all!(crate::Cursor: Clone, std::fmt::Debug, PartialEq);
564
565 // PageInfo is Clone, Debug, PartialEq, Eq, Default
566 assert_impl_all!(crate::PageInfo: Clone, std::fmt::Debug, PartialEq, Eq, Default);
567
568 // ========================================================================
569 // Value and Filter types
570 // ========================================================================
571
572 // Value is Clone, Debug, PartialEq (no Eq because of Float)
573 assert_impl_all!(crate::Value: Clone, std::fmt::Debug, PartialEq);
574
575 // Filter is Clone, Debug, PartialEq
576 assert_impl_all!(crate::Filter: Clone, std::fmt::Debug, PartialEq);
577
578 // FilterExpr is Clone, Debug, PartialEq
579 assert_impl_all!(crate::FilterExpr: Clone, std::fmt::Debug, PartialEq);
580
581 // ========================================================================
582 // Enum types
583 // ========================================================================
584
585 // Operator is Copy, Clone, Debug, PartialEq, Eq
586 assert_impl_all!(crate::Operator: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
587
588 // LogicalOp is Copy, Clone, Debug, PartialEq, Eq
589 assert_impl_all!(crate::LogicalOp: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
590
591 // SortDir is Copy, Clone, Debug, PartialEq, Eq
592 assert_impl_all!(crate::SortDir: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
593
594 // CursorDirection is Copy, Clone, Debug, PartialEq, Eq
595 assert_impl_all!(crate::CursorDirection: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
596
597 // AggregateFunc is Copy, Clone, Debug, PartialEq, Eq
598 assert_impl_all!(crate::AggregateFunc: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
599
600 // ========================================================================
601 // Error types
602 // ========================================================================
603
604 // CursorError is Clone, Debug, PartialEq, Eq
605 assert_impl_all!(crate::CursorError: Clone, std::fmt::Debug, PartialEq, Eq);
606
607 // ValidationError is Clone, Debug, PartialEq, Eq
608 assert_impl_all!(crate::ValidationError: Clone, std::fmt::Debug, PartialEq, Eq);
609
610 // ========================================================================
611 // Helper types
612 // ========================================================================
613
614 // SortField is Clone, Debug, PartialEq, Eq
615 assert_impl_all!(crate::SortField: Clone, std::fmt::Debug, PartialEq, Eq);
616
617 // Aggregate is Clone, Debug, PartialEq, Eq
618 assert_impl_all!(crate::Aggregate: Clone, std::fmt::Debug, PartialEq, Eq);
619
620 // ComputedField is Clone, Debug, PartialEq, Eq
621 assert_impl_all!(crate::ComputedField: Clone, std::fmt::Debug, PartialEq, Eq);
622
623 // KeysetCondition is Clone, Debug, PartialEq
624 assert_impl_all!(crate::KeysetCondition: Clone, std::fmt::Debug, PartialEq);
625}