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}