@@ -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+
261361private 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