Skip to content

Commit 72555c7

Browse files
committed
Translate arithmetic expressions to SQL
1 parent bdf48a5 commit 72555c7

1 file changed

Lines changed: 102 additions & 4 deletions

File tree

‎Sources/CoreModelSQLite/Predicate.swift‎

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,35 @@ internal extension FetchRequest.Predicate.Comparison {
107107
}
108108
}
109109

110+
// `(a <op> b) <operator> constant` comparisons compile to a SQL expression.
111+
if case let .arithmetic(arithmetic) = left {
112+
guard modifier == nil else {
113+
throw SQLiteDatabaseError.invalidPredicate(predicate)
114+
}
115+
let arithmeticFragment = try arithmetic.sqlFragment(for: entity, predicate: predicate)
116+
switch type {
117+
case .lessThan, .lessThanOrEqualTo, .greaterThan, .greaterThanOrEqualTo:
118+
let value = try right.constantBinding(predicate: predicate)
119+
return SQLFragment(
120+
sql: "\(arithmeticFragment.sql) \(type.rawValue) ?",
121+
bindings: arithmeticFragment.bindings + [value]
122+
)
123+
case .equalTo, .notEqualTo:
124+
let value = try right.constantBinding(predicate: predicate)
125+
let sqlOperator = (type == .equalTo) ? "=" : "<>"
126+
guard let value else {
127+
let nullOperator = (type == .equalTo) ? "IS NULL" : "IS NOT NULL"
128+
return SQLFragment(sql: "\(arithmeticFragment.sql) \(nullOperator)", bindings: arithmeticFragment.bindings)
129+
}
130+
return SQLFragment(
131+
sql: "\(arithmeticFragment.sql) \(sqlOperator) ?",
132+
bindings: arithmeticFragment.bindings + [value]
133+
)
134+
default:
135+
throw SQLiteDatabaseError.invalidPredicate(predicate)
136+
}
137+
}
138+
110139
// Only `keyPath <operator> constant` comparisons map directly to columns.
111140
guard case let .keyPath(keyPath) = left else {
112141
throw SQLiteDatabaseError.invalidPredicate(predicate)
@@ -258,6 +287,77 @@ internal extension FetchRequest.Predicate.FunctionExpression {
258287
}
259288
}
260289

290+
private extension FetchRequest.Predicate.ArithmeticExpression {
291+
292+
/// The arithmetic expression as a parenthesized SQL expression.
293+
///
294+
/// SQLite's native operators match CoreModel's in-memory semantics for the cases
295+
/// that matter: `/` truncates when both operands are `INTEGER` (`7 / 2` is `3`),
296+
/// promotes to `REAL` when either operand is, and division or remainder by zero
297+
/// yields SQL `NULL` — which fails every comparison, exactly as the in-memory
298+
/// engine's `nil` does.
299+
///
300+
/// `.modulus` is rejected when a statically-known operand is non-integer:
301+
/// SQLite's `%` casts its operands to `INTEGER`, but the in-memory engine
302+
/// defines remainder for integers only, and a silently cast result would
303+
/// diverge between backends.
304+
///
305+
/// - Note: The only knowing divergence is at the edges of `Int64`: the in-memory
306+
/// engine wraps (`&+`), while SQLite promotes an overflowing `INTEGER` result
307+
/// to a `REAL` approximation.
308+
func sqlFragment(
309+
for entity: EntityDescription,
310+
predicate: FetchRequest.Predicate
311+
) throws -> SQLFragment {
312+
if function == .modulus {
313+
for operand in [left, right] where operand.isKnownNonInteger(for: entity) {
314+
throw SQLiteDatabaseError.invalidPredicate(predicate)
315+
}
316+
}
317+
let leftFragment = try left.argumentSQLFragment(for: entity, predicate: predicate)
318+
let rightFragment = try right.argumentSQLFragment(for: entity, predicate: predicate)
319+
return SQLFragment(
320+
sql: "(" + leftFragment.sql + " " + function.symbol + " " + rightFragment.sql + ")",
321+
bindings: leftFragment.bindings + rightFragment.bindings
322+
)
323+
}
324+
}
325+
326+
private extension FetchRequest.Predicate.Expression {
327+
328+
/// Whether this operand is statically known to be non-integer.
329+
///
330+
/// Used to reject `.modulus` on floating-point operands. `false` means "integer
331+
/// or unknown" — a custom function's result type can't be known here, and SQL
332+
/// `NULL` propagation makes a wrong guess harmless for every case but `%`.
333+
func isKnownNonInteger(for entity: EntityDescription) -> Bool {
334+
switch self {
335+
case let .attribute(value):
336+
switch value {
337+
case .int16, .int32, .int64, .bool:
338+
return false
339+
case .null, .string, .uuid, .url, .data, .date, .float, .double, .decimal, .composite:
340+
return true
341+
}
342+
case let .keyPath(keyPath):
343+
guard let column = entity.attributeColumns.first(where: { $0.name == keyPath.rawValue }) else {
344+
return false
345+
}
346+
switch column.type {
347+
case .int16, .int32, .int64, .bool:
348+
return false
349+
case .string, .uuid, .url, .data, .date, .float, .double, .decimal, .composite:
350+
return true
351+
}
352+
case let .arithmetic(nested):
353+
// integer only if both operands are; division stays integer for integers
354+
return nested.left.isKnownNonInteger(for: entity) || nested.right.isKnownNonInteger(for: entity)
355+
case .function, .relationship:
356+
return false
357+
}
358+
}
359+
}
360+
261361
private extension FetchRequest.Predicate.Expression {
262362

263363
/// The expression as a SQL fragment suitable for use as a function argument
@@ -274,10 +374,8 @@ private extension FetchRequest.Predicate.Expression {
274374
return try function.sqlFragment(for: entity, predicate: predicate)
275375
case .attribute, .relationship:
276376
return SQLFragment(sql: "?", bindings: [try constantBinding(predicate: predicate)])
277-
case .arithmetic:
278-
// - TODO: Translate arithmetic expressions to SQL. Until then they are
279-
// rejected so the caller can fall back to in-memory evaluation.
280-
throw SQLiteDatabaseError.invalidPredicate(predicate)
377+
case let .arithmetic(arithmetic):
378+
return try arithmetic.sqlFragment(for: entity, predicate: predicate)
281379
}
282380
}
283381

0 commit comments

Comments
 (0)