1mod delete;
4mod expr;
5mod insert;
6mod join;
7mod order;
8mod pagination;
9mod predicate;
10mod select;
11mod update;
12
13use sql_orm_core::{CrateIdentity, SqlValue};
14
15pub use delete::DeleteQuery;
16pub use expr::{BinaryOp, ColumnRef, Expr, TableRef, UnaryOp};
17pub use insert::InsertQuery;
18pub use join::{Join, JoinType};
19pub use order::{OrderBy, SortDirection};
20pub use pagination::Pagination;
21pub use predicate::Predicate;
22pub use select::{CountQuery, SelectProjection, SelectQuery};
23pub use update::UpdateQuery;
24
25#[derive(Debug, Clone, PartialEq)]
26pub struct CompiledQuery {
27 pub sql: String,
28 pub params: Vec<SqlValue>,
29}
30
31impl CompiledQuery {
32 pub fn new(sql: impl Into<String>, params: Vec<SqlValue>) -> Self {
33 Self {
34 sql: sql.into(),
35 params,
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub enum Query {
42 Select(SelectQuery),
43 Insert(InsertQuery),
44 Update(UpdateQuery),
45 Delete(DeleteQuery),
46 Count(CountQuery),
47}
48
49pub const CRATE_IDENTITY: CrateIdentity = CrateIdentity {
50 name: "sql-orm-query",
51 responsibility: "typed AST and query builder primitives without SQL generation",
52};
53
54#[cfg(test)]
55mod tests {
56 use super::{
57 BinaryOp, CRATE_IDENTITY, ColumnRef, CompiledQuery, CountQuery, DeleteQuery, Expr,
58 InsertQuery, Join, JoinType, OrderBy, Pagination, Predicate, Query, SelectProjection,
59 SelectQuery, SortDirection, TableRef, UpdateQuery,
60 };
61 use sql_orm_core::{
62 Changeset, ColumnMetadata, ColumnValue, Entity, EntityColumn, EntityMetadata,
63 IdentityMetadata, Insertable, PrimaryKeyMetadata, SqlServerType, SqlValue,
64 };
65
66 #[allow(dead_code)]
67 struct Customer;
68
69 #[allow(dead_code)]
70 struct Order;
71
72 static CUSTOMER_COLUMNS: [ColumnMetadata; 4] = [
73 ColumnMetadata {
74 rust_field: "id",
75 column_name: "id",
76 renamed_from: None,
77 sql_type: SqlServerType::BigInt,
78 nullable: false,
79 primary_key: true,
80 identity: Some(IdentityMetadata::new(1, 1)),
81 default_sql: None,
82 computed_sql: None,
83 rowversion: false,
84 insertable: false,
85 updatable: false,
86 max_length: None,
87 precision: None,
88 scale: None,
89 },
90 ColumnMetadata {
91 rust_field: "email",
92 column_name: "email",
93 renamed_from: None,
94 sql_type: SqlServerType::NVarChar,
95 nullable: false,
96 primary_key: false,
97 identity: None,
98 default_sql: None,
99 computed_sql: None,
100 rowversion: false,
101 insertable: true,
102 updatable: true,
103 max_length: Some(160),
104 precision: None,
105 scale: None,
106 },
107 ColumnMetadata {
108 rust_field: "active",
109 column_name: "active",
110 renamed_from: None,
111 sql_type: SqlServerType::Bit,
112 nullable: false,
113 primary_key: false,
114 identity: None,
115 default_sql: Some("1"),
116 computed_sql: None,
117 rowversion: false,
118 insertable: true,
119 updatable: true,
120 max_length: None,
121 precision: None,
122 scale: None,
123 },
124 ColumnMetadata {
125 rust_field: "created_at",
126 column_name: "created_at",
127 renamed_from: None,
128 sql_type: SqlServerType::DateTime2,
129 nullable: false,
130 primary_key: false,
131 identity: None,
132 default_sql: Some("SYSUTCDATETIME()"),
133 computed_sql: None,
134 rowversion: false,
135 insertable: true,
136 updatable: true,
137 max_length: None,
138 precision: None,
139 scale: None,
140 },
141 ];
142
143 static CUSTOMER_METADATA: EntityMetadata = EntityMetadata {
144 rust_name: "Customer",
145 schema: "sales",
146 table: "customers",
147 renamed_from: None,
148 columns: &CUSTOMER_COLUMNS,
149 primary_key: PrimaryKeyMetadata::new(Some("pk_customers"), &["id"]),
150 indexes: &[],
151 foreign_keys: &[],
152 navigations: &[],
153 };
154
155 impl Entity for Customer {
156 fn metadata() -> &'static EntityMetadata {
157 &CUSTOMER_METADATA
158 }
159 }
160
161 static ORDER_COLUMNS: [ColumnMetadata; 3] = [
162 ColumnMetadata {
163 rust_field: "id",
164 column_name: "id",
165 renamed_from: None,
166 sql_type: SqlServerType::BigInt,
167 nullable: false,
168 primary_key: true,
169 identity: Some(IdentityMetadata::new(1, 1)),
170 default_sql: None,
171 computed_sql: None,
172 rowversion: false,
173 insertable: false,
174 updatable: false,
175 max_length: None,
176 precision: None,
177 scale: None,
178 },
179 ColumnMetadata {
180 rust_field: "customer_id",
181 column_name: "customer_id",
182 renamed_from: None,
183 sql_type: SqlServerType::BigInt,
184 nullable: false,
185 primary_key: false,
186 identity: None,
187 default_sql: None,
188 computed_sql: None,
189 rowversion: false,
190 insertable: true,
191 updatable: true,
192 max_length: None,
193 precision: None,
194 scale: None,
195 },
196 ColumnMetadata {
197 rust_field: "total_cents",
198 column_name: "total_cents",
199 renamed_from: None,
200 sql_type: SqlServerType::BigInt,
201 nullable: false,
202 primary_key: false,
203 identity: None,
204 default_sql: None,
205 computed_sql: None,
206 rowversion: false,
207 insertable: true,
208 updatable: true,
209 max_length: None,
210 precision: None,
211 scale: None,
212 },
213 ];
214
215 static ORDER_METADATA: EntityMetadata = EntityMetadata {
216 rust_name: "Order",
217 schema: "sales",
218 table: "orders",
219 renamed_from: None,
220 columns: &ORDER_COLUMNS,
221 primary_key: PrimaryKeyMetadata::new(Some("pk_orders"), &["id"]),
222 indexes: &[],
223 foreign_keys: &[],
224 navigations: &[],
225 };
226
227 impl Entity for Order {
228 fn metadata() -> &'static EntityMetadata {
229 &ORDER_METADATA
230 }
231 }
232
233 #[allow(non_upper_case_globals)]
234 impl Customer {
235 const id: EntityColumn<Customer> = EntityColumn::new("id", "id");
236 const email: EntityColumn<Customer> = EntityColumn::new("email", "email");
237 const active: EntityColumn<Customer> = EntityColumn::new("active", "active");
238 const created_at: EntityColumn<Customer> = EntityColumn::new("created_at", "created_at");
239 }
240
241 #[allow(non_upper_case_globals)]
242 impl Order {
243 const customer_id: EntityColumn<Order> = EntityColumn::new("customer_id", "customer_id");
244 const total_cents: EntityColumn<Order> = EntityColumn::new("total_cents", "total_cents");
245 }
246
247 struct NewCustomer {
248 email: String,
249 active: bool,
250 }
251
252 impl Insertable<Customer> for NewCustomer {
253 fn values(&self) -> Vec<ColumnValue> {
254 vec![
255 ColumnValue::new("email", SqlValue::String(self.email.clone())),
256 ColumnValue::new("active", SqlValue::Bool(self.active)),
257 ]
258 }
259 }
260
261 struct UpdateCustomer {
262 email: Option<String>,
263 }
264
265 impl Changeset<Customer> for UpdateCustomer {
266 fn changes(&self) -> Vec<ColumnValue> {
267 self.email
268 .clone()
269 .map(|email| vec![ColumnValue::new("email", SqlValue::String(email))])
270 .unwrap_or_default()
271 }
272 }
273
274 #[test]
275 fn keeps_query_layer_sql_free() {
276 assert!(
277 CRATE_IDENTITY
278 .responsibility
279 .contains("without SQL generation")
280 );
281 }
282
283 #[test]
284 fn entity_columns_become_table_aware_column_refs() {
285 let column = ColumnRef::for_entity_column(Customer::email);
286
287 assert_eq!(column.table, TableRef::new("sales", "customers"));
288 assert_eq!(column.rust_field, "email");
289 assert_eq!(column.column_name, "email");
290 }
291
292 #[test]
293 fn expr_supports_columns_values_functions_and_operations() {
294 let expr = Expr::binary(
295 Expr::function("LOWER", vec![Expr::from(Customer::email)]),
296 BinaryOp::Add,
297 Expr::value(SqlValue::String("@example.com".to_string())),
298 );
299
300 match expr {
301 Expr::Binary { left, op, right } => {
302 assert_eq!(op, BinaryOp::Add);
303 assert!(matches!(*left, Expr::Function { .. }));
304 assert_eq!(
305 *right,
306 Expr::Value(SqlValue::String("@example.com".to_string()))
307 );
308 }
309 other => panic!("unexpected expr shape: {other:?}"),
310 }
311 }
312
313 #[test]
314 fn predicates_can_be_composed_without_sql_rendering() {
315 let predicate = Predicate::and(vec![
316 Predicate::eq(
317 Expr::from(Customer::active),
318 Expr::value(SqlValue::Bool(true)),
319 ),
320 Predicate::like(
321 Expr::from(Customer::email),
322 Expr::value(SqlValue::String("%@example.com".to_string())),
323 ),
324 ]);
325
326 match predicate {
327 Predicate::And(parts) => assert_eq!(parts.len(), 2),
328 other => panic!("unexpected predicate shape: {other:?}"),
329 }
330 }
331
332 #[test]
333 fn select_query_captures_projection_filters_order_and_pagination() {
334 let query = SelectQuery::from_entity::<Customer>()
335 .select(vec![Expr::from(Customer::id), Expr::from(Customer::email)])
336 .filter(Predicate::eq(
337 Expr::from(Customer::active),
338 Expr::value(SqlValue::Bool(true)),
339 ))
340 .filter(Predicate::like(
341 Expr::from(Customer::email),
342 Expr::value(SqlValue::String("%@example.com".to_string())),
343 ))
344 .order_by(OrderBy::desc(Customer::created_at))
345 .paginate(Pagination::page(2, 20));
346
347 assert_eq!(query.from, TableRef::new("sales", "customers"));
348 assert!(query.joins.is_empty());
349 assert_eq!(
350 query.projection,
351 vec![
352 SelectProjection::column(Customer::id),
353 SelectProjection::column(Customer::email)
354 ]
355 );
356 assert_eq!(
357 query.order_by,
358 vec![OrderBy::new(
359 TableRef::new("sales", "customers"),
360 "created_at",
361 SortDirection::Desc,
362 )]
363 );
364 assert_eq!(query.pagination, Some(Pagination::new(20, 20)));
365 assert!(matches!(query.predicate, Some(Predicate::And(_))));
366 }
367
368 #[test]
369 fn select_query_captures_explicit_joins_without_sql_rendering() {
370 let query = SelectQuery::from_entity::<Customer>()
371 .inner_join::<Order>(Predicate::eq(
372 Expr::from(Customer::id),
373 Expr::from(Order::customer_id),
374 ))
375 .join(Join::left(
376 TableRef::new("sales", "orders"),
377 Predicate::gt(
378 Expr::from(Order::total_cents),
379 Expr::value(SqlValue::I64(0)),
380 ),
381 ));
382
383 assert_eq!(query.joins.len(), 2);
384 assert_eq!(query.joins[0].join_type, JoinType::Inner);
385 assert_eq!(query.joins[0].table, TableRef::new("sales", "orders"));
386 assert!(matches!(query.joins[0].on, Predicate::Eq(_, _)));
387 assert_eq!(query.joins[1].join_type, JoinType::Left);
388 assert_eq!(query.joins[1].table, TableRef::new("sales", "orders"));
389 assert!(matches!(query.joins[1].on, Predicate::Gt(_, _)));
390 }
391
392 #[test]
393 fn table_refs_capture_optional_aliases_without_sql_rendering() {
394 let table = TableRef::for_entity_as::<Customer>("root");
395 let column = ColumnRef::for_entity_column_as(Customer::email, "root");
396 let expr = Expr::column_as(Customer::id, "root");
397
398 assert_eq!(table.schema, "sales");
399 assert_eq!(table.table, "customers");
400 assert_eq!(table.alias, Some("root"));
401 assert_eq!(table.reference_name(), "root");
402 assert_eq!(table.without_alias(), TableRef::new("sales", "customers"));
403 assert_eq!(column.table, table);
404
405 match expr {
406 Expr::Column(column) => {
407 assert_eq!(column.table.alias, Some("root"));
408 assert_eq!(column.column_name, "id");
409 }
410 other => panic!("unexpected expr shape: {other:?}"),
411 }
412 }
413
414 #[test]
415 fn select_query_captures_aliased_sources_and_repeated_joins() {
416 let query = SelectQuery::from_entity_as::<Customer>("c")
417 .inner_join_as::<Order>(
418 "created_orders",
419 Predicate::eq(
420 Expr::column_as(Customer::id, "c"),
421 Expr::column_as(Order::customer_id, "created_orders"),
422 ),
423 )
424 .left_join_as::<Order>(
425 "completed_orders",
426 Predicate::gt(
427 Expr::column_as(Order::total_cents, "completed_orders"),
428 Expr::value(SqlValue::I64(0)),
429 ),
430 );
431
432 assert_eq!(query.from, TableRef::with_alias("sales", "customers", "c"));
433 assert_eq!(query.joins.len(), 2);
434 assert_eq!(
435 query.joins[0].table,
436 TableRef::with_alias("sales", "orders", "created_orders")
437 );
438 assert_eq!(
439 query.joins[1].table,
440 TableRef::with_alias("sales", "orders", "completed_orders")
441 );
442 assert_ne!(query.joins[0].table, query.joins[1].table);
443 }
444
445 #[test]
446 fn select_projection_captures_default_and_explicit_aliases() {
447 let column_projection = SelectProjection::column(Customer::email);
448 assert_eq!(column_projection.alias, Some("email"));
449 assert_eq!(column_projection.expr, Expr::from(Customer::email));
450
451 let expression_projection = SelectProjection::expr_as(
452 Expr::function("LOWER", vec![Expr::from(Customer::email)]),
453 "email_lower",
454 );
455 assert_eq!(expression_projection.alias, Some("email_lower"));
456
457 let unaliased_expression =
458 SelectProjection::expr(Expr::function("LOWER", vec![Expr::from(Customer::email)]));
459 assert_eq!(unaliased_expression.alias, None);
460 }
461
462 #[test]
463 fn insert_update_delete_and_count_queries_capture_operation_data() {
464 let insert = InsertQuery::for_entity::<Customer, _>(&NewCustomer {
465 email: "ana@example.com".to_string(),
466 active: true,
467 });
468 let update = UpdateQuery::for_entity::<Customer, _>(&UpdateCustomer {
469 email: Some("ana.maria@example.com".to_string()),
470 })
471 .filter(Predicate::eq(
472 Expr::from(Customer::id),
473 Expr::value(SqlValue::I64(7)),
474 ));
475 let delete = DeleteQuery::from_entity::<Customer>().filter(Predicate::eq(
476 Expr::from(Customer::id),
477 Expr::value(SqlValue::I64(7)),
478 ));
479 let count = CountQuery::from_entity::<Customer>().filter(Predicate::eq(
480 Expr::from(Customer::active),
481 Expr::value(SqlValue::Bool(true)),
482 ));
483
484 assert_eq!(insert.into, TableRef::new("sales", "customers"));
485 assert_eq!(insert.values.len(), 2);
486 assert_eq!(update.table, TableRef::new("sales", "customers"));
487 assert_eq!(update.changes.len(), 1);
488 assert!(update.predicate.is_some());
489 assert_eq!(delete.from, TableRef::new("sales", "customers"));
490 assert!(delete.predicate.is_some());
491 assert_eq!(count.from, TableRef::new("sales", "customers"));
492 assert!(count.predicate.is_some());
493
494 assert!(matches!(Query::Insert(insert.clone()), Query::Insert(_)));
495 assert!(matches!(Query::Update(update.clone()), Query::Update(_)));
496 assert!(matches!(Query::Delete(delete.clone()), Query::Delete(_)));
497 assert!(matches!(Query::Count(count.clone()), Query::Count(_)));
498 }
499
500 #[test]
501 fn compiled_query_keeps_sql_and_parameter_order() {
502 let compiled = CompiledQuery::new(
503 "SELECT [id] FROM [sales].[customers] WHERE [active] = @P1 AND [email] LIKE @P2",
504 vec![
505 SqlValue::Bool(true),
506 SqlValue::String("%@example.com".to_string()),
507 ],
508 );
509
510 assert_eq!(
511 compiled.sql,
512 "SELECT [id] FROM [sales].[customers] WHERE [active] = @P1 AND [email] LIKE @P2"
513 );
514 assert_eq!(
515 compiled.params,
516 vec![
517 SqlValue::Bool(true),
518 SqlValue::String("%@example.com".to_string()),
519 ]
520 );
521 }
522}