1#![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#![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;
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#[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
129pub use mik_sql_macros::{ids, sql_create, sql_delete, sql_read, sql_update};
131
132#[must_use]
136pub fn postgres(table: &str) -> QueryBuilder<Postgres> {
137 QueryBuilder::new(Postgres, table)
138}
139
140#[must_use]
144pub fn sqlite(table: &str) -> QueryBuilder<Sqlite> {
145 QueryBuilder::new(Sqlite, table)
146}
147
148pub 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 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 #[test]
224 fn test_multi_field_cursor_mixed_sort_directions() {
225 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) .sort("id", SortDir::Asc) .after_cursor(cursor)
236 .limit(20)
237 .build();
238
239 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 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 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 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 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 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 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 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 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 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 let cursor = Cursor::new()
366 .string("extra_field", "should_be_ignored")
367 .int("another_extra", 999)
368 .int("id", 42); let result = postgres("users")
371 .fields(&["id", "name"])
372 .sort("id", SortDir::Asc) .after_cursor(cursor)
374 .limit(20)
375 .build();
376
377 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 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 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 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 assert!(result.sql.contains("= ANY($1)"));
420 assert_eq!(result.params.len(), 1); }
422
423 #[test]
428 fn test_between_with_exactly_two_values() {
429 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 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 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 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 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 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#[cfg(test)]
531mod api_contracts {
532 use static_assertions::assert_impl_all;
533
534 assert_impl_all!(crate::QueryResult: Clone, std::fmt::Debug, PartialEq);
540
541 assert_impl_all!(crate::Cursor: Clone, std::fmt::Debug, PartialEq);
543
544 assert_impl_all!(crate::PageInfo: Clone, std::fmt::Debug, PartialEq, Eq, Default);
546
547 assert_impl_all!(crate::Value: Clone, std::fmt::Debug, PartialEq);
553
554 assert_impl_all!(crate::Filter: Clone, std::fmt::Debug, PartialEq);
556
557 assert_impl_all!(crate::FilterExpr: Clone, std::fmt::Debug, PartialEq);
559
560 assert_impl_all!(crate::Operator: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
566
567 assert_impl_all!(crate::LogicalOp: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
569
570 assert_impl_all!(crate::SortDir: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
572
573 assert_impl_all!(crate::CursorDirection: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
575
576 assert_impl_all!(crate::AggregateFunc: Copy, Clone, std::fmt::Debug, PartialEq, Eq);
578
579 assert_impl_all!(crate::CursorError: Clone, std::fmt::Debug, PartialEq, Eq);
585
586 assert_impl_all!(crate::ValidationError: Clone, std::fmt::Debug, PartialEq, Eq);
588
589 assert_impl_all!(crate::SortField: Clone, std::fmt::Debug, PartialEq, Eq);
595
596 assert_impl_all!(crate::Aggregate: Clone, std::fmt::Debug, PartialEq, Eq);
598
599 assert_impl_all!(crate::ComputedField: Clone, std::fmt::Debug, PartialEq, Eq);
601
602 assert_impl_all!(crate::KeysetCondition: Clone, std::fmt::Debug, PartialEq);
604}