1#![forbid(unsafe_code)]
5#![deny(unused_must_use)]
6#![warn(missing_debug_implementations)]
7#![warn(rust_2018_idioms)]
8#![warn(unreachable_pub)]
9#![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)] mod builder;
115mod dialect;
116mod pagination;
117mod validate;
118
119pub use builder::{
120 Aggregate, AggregateFunc, CompoundFilter, ComputedField, CursorDirection, DeleteBuilder,
121 Filter, FilterExpr, InsertBuilder, LogicalOp, Operator, QueryBuilder, QueryResult, SortDir,
122 SortField, UpdateBuilder, Value, and, delete, insert, not, or, simple, update,
123};
124
125#[doc(hidden)]
127pub use builder::{delete_sqlite, insert_sqlite, update_sqlite};
128pub use dialect::{Dialect, Postgres, Sqlite};
129pub use pagination::{Cursor, CursorError, IntoCursor, KeysetCondition, PageInfo};
130pub use validate::{
131 FilterValidator, ValidationError, assert_valid_sql_expression, assert_valid_sql_identifier,
132 is_valid_sql_expression, is_valid_sql_identifier, merge_filters,
133};
134
135pub use mik_sql_macros::{ids, sql_create, sql_delete, sql_read, sql_update};
137
138#[must_use]
142pub fn postgres(table: &str) -> QueryBuilder<Postgres> {
143 QueryBuilder::new(Postgres, table)
144}
145
146#[must_use]
150pub fn sqlite(table: &str) -> QueryBuilder<Sqlite> {
151 QueryBuilder::new(Sqlite, table)
152}
153
154pub mod prelude {
160 pub use crate::{
161 Aggregate, AggregateFunc, CompoundFilter, ComputedField, Cursor, CursorDirection,
162 CursorError, DeleteBuilder, Dialect, Filter, FilterExpr, FilterValidator, InsertBuilder,
163 IntoCursor, KeysetCondition, LogicalOp, Operator, PageInfo, Postgres, QueryBuilder,
164 QueryResult, SortDir, SortField, Sqlite, UpdateBuilder, ValidationError, Value, and,
165 delete, insert, merge_filters, not, or, postgres, simple, sqlite, update,
166 };
167
168 pub use mik_sql_macros::{ids, sql_create, sql_delete, sql_read, sql_update};
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_simple_select() {
178 let result = postgres("users").fields(&["id", "name", "email"]).build();
179
180 assert_eq!(result.sql, "SELECT id, name, email FROM users");
181 assert!(result.params.is_empty());
182 }
183
184 #[test]
185 fn test_select_with_filter() {
186 let result = postgres("users")
187 .fields(&["id", "name"])
188 .filter("active", Operator::Eq, Value::Bool(true))
189 .build();
190
191 assert_eq!(result.sql, "SELECT id, name FROM users WHERE active = $1");
192 assert_eq!(result.params.len(), 1);
193 }
194
195 #[test]
196 fn test_sqlite_dialect() {
197 let result = sqlite("users")
198 .fields(&["id", "name"])
199 .filter("active", Operator::Eq, Value::Bool(true))
200 .build();
201
202 assert_eq!(result.sql, "SELECT id, name FROM users WHERE active = ?1");
203 }
204
205 #[test]
206 fn test_cursor_pagination() {
207 let cursor = Cursor::new().int("id", 100);
208
209 let result = postgres("users")
210 .fields(&["id", "name"])
211 .sort("id", SortDir::Asc)
212 .after_cursor(cursor.clone())
213 .limit(20)
214 .build();
215
216 assert_eq!(
217 result.sql,
218 "SELECT id, name FROM users WHERE id > $1 ORDER BY id ASC LIMIT 20"
219 );
220 }
221
222 #[test]
227 fn test_multi_field_cursor_mixed_sort_directions() {
228 let cursor = Cursor::new()
231 .string("created_at", "2024-01-15T10:00:00Z")
232 .int("id", 42);
233
234 let result = postgres("posts")
235 .fields(&["id", "title", "created_at"])
236 .sort("created_at", SortDir::Desc) .sort("id", SortDir::Asc) .after_cursor(cursor.clone())
239 .limit(20)
240 .build();
241
242 assert!(result.sql.contains("ORDER BY created_at DESC, id ASC"));
245 assert_eq!(result.params.len(), 2);
246 }
247
248 #[test]
249 fn test_multi_field_cursor_all_desc() {
250 let cursor = Cursor::new()
252 .string("created_at", "2024-01-15T10:00:00Z")
253 .int("id", 42);
254
255 let result = postgres("posts")
256 .fields(&["id", "title"])
257 .sort("created_at", SortDir::Desc)
258 .sort("id", SortDir::Desc)
259 .after_cursor(cursor.clone())
260 .limit(10)
261 .build();
262
263 assert!(result.sql.contains("ORDER BY created_at DESC, id DESC"));
264 }
265
266 #[test]
267 fn test_sqlite_between_operator() {
268 let result = sqlite("products")
270 .fields(&["id", "name", "price"])
271 .filter(
272 "price",
273 Operator::Between,
274 Value::Array(vec![Value::Float(10.0), Value::Float(100.0)]),
275 )
276 .build();
277
278 assert!(result.sql.contains("BETWEEN"));
280 assert!(result.sql.contains("?1"));
281 assert!(result.sql.contains("?2"));
282 assert_eq!(result.params.len(), 2);
283 }
284
285 #[test]
286 fn test_postgres_between_operator() {
287 let result = postgres("products")
289 .fields(&["id", "name", "price"])
290 .filter(
291 "price",
292 Operator::Between,
293 Value::Array(vec![Value::Int(10), Value::Int(100)]),
294 )
295 .build();
296
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_compound_filter_nested() {
305 use builder::{CompoundFilter, FilterExpr, simple};
306
307 let nested_or = CompoundFilter::or(vec![
309 simple("status", Operator::Eq, Value::String("active".into())),
310 simple("status", Operator::Eq, Value::String("pending".into())),
311 ]);
312
313 let result = postgres("orders")
314 .fields(&["id", "status", "amount"])
315 .filter_expr(FilterExpr::Compound(CompoundFilter::and(vec![
316 simple("amount", Operator::Gte, Value::Int(100)),
317 FilterExpr::Compound(nested_or),
318 ])))
319 .build();
320
321 assert!(result.sql.contains("AND"));
323 assert!(result.sql.contains("OR"));
324 assert_eq!(result.params.len(), 3);
325 }
326
327 #[test]
328 fn test_compound_filter_not() {
329 use builder::{CompoundFilter, FilterExpr, simple};
330
331 let result = postgres("users")
333 .fields(&["id", "name", "role"])
334 .filter_expr(FilterExpr::Compound(CompoundFilter::not(simple(
335 "role",
336 Operator::Eq,
337 Value::String("admin".into()),
338 ))))
339 .build();
340
341 assert!(result.sql.contains("NOT"));
342 assert_eq!(result.params.len(), 1);
343 }
344
345 #[test]
346 fn test_empty_cursor_ignored() {
347 let cursor = Cursor::new();
349
350 let result = postgres("users")
351 .fields(&["id", "name"])
352 .sort("id", SortDir::Asc)
353 .after_cursor(cursor.clone())
354 .limit(20)
355 .build();
356
357 assert_eq!(
359 result.sql,
360 "SELECT id, name FROM users ORDER BY id ASC LIMIT 20"
361 );
362 }
363
364 #[test]
365 fn test_cursor_extra_fields_ignored() {
366 let cursor = Cursor::new()
369 .string("extra_field", "should_be_ignored")
370 .int("another_extra", 999)
371 .int("id", 42); let result = postgres("users")
374 .fields(&["id", "name"])
375 .sort("id", SortDir::Asc) .after_cursor(cursor.clone())
377 .limit(20)
378 .build();
379
380 assert!(result.sql.contains("id > $1"));
382 assert_eq!(result.params.len(), 1);
383 }
384
385 #[test]
386 fn test_sqlite_in_clause_expansion() {
387 let result = sqlite("users")
389 .fields(&["id", "name"])
390 .filter(
391 "status",
392 Operator::In,
393 Value::Array(vec![
394 Value::String("active".into()),
395 Value::String("pending".into()),
396 Value::String("review".into()),
397 ]),
398 )
399 .build();
400
401 assert!(result.sql.contains("IN (?1, ?2, ?3)"));
403 assert_eq!(result.params.len(), 3);
404 }
405
406 #[test]
407 fn test_postgres_in_clause_array() {
408 let result = postgres("users")
410 .fields(&["id", "name"])
411 .filter(
412 "status",
413 Operator::In,
414 Value::Array(vec![
415 Value::String("active".into()),
416 Value::String("pending".into()),
417 ]),
418 )
419 .build();
420
421 assert!(result.sql.contains("= ANY($1)"));
423 assert_eq!(result.params.len(), 1); }
425
426 #[test]
431 fn test_between_with_exactly_two_values() {
432 let result = postgres("products")
434 .fields(&["id", "price"])
435 .filter(
436 "price",
437 Operator::Between,
438 Value::Array(vec![Value::Int(10), Value::Int(100)]),
439 )
440 .build();
441
442 assert!(result.sql.contains("BETWEEN $1 AND $2"));
443 assert_eq!(result.params.len(), 2);
444 }
445
446 #[test]
447 fn test_between_with_one_value_fallback() {
448 let result = postgres("products")
450 .fields(&["id", "price"])
451 .filter(
452 "price",
453 Operator::Between,
454 Value::Array(vec![Value::Int(10)]),
455 )
456 .build();
457 assert!(
458 result.sql.contains("1=0"),
459 "Should return impossible condition"
460 );
461 }
462
463 #[test]
464 fn test_between_with_three_values_fallback() {
465 let result = postgres("products")
467 .fields(&["id", "price"])
468 .filter(
469 "price",
470 Operator::Between,
471 Value::Array(vec![Value::Int(10), Value::Int(50), Value::Int(100)]),
472 )
473 .build();
474 assert!(
475 result.sql.contains("1=0"),
476 "Should return impossible condition"
477 );
478 }
479
480 #[test]
481 fn test_between_with_empty_array_fallback() {
482 let result = postgres("products")
484 .fields(&["id", "price"])
485 .filter("price", Operator::Between, Value::Array(vec![]))
486 .build();
487 assert!(
488 result.sql.contains("1=0"),
489 "Should return impossible condition"
490 );
491 }
492
493 #[test]
494 fn test_between_with_different_value_types() {
495 let result = postgres("orders")
497 .fields(&["id", "created_at"])
498 .filter(
499 "created_at",
500 Operator::Between,
501 Value::Array(vec![
502 Value::String("2024-01-01".into()),
503 Value::String("2024-12-31".into()),
504 ]),
505 )
506 .build();
507
508 assert!(result.sql.contains("BETWEEN $1 AND $2"));
509 assert_eq!(result.params.len(), 2);
510 }
511
512 #[test]
513 fn test_between_sqlite_dialect() {
514 let result = sqlite("products")
516 .fields(&["id", "price"])
517 .filter(
518 "price",
519 Operator::Between,
520 Value::Array(vec![Value::Float(9.99), Value::Float(99.99)]),
521 )
522 .build();
523
524 assert!(result.sql.contains("BETWEEN ?1 AND ?2"));
525 assert_eq!(result.params.len(), 2);
526 }
527}