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, QueryBuilder, QueryResult, SortDir,
116    SortField, UpdateBuilder, Value, and, delete, insert, not, or, simple, update,
117};
118
119// Internal functions used by macros - not part of public API
120#[doc(hidden)]
121pub use builder::{delete_sqlite, insert_sqlite, update_sqlite};
122pub use dialect::{Dialect, Postgres, Sqlite};
123pub use pagination::{Cursor, CursorError, IntoCursor, KeysetCondition, PageInfo};
124pub use validate::{
125    FilterValidator, ValidationError, assert_valid_sql_expression, assert_valid_sql_identifier,
126    is_valid_sql_expression, is_valid_sql_identifier, merge_filters,
127};
128
129// Re-export macros from mik-sql-macros
130pub use mik_sql_macros::{ids, sql_create, sql_delete, sql_read, sql_update};
131
132/// Build a query for Postgres.
133///
134/// Convenience function that creates a `QueryBuilder` with Postgres dialect.
135#[must_use]
136pub fn postgres(table: &str) -> QueryBuilder<Postgres> {
137    QueryBuilder::new(Postgres, table)
138}
139
140/// Build a query for `SQLite`.
141///
142/// Convenience function that creates a `QueryBuilder` with `SQLite` dialect.
143#[must_use]
144pub fn sqlite(table: &str) -> QueryBuilder<Sqlite> {
145    QueryBuilder::new(Sqlite, table)
146}
147
148/// Prelude module for convenient imports.
149///
150/// ```
151/// use mik_sql::prelude::*;
152/// // Now Cursor, PageInfo, postgres(), sqlite(), etc. are available
153/// let result = postgres("users").fields(&["id"]).build();
154/// assert!(result.sql.contains("SELECT id FROM users"));
155/// ```
156pub mod prelude {
157    pub use crate::{
158        Aggregate, AggregateFunc, CompoundFilter, ComputedField, Cursor, CursorDirection,
159        CursorError, DeleteBuilder, Dialect, Filter, FilterExpr, FilterValidator, InsertBuilder,
160        IntoCursor, KeysetCondition, LogicalOp, Operator, PageInfo, Postgres, QueryBuilder,
161        QueryResult, SortDir, SortField, Sqlite, UpdateBuilder, ValidationError, Value, and,
162        delete, insert, merge_filters, not, or, postgres, simple, sqlite, update,
163    };
164
165    // Re-export macros
166    pub use mik_sql_macros::{ids, sql_create, sql_delete, sql_read, sql_update};
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_simple_select() {
175        let result = postgres("users").fields(&["id", "name", "email"]).build();
176
177        assert_eq!(result.sql, "SELECT id, name, email FROM users");
178        assert!(result.params.is_empty());
179    }
180
181    #[test]
182    fn test_select_with_filter() {
183        let result = postgres("users")
184            .fields(&["id", "name"])
185            .filter("active", Operator::Eq, Value::Bool(true))
186            .build();
187
188        assert_eq!(result.sql, "SELECT id, name FROM users WHERE active = $1");
189        assert_eq!(result.params.len(), 1);
190    }
191
192    #[test]
193    fn test_sqlite_dialect() {
194        let result = sqlite("users")
195            .fields(&["id", "name"])
196            .filter("active", Operator::Eq, Value::Bool(true))
197            .build();
198
199        assert_eq!(result.sql, "SELECT id, name FROM users WHERE active = ?1");
200    }
201
202    #[test]
203    fn test_cursor_pagination() {
204        let cursor = Cursor::new().int("id", 100);
205
206        let result = postgres("users")
207            .fields(&["id", "name"])
208            .sort("id", SortDir::Asc)
209            .after_cursor(cursor)
210            .limit(20)
211            .build();
212
213        assert_eq!(
214            result.sql,
215            "SELECT id, name FROM users WHERE id > $1 ORDER BY id ASC LIMIT 20"
216        );
217    }
218
219    // ═══════════════════════════════════════════════════════════════════════════
220    // EDGE CASE TESTS
221    // ═══════════════════════════════════════════════════════════════════════════
222
223    #[test]
224    fn test_multi_field_cursor_mixed_sort_directions() {
225        // Edge case: Multi-field cursor with mixed ASC/DESC sort directions
226        // DESC fields use < operator, ASC fields use > operator
227        let cursor = Cursor::new()
228            .string("created_at", "2024-01-15T10:00:00Z")
229            .int("id", 42);
230
231        let result = postgres("posts")
232            .fields(&["id", "title", "created_at"])
233            .sort("created_at", SortDir::Desc) // Newest first
234            .sort("id", SortDir::Asc) // Then by ID ascending
235            .after_cursor(cursor)
236            .limit(20)
237            .build();
238
239        // Multi-field keyset uses tuple comparison: (created_at, id) < ($1, $2)
240        // For DESC primary, ASC secondary: items after cursor have (smaller created_at) OR (same created_at AND larger id)
241        assert!(result.sql.contains("ORDER BY created_at DESC, id ASC"));
242        assert_eq!(result.params.len(), 2);
243    }
244
245    #[test]
246    fn test_multi_field_cursor_all_desc() {
247        // Edge case: All fields descending
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"])
254            .sort("created_at", SortDir::Desc)
255            .sort("id", SortDir::Desc)
256            .after_cursor(cursor)
257            .limit(10)
258            .build();
259
260        assert!(result.sql.contains("ORDER BY created_at DESC, id DESC"));
261    }
262
263    #[test]
264    fn test_sqlite_between_operator() {
265        // Edge case: SQLite BETWEEN handling with expanded parameters
266        let result = sqlite("products")
267            .fields(&["id", "name", "price"])
268            .filter(
269                "price",
270                Operator::Between,
271                Value::Array(vec![Value::Float(10.0), Value::Float(100.0)]),
272            )
273            .build();
274
275        // SQLite BETWEEN uses ?1 AND ?2 placeholders
276        assert!(result.sql.contains("BETWEEN"));
277        assert!(result.sql.contains("?1"));
278        assert!(result.sql.contains("?2"));
279        assert_eq!(result.params.len(), 2);
280    }
281
282    #[test]
283    fn test_postgres_between_operator() {
284        // Postgres BETWEEN for comparison
285        let result = postgres("products")
286            .fields(&["id", "name", "price"])
287            .filter(
288                "price",
289                Operator::Between,
290                Value::Array(vec![Value::Int(10), Value::Int(100)]),
291            )
292            .build();
293
294        assert!(result.sql.contains("BETWEEN"));
295        assert!(result.sql.contains("$1"));
296        assert!(result.sql.contains("$2"));
297        assert_eq!(result.params.len(), 2);
298    }
299
300    #[test]
301    fn test_compound_filter_nested() {
302        use builder::{CompoundFilter, FilterExpr, simple};
303
304        // Edge case: Nested compound filters (AND containing OR)
305        let nested_or = CompoundFilter::or(vec![
306            simple("status", Operator::Eq, Value::String("active".into())),
307            simple("status", Operator::Eq, Value::String("pending".into())),
308        ]);
309
310        let result = postgres("orders")
311            .fields(&["id", "status", "amount"])
312            .filter_expr(FilterExpr::Compound(CompoundFilter::and(vec![
313                simple("amount", Operator::Gte, Value::Int(100)),
314                FilterExpr::Compound(nested_or),
315            ])))
316            .build();
317
318        // Should produce: amount >= $1 AND (status = $2 OR status = $3)
319        assert!(result.sql.contains("AND"));
320        assert!(result.sql.contains("OR"));
321        assert_eq!(result.params.len(), 3);
322    }
323
324    #[test]
325    fn test_compound_filter_not() {
326        use builder::{CompoundFilter, FilterExpr, simple};
327
328        // Edge case: NOT compound filter
329        let result = postgres("users")
330            .fields(&["id", "name", "role"])
331            .filter_expr(FilterExpr::Compound(CompoundFilter::not(simple(
332                "role",
333                Operator::Eq,
334                Value::String("admin".into()),
335            ))))
336            .build();
337
338        assert!(result.sql.contains("NOT"));
339        assert_eq!(result.params.len(), 1);
340    }
341
342    #[test]
343    fn test_empty_cursor_ignored() {
344        // Edge case: Empty cursor should not add any keyset conditions
345        let cursor = Cursor::new();
346
347        let result = postgres("users")
348            .fields(&["id", "name"])
349            .sort("id", SortDir::Asc)
350            .after_cursor(cursor)
351            .limit(20)
352            .build();
353
354        // Should not have WHERE clause from empty cursor
355        assert_eq!(
356            result.sql,
357            "SELECT id, name FROM users ORDER BY id ASC LIMIT 20"
358        );
359    }
360
361    #[test]
362    fn test_cursor_extra_fields_ignored() {
363        // Edge case: Cursor contains fields that are NOT in the sort specification.
364        // These extra fields should be silently ignored - only sort fields matter.
365        let cursor = Cursor::new()
366            .string("extra_field", "should_be_ignored")
367            .int("another_extra", 999)
368            .int("id", 42); // Only this matches sort
369
370        let result = postgres("users")
371            .fields(&["id", "name"])
372            .sort("id", SortDir::Asc) // Only sort by id
373            .after_cursor(cursor)
374            .limit(20)
375            .build();
376
377        // Should only use 'id' from cursor, ignore extra_field and another_extra
378        assert!(result.sql.contains("id > $1"));
379        assert_eq!(result.params.len(), 1);
380    }
381
382    #[test]
383    fn test_sqlite_in_clause_expansion() {
384        // Edge case: SQLite IN clause expands to multiple placeholders
385        let result = sqlite("users")
386            .fields(&["id", "name"])
387            .filter(
388                "status",
389                Operator::In,
390                Value::Array(vec![
391                    Value::String("active".into()),
392                    Value::String("pending".into()),
393                    Value::String("review".into()),
394                ]),
395            )
396            .build();
397
398        // SQLite: status IN (?1, ?2, ?3)
399        assert!(result.sql.contains("IN (?1, ?2, ?3)"));
400        assert_eq!(result.params.len(), 3);
401    }
402
403    #[test]
404    fn test_postgres_in_clause_array() {
405        // Postgres uses ANY with array parameter
406        let result = postgres("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                ]),
415            )
416            .build();
417
418        // Postgres: status = ANY($1)
419        assert!(result.sql.contains("= ANY($1)"));
420        assert_eq!(result.params.len(), 1); // Single array param
421    }
422
423    // =========================================================================
424    // BETWEEN OPERATOR EDGE CASE TESTS
425    // =========================================================================
426
427    #[test]
428    fn test_between_with_exactly_two_values() {
429        // BETWEEN with exactly 2 values should work
430        let result = postgres("products")
431            .fields(&["id", "price"])
432            .filter(
433                "price",
434                Operator::Between,
435                Value::Array(vec![Value::Int(10), Value::Int(100)]),
436            )
437            .build();
438
439        assert!(result.sql.contains("BETWEEN $1 AND $2"));
440        assert_eq!(result.params.len(), 2);
441    }
442
443    #[test]
444    fn test_between_with_one_value_fallback() {
445        // BETWEEN with 1 value returns impossible condition (consistent behavior)
446        let result = postgres("products")
447            .fields(&["id", "price"])
448            .filter(
449                "price",
450                Operator::Between,
451                Value::Array(vec![Value::Int(10)]),
452            )
453            .build();
454        assert!(
455            result.sql.contains("1=0"),
456            "Should return impossible condition"
457        );
458    }
459
460    #[test]
461    fn test_between_with_three_values_fallback() {
462        // BETWEEN with 3 values returns impossible condition (consistent behavior)
463        let result = postgres("products")
464            .fields(&["id", "price"])
465            .filter(
466                "price",
467                Operator::Between,
468                Value::Array(vec![Value::Int(10), Value::Int(50), Value::Int(100)]),
469            )
470            .build();
471        assert!(
472            result.sql.contains("1=0"),
473            "Should return impossible condition"
474        );
475    }
476
477    #[test]
478    fn test_between_with_empty_array_fallback() {
479        // BETWEEN with empty array returns impossible condition (consistent behavior)
480        let result = postgres("products")
481            .fields(&["id", "price"])
482            .filter("price", Operator::Between, Value::Array(vec![]))
483            .build();
484        assert!(
485            result.sql.contains("1=0"),
486            "Should return impossible condition"
487        );
488    }
489
490    #[test]
491    fn test_between_with_different_value_types() {
492        // BETWEEN with different value types (strings for date range)
493        let result = postgres("orders")
494            .fields(&["id", "created_at"])
495            .filter(
496                "created_at",
497                Operator::Between,
498                Value::Array(vec![
499                    Value::String("2024-01-01".into()),
500                    Value::String("2024-12-31".into()),
501                ]),
502            )
503            .build();
504
505        assert!(result.sql.contains("BETWEEN $1 AND $2"));
506        assert_eq!(result.params.len(), 2);
507    }
508
509    #[test]
510    fn test_between_sqlite_dialect() {
511        // BETWEEN with SQLite dialect
512        let result = sqlite("products")
513            .fields(&["id", "price"])
514            .filter(
515                "price",
516                Operator::Between,
517                Value::Array(vec![Value::Float(9.99), Value::Float(99.99)]),
518            )
519            .build();
520
521        assert!(result.sql.contains("BETWEEN ?1 AND ?2"));
522        assert_eq!(result.params.len(), 2);
523    }
524}
525
526// ============================================================================
527// API Contract Tests (compile-time assertions)
528// ============================================================================
529
530#[cfg(test)]
531mod api_contracts {
532    use static_assertions::assert_impl_all;
533
534    // ========================================================================
535    // Query Builder types
536    // ========================================================================
537
538    // QueryResult is Clone, Debug, PartialEq
539    assert_impl_all!(crate::QueryResult: Clone, std::fmt::Debug, PartialEq);
540
541    // Cursor is Clone, Debug, PartialEq
542    assert_impl_all!(crate::Cursor: Clone, std::fmt::Debug, PartialEq);
543
544    // PageInfo is Clone, Debug, PartialEq, Eq, Default
545    assert_impl_all!(crate::PageInfo: Clone, std::fmt::Debug, PartialEq, Eq, Default);
546
547    // ========================================================================
548    // Value and Filter types
549    // ========================================================================
550
551    // Value is Clone, Debug, PartialEq (no Eq because of Float)
552    assert_impl_all!(crate::Value: Clone, std::fmt::Debug, PartialEq);
553
554    // Filter is Clone, Debug, PartialEq
555    assert_impl_all!(crate::Filter: Clone, std::fmt::Debug, PartialEq);
556
557    // FilterExpr is Clone, Debug, PartialEq
558    assert_impl_all!(crate::FilterExpr: Clone, std::fmt::Debug, PartialEq);
559
560    // ========================================================================
561    // Enum types
562    // ========================================================================
563
564    // Operator is Copy, Clone, Debug, PartialEq, Eq
565    assert_impl_all!(crate::Operator: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
566
567    // LogicalOp is Copy, Clone, Debug, PartialEq, Eq
568    assert_impl_all!(crate::LogicalOp: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
569
570    // SortDir is Copy, Clone, Debug, PartialEq, Eq
571    assert_impl_all!(crate::SortDir: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
572
573    // CursorDirection is Copy, Clone, Debug, PartialEq, Eq
574    assert_impl_all!(crate::CursorDirection: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
575
576    // AggregateFunc is Copy, Clone, Debug, PartialEq, Eq
577    assert_impl_all!(crate::AggregateFunc: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
578
579    // ========================================================================
580    // Error types
581    // ========================================================================
582
583    // CursorError is Clone, Debug, PartialEq, Eq
584    assert_impl_all!(crate::CursorError: Clone, std::fmt::Debug, PartialEq, Eq);
585
586    // ValidationError is Clone, Debug, PartialEq, Eq
587    assert_impl_all!(crate::ValidationError: Clone, std::fmt::Debug, PartialEq, Eq);
588
589    // ========================================================================
590    // Helper types
591    // ========================================================================
592
593    // SortField is Clone, Debug, PartialEq, Eq
594    assert_impl_all!(crate::SortField: Clone, std::fmt::Debug, PartialEq, Eq);
595
596    // Aggregate is Clone, Debug, PartialEq, Eq
597    assert_impl_all!(crate::Aggregate: Clone, std::fmt::Debug, PartialEq, Eq);
598
599    // ComputedField is Clone, Debug, PartialEq, Eq
600    assert_impl_all!(crate::ComputedField: Clone, std::fmt::Debug, PartialEq, Eq);
601
602    // KeysetCondition is Clone, Debug, PartialEq
603    assert_impl_all!(crate::KeysetCondition: Clone, std::fmt::Debug, PartialEq);
604}