1use crate::error::Result;
41use crate::resolver::columns::TableScope;
42use crate::types::{BinaryOp, ColumnInfo, SqlDataType, SqlExpr, SqlValue, UnaryOp};
43
44use super::declared_type_coerce::coerce_value;
45
46pub fn coerce_read_literal(column: &ColumnInfo, value: SqlValue) -> Result<SqlValue> {
59 if column.is_primary_key || !is_instant(&column.data_type) {
60 return Ok(value);
61 }
62 coerce_value(&column.name, value, &column.data_type)
63}
64
65pub(crate) fn coerce_predicate_literals(expr: &mut SqlExpr, scope: &TableScope) -> Result<()> {
74 match expr {
75 SqlExpr::BinaryOp { left, op, right } => match op {
76 BinaryOp::And | BinaryOp::Or => {
77 coerce_predicate_literals(left, scope)?;
78 coerce_predicate_literals(right, scope)
79 }
80 BinaryOp::Eq
81 | BinaryOp::Ne
82 | BinaryOp::Gt
83 | BinaryOp::Ge
84 | BinaryOp::Lt
85 | BinaryOp::Le => match (left.as_mut(), right.as_mut()) {
86 (SqlExpr::Column { table, name }, SqlExpr::Literal(value))
87 | (SqlExpr::Literal(value), SqlExpr::Column { table, name }) => {
88 coerce_literal(scope, table.as_deref(), name, value)
89 }
90 _ => Ok(()),
93 },
94 BinaryOp::Add
95 | BinaryOp::Sub
96 | BinaryOp::Mul
97 | BinaryOp::Div
98 | BinaryOp::Mod
99 | BinaryOp::Concat => Ok(()),
100 },
101 SqlExpr::UnaryOp {
102 op: UnaryOp::Not,
103 expr,
104 } => coerce_predicate_literals(expr, scope),
105 SqlExpr::Between {
106 expr, low, high, ..
107 } => {
108 let SqlExpr::Column { table, name } = expr.as_ref() else {
109 return Ok(());
110 };
111 for bound in [low.as_mut(), high.as_mut()] {
112 if let SqlExpr::Literal(value) = bound {
113 coerce_literal(scope, table.as_deref(), name, value)?;
114 }
115 }
116 Ok(())
117 }
118 SqlExpr::InList { expr, list, .. } => {
119 let SqlExpr::Column { table, name } = expr.as_ref() else {
120 return Ok(());
121 };
122 for element in list.iter_mut() {
123 if let SqlExpr::Literal(value) = element {
124 coerce_literal(scope, table.as_deref(), name, value)?;
125 }
126 }
127 Ok(())
128 }
129 SqlExpr::UnaryOp {
130 op: UnaryOp::Neg, ..
131 }
132 | SqlExpr::Column { .. }
133 | SqlExpr::Literal(_)
134 | SqlExpr::Function { .. }
135 | SqlExpr::Case { .. }
136 | SqlExpr::Cast { .. }
137 | SqlExpr::Subquery(_)
138 | SqlExpr::Wildcard
139 | SqlExpr::IsNull { .. }
140 | SqlExpr::Like { .. }
141 | SqlExpr::ArrayLiteral(_) => Ok(()),
142 }
143}
144
145fn coerce_literal(
148 scope: &TableScope,
149 table: Option<&str>,
150 name: &str,
151 value: &mut SqlValue,
152) -> Result<()> {
153 let Some(column) = scope.declared_column(table, name) else {
154 return Ok(());
155 };
156 let taken = std::mem::replace(value, SqlValue::Null);
157 *value = coerce_read_literal(column, taken)?;
158 Ok(())
159}
160
161fn is_instant(declared: &SqlDataType) -> bool {
163 match declared {
164 SqlDataType::Timestamp | SqlDataType::Timestamptz => true,
165 SqlDataType::Int64
166 | SqlDataType::Float64
167 | SqlDataType::String
168 | SqlDataType::Bool
169 | SqlDataType::Bytes
170 | SqlDataType::Decimal(_)
171 | SqlDataType::Uuid
172 | SqlDataType::Vector(_)
173 | SqlDataType::Geometry
174 | SqlDataType::Json
175 | SqlDataType::Unknown => false,
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::resolver::columns::ResolvedTable;
183 use crate::types::{CollectionInfo, ColumnInfo, EngineType};
184 use nodedb_types::datetime::NdbDateTime;
185
186 const EARLY_MS: i64 = 1_583_402_400_000;
188
189 fn early() -> NdbDateTime {
190 NdbDateTime::from_micros(EARLY_MS * 1_000)
191 }
192
193 fn column(name: &str, data_type: SqlDataType, is_primary_key: bool) -> ColumnInfo {
194 ColumnInfo {
195 name: name.to_string(),
196 data_type,
197 nullable: true,
198 is_primary_key,
199 default: None,
200 raw_type: None,
201 int_width: None,
202 float_width: None,
203 }
204 }
205
206 fn scope() -> TableScope {
209 let info = CollectionInfo {
210 name: "t".into(),
211 engine: EngineType::DocumentStrict,
212 columns: vec![
213 column("id", SqlDataType::String, true),
214 column("at", SqlDataType::Timestamp, false),
215 column("at_tz", SqlDataType::Timestamptz, false),
216 column("n", SqlDataType::Int64, false),
217 column("pk_at", SqlDataType::Timestamp, true),
218 ],
219 primary_key: Some("id".into()),
220 has_auto_tier: false,
221 indexes: Vec::new(),
222 bitemporal: false,
223 primary: nodedb_types::PrimaryEngine::Document,
224 vector_primary: None,
225 partition_strategy: nodedb_types::PartitionStrategy::CollectionHomed,
226 open_schema: CollectionInfo::open_schema_for(EngineType::DocumentStrict),
227 };
228 TableScope::single(ResolvedTable {
229 name: info.name.clone(),
230 alias: Some("a".into()),
231 info,
232 })
233 .expect("single-relation scope")
234 }
235
236 fn col(table: Option<&str>, name: &str) -> SqlExpr {
237 SqlExpr::Column {
238 table: table.map(str::to_string),
239 name: name.into(),
240 }
241 }
242
243 fn lit(value: SqlValue) -> SqlExpr {
244 SqlExpr::Literal(value)
245 }
246
247 fn cmp(left: SqlExpr, op: BinaryOp, right: SqlExpr) -> SqlExpr {
248 SqlExpr::BinaryOp {
249 left: Box::new(left),
250 op,
251 right: Box::new(right),
252 }
253 }
254
255 fn literal_of(expr: &SqlExpr) -> &SqlValue {
256 match expr {
257 SqlExpr::Literal(v) => v,
258 other => panic!("expected a literal, got {other:?}"),
259 }
260 }
261
262 fn coerced(mut expr: SqlExpr) -> SqlExpr {
263 coerce_predicate_literals(&mut expr, &scope()).expect("predicate coerces");
264 expr
265 }
266
267 #[test]
268 fn column_op_literal_coerces_a_numeric_literal_to_the_declared_instant() {
269 let expr = coerced(cmp(
270 col(None, "at"),
271 BinaryOp::Gt,
272 lit(SqlValue::Int(EARLY_MS)),
273 ));
274 let SqlExpr::BinaryOp { right, .. } = &expr else {
275 panic!("shape preserved");
276 };
277 assert_eq!(literal_of(right), &SqlValue::Timestamp(early()));
278 }
279
280 #[test]
281 fn literal_op_column_is_coerced_in_the_mirrored_orientation() {
282 let expr = coerced(cmp(
283 lit(SqlValue::String("2020-03-05 10:00:00".into())),
284 BinaryOp::Lt,
285 col(Some("a"), "at_tz"),
286 ));
287 let SqlExpr::BinaryOp { left, .. } = &expr else {
288 panic!("shape preserved");
289 };
290 assert_eq!(literal_of(left), &SqlValue::Timestamptz(early()));
291 }
292
293 #[test]
294 fn between_coerces_both_bounds() {
295 let expr = coerced(SqlExpr::Between {
296 expr: Box::new(col(None, "at")),
297 low: Box::new(lit(SqlValue::Int(EARLY_MS))),
298 high: Box::new(lit(SqlValue::String("2020-03-05T11:00:00Z".into()))),
299 negated: false,
300 });
301 let SqlExpr::Between { low, high, .. } = &expr else {
302 panic!("shape preserved");
303 };
304 assert_eq!(literal_of(low), &SqlValue::Timestamp(early()));
305 assert_eq!(
306 literal_of(high),
307 &SqlValue::Timestamp(NdbDateTime::from_micros((EARLY_MS + 3_600_000) * 1_000))
308 );
309 }
310
311 #[test]
312 fn in_list_coerces_every_literal_element() {
313 let expr = coerced(SqlExpr::InList {
314 expr: Box::new(col(None, "at")),
315 list: vec![
316 lit(SqlValue::Int(EARLY_MS)),
317 lit(SqlValue::String("2020-03-05 10:00:00".into())),
318 ],
319 negated: true,
320 });
321 let SqlExpr::InList { list, .. } = &expr else {
322 panic!("shape preserved");
323 };
324 for element in list {
325 assert_eq!(literal_of(element), &SqlValue::Timestamp(early()));
326 }
327 }
328
329 #[test]
330 fn nested_and_or_not_reach_every_predicate() {
331 let expr = coerced(SqlExpr::UnaryOp {
332 op: UnaryOp::Not,
333 expr: Box::new(cmp(
334 cmp(col(None, "at"), BinaryOp::Ge, lit(SqlValue::Int(EARLY_MS))),
335 BinaryOp::Or,
336 cmp(
337 cmp(col(None, "n"), BinaryOp::Eq, lit(SqlValue::Int(1))),
338 BinaryOp::And,
339 cmp(
340 col(None, "at_tz"),
341 BinaryOp::Le,
342 lit(SqlValue::Int(EARLY_MS)),
343 ),
344 ),
345 )),
346 });
347 let SqlExpr::UnaryOp { expr, .. } = &expr else {
348 panic!("shape preserved");
349 };
350 let SqlExpr::BinaryOp { left, right, .. } = expr.as_ref() else {
351 panic!("shape preserved");
352 };
353 let SqlExpr::BinaryOp { right: at_lit, .. } = left.as_ref() else {
354 panic!("shape preserved");
355 };
356 assert_eq!(literal_of(at_lit), &SqlValue::Timestamp(early()));
357 let SqlExpr::BinaryOp {
358 left: n_pred,
359 right: tz_pred,
360 ..
361 } = right.as_ref()
362 else {
363 panic!("shape preserved");
364 };
365 let SqlExpr::BinaryOp { right: n_lit, .. } = n_pred.as_ref() else {
366 panic!("shape preserved");
367 };
368 assert_eq!(literal_of(n_lit), &SqlValue::Int(1), "BIGINT is untouched");
369 let SqlExpr::BinaryOp { right: tz_lit, .. } = tz_pred.as_ref() else {
370 panic!("shape preserved");
371 };
372 assert_eq!(literal_of(tz_lit), &SqlValue::Timestamptz(early()));
373 }
374
375 #[test]
376 fn a_column_the_scope_does_not_declare_is_left_untouched() {
377 let expr = coerced(cmp(
378 col(None, "created"),
379 BinaryOp::Eq,
380 lit(SqlValue::Int(EARLY_MS)),
381 ));
382 let SqlExpr::BinaryOp { right, .. } = &expr else {
383 panic!("shape preserved");
384 };
385 assert_eq!(literal_of(right), &SqlValue::Int(EARLY_MS));
386 }
387
388 #[test]
389 fn a_qualifier_naming_another_relation_is_left_untouched() {
390 let expr = coerced(cmp(
391 col(Some("other"), "at"),
392 BinaryOp::Eq,
393 lit(SqlValue::Int(EARLY_MS)),
394 ));
395 let SqlExpr::BinaryOp { right, .. } = &expr else {
396 panic!("shape preserved");
397 };
398 assert_eq!(literal_of(right), &SqlValue::Int(EARLY_MS));
399 }
400
401 #[test]
402 fn the_primary_key_column_is_exempt() {
403 let expr = coerced(cmp(
404 col(None, "pk_at"),
405 BinaryOp::Eq,
406 lit(SqlValue::Int(EARLY_MS)),
407 ));
408 let SqlExpr::BinaryOp { right, .. } = &expr else {
409 panic!("shape preserved");
410 };
411 assert_eq!(literal_of(right), &SqlValue::Int(EARLY_MS));
412 }
413
414 #[test]
415 fn column_vs_column_and_null_are_left_alone() {
416 let expr = coerced(cmp(
417 cmp(col(None, "at"), BinaryOp::Lt, col(None, "at_tz")),
418 BinaryOp::And,
419 cmp(col(None, "at"), BinaryOp::Eq, lit(SqlValue::Null)),
420 ));
421 let SqlExpr::BinaryOp { right, .. } = &expr else {
422 panic!("shape preserved");
423 };
424 let SqlExpr::BinaryOp {
425 right: null_lit, ..
426 } = right.as_ref()
427 else {
428 panic!("shape preserved");
429 };
430 assert_eq!(literal_of(null_lit), &SqlValue::Null);
431 }
432
433 #[test]
434 fn a_boolean_literal_is_a_typed_error_naming_the_column() {
435 let mut expr = cmp(col(None, "at"), BinaryOp::Eq, lit(SqlValue::Bool(true)));
436 let err = coerce_predicate_literals(&mut expr, &scope())
437 .expect_err("a boolean carries no instant");
438 let detail = err.to_string();
439 assert!(
440 detail.contains("at") && detail.contains("a boolean"),
441 "error must name the column and the kind: {detail}"
442 );
443 }
444
445 #[test]
446 fn unparseable_text_is_a_typed_error_naming_the_column_and_literal() {
447 let mut expr = SqlExpr::InList {
448 expr: Box::new(col(None, "at")),
449 list: vec![lit(SqlValue::String("not a date".into()))],
450 negated: false,
451 };
452 let err = coerce_predicate_literals(&mut expr, &scope())
453 .expect_err("text that spells no instant is refused");
454 let detail = err.to_string();
455 assert!(
456 detail.contains("at") && detail.contains("not a date"),
457 "error must name the column and the literal: {detail}"
458 );
459 }
460}