From 42c271d77443bf832ee846f4895b8be097e1cb82 Mon Sep 17 00:00:00 2001 From: Vijay Misal Date: Mon, 10 Aug 2026 21:36:06 +0530 Subject: [PATCH] Wrap range/BETWEEN expressions in parens to fix invalid CHECK SQL The `~=` operator overloads used for Range/ClosedRange/PartialRange pattern matching (e.g. `0...26 ~= column`) build their SQL fragment without wrapping it in parentheses, unlike every other comparison operator (`>`, `<`, `==`, etc.), which all self-wrap via `Operator.infix(...)`. This is harmless standalone (e.g. inside `.filter(...)`), but it produces invalid SQL when used as a column-level CHECK constraint via `table.create { t in t.column(col, check: 0...26 ~= col) }`, because Schema.swift's per-column `definition()` relies on the check expression already being parenthesized (it just does `"CHECK" + " " + check`, same as the table-level `check()` builder relies on `.prefix` to add its own parens). Since BETWEEN/range expressions weren't self-wrapped, the generated SQL came out as: CREATE TABLE "t" ("type" INTEGER NOT NULL CHECK "type" BETWEEN 0 AND 26) which SQLite rejects with a syntax error near the CHECK condition, exactly as reported in #1056. Fixes #1056. Fix: wrap the `~=` operators' generated SQL in parens, matching the convention already used by every other comparison operator. Updated the existing BETWEEN/range operator tests to expect the parenthesized output, and added a regression test in SchemaTests that reproduces the exact column-level CHECK scenario from the issue and asserts the generated CREATE TABLE statement is now well-formed. Verified by tracing the SQL-generation code path end-to-end (Operator `infix`/`wrap` helpers, the `~=` overloads, and Schema.swift's `definition()`/`check()`) since a full Swift toolchain wasn't available in this environment to run `swift test` directly; the updated/added unit tests exercise this exact path and should be run by CI. --- Sources/SQLite/Typed/Operators.swift | 20 ++++++------ Tests/SQLiteTests/Schema/SchemaTests.swift | 11 +++++++ Tests/SQLiteTests/Typed/OperatorsTests.swift | 32 ++++++++++---------- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Sources/SQLite/Typed/Operators.swift b/Sources/SQLite/Typed/Operators.swift index 7048b68ea..dbe3aedaa 100644 --- a/Sources/SQLite/Typed/Operators.swift +++ b/Sources/SQLite/Typed/Operators.swift @@ -558,45 +558,45 @@ public func <=(lhs: V, rhs: Expression) -> Expression where } public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) + Expression("(\(rhs.template) BETWEEN ? AND ?)", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) } public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) + Expression("(\(rhs.template) BETWEEN ? AND ?)", rhs.bindings + [lhs.lowerBound.datatypeValue, lhs.upperBound.datatypeValue]) } public func ~=(lhs: Range, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) >= ? AND \(rhs.template) < ?", + Expression("(\(rhs.template) >= ? AND \(rhs.template) < ?)", rhs.bindings + [lhs.lowerBound.datatypeValue] + rhs.bindings + [lhs.upperBound.datatypeValue]) } public func ~=(lhs: Range, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) >= ? AND \(rhs.template) < ?", + Expression("(\(rhs.template) >= ? AND \(rhs.template) < ?)", rhs.bindings + [lhs.lowerBound.datatypeValue] + rhs.bindings + [lhs.upperBound.datatypeValue]) } public func ~=(lhs: PartialRangeThrough, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) <= ?", rhs.bindings + [lhs.upperBound.datatypeValue]) + Expression("(\(rhs.template) <= ?)", rhs.bindings + [lhs.upperBound.datatypeValue]) } public func ~=(lhs: PartialRangeThrough, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) <= ?", rhs.bindings + [lhs.upperBound.datatypeValue]) + Expression("(\(rhs.template) <= ?)", rhs.bindings + [lhs.upperBound.datatypeValue]) } public func ~=(lhs: PartialRangeUpTo, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) < ?", rhs.bindings + [lhs.upperBound.datatypeValue]) + Expression("(\(rhs.template) < ?)", rhs.bindings + [lhs.upperBound.datatypeValue]) } public func ~=(lhs: PartialRangeUpTo, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) < ?", rhs.bindings + [lhs.upperBound.datatypeValue]) + Expression("(\(rhs.template) < ?)", rhs.bindings + [lhs.upperBound.datatypeValue]) } public func ~=(lhs: PartialRangeFrom, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) >= ?", rhs.bindings + [lhs.lowerBound.datatypeValue]) + Expression("(\(rhs.template) >= ?)", rhs.bindings + [lhs.lowerBound.datatypeValue]) } public func ~=(lhs: PartialRangeFrom, rhs: Expression) -> Expression where V.Datatype: Comparable & Value { - Expression("\(rhs.template) >= ?", rhs.bindings + [lhs.lowerBound.datatypeValue]) + Expression("(\(rhs.template) >= ?)", rhs.bindings + [lhs.lowerBound.datatypeValue]) } // MARK: - diff --git a/Tests/SQLiteTests/Schema/SchemaTests.swift b/Tests/SQLiteTests/Schema/SchemaTests.swift index 4f4a49d1d..b291d97e1 100644 --- a/Tests/SQLiteTests/Schema/SchemaTests.swift +++ b/Tests/SQLiteTests/Schema/SchemaTests.swift @@ -290,6 +290,17 @@ class SchemaTests: XCTestCase { ) } + // https://github.com/stephencelis/SQLite.swift/issues/1056 + // A column-level CHECK built from a range pattern (BETWEEN) must wrap its + // condition in parentheses, just like every other check condition does, + // otherwise SQLite rejects the generated `CREATE TABLE` statement. + func test_column_withRangeCheck_compilesValidCheckConstraint() { + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" BETWEEN 0 AND 26))", + table.create { t in t.column(int64, check: 0...26 ~= int64) } + ) + } + func test_column_withIntegerExpression_compilesPrimaryKeyAutoincrementColumnDefinitionExpression() { XCTAssertEqual( "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", diff --git a/Tests/SQLiteTests/Typed/OperatorsTests.swift b/Tests/SQLiteTests/Typed/OperatorsTests.swift index 7a5d83dac..f912860a3 100644 --- a/Tests/SQLiteTests/Typed/OperatorsTests.swift +++ b/Tests/SQLiteTests/Typed/OperatorsTests.swift @@ -274,38 +274,38 @@ class OperatorsTests: XCTestCase { } func test_patternMatchingOperator_withComparableCountableClosedRange_buildsBetweenBooleanExpression() { - assertSQL("\"int\" BETWEEN 0 AND 5", 0...5 ~= int) - assertSQL("\"intOptional\" BETWEEN 0 AND 5", 0...5 ~= intOptional) + assertSQL("(\"int\" BETWEEN 0 AND 5)", 0...5 ~= int) + assertSQL("(\"intOptional\" BETWEEN 0 AND 5)", 0...5 ~= intOptional) } func test_patternMatchingOperator_withComparableClosedRange_buildsBetweenBooleanExpression() { - assertSQL("\"double\" BETWEEN 1.2 AND 4.5", 1.2...4.5 ~= double) - assertSQL("\"doubleOptional\" BETWEEN 1.2 AND 4.5", 1.2...4.5 ~= doubleOptional) + assertSQL("(\"double\" BETWEEN 1.2 AND 4.5)", 1.2...4.5 ~= double) + assertSQL("(\"doubleOptional\" BETWEEN 1.2 AND 4.5)", 1.2...4.5 ~= doubleOptional) } func test_patternMatchingOperator_withComparableRange_buildsBooleanExpression() { - assertSQL("\"double\" >= 1.2 AND \"double\" < 4.5", 1.2..<4.5 ~= double) - assertSQL("\"doubleOptional\" >= 1.2 AND \"doubleOptional\" < 4.5", 1.2..<4.5 ~= doubleOptional) + assertSQL("(\"double\" >= 1.2 AND \"double\" < 4.5)", 1.2..<4.5 ~= double) + assertSQL("(\"doubleOptional\" >= 1.2 AND \"doubleOptional\" < 4.5)", 1.2..<4.5 ~= doubleOptional) } func test_patternMatchingOperator_withComparablePartialRangeThrough_buildsBooleanExpression() { - assertSQL("\"double\" <= 4.5", ...4.5 ~= double) - assertSQL("\"doubleOptional\" <= 4.5", ...4.5 ~= doubleOptional) + assertSQL("(\"double\" <= 4.5)", ...4.5 ~= double) + assertSQL("(\"doubleOptional\" <= 4.5)", ...4.5 ~= doubleOptional) } func test_patternMatchingOperator_withComparablePartialRangeUpTo_buildsBooleanExpression() { - assertSQL("\"double\" < 4.5", ..<4.5 ~= double) - assertSQL("\"doubleOptional\" < 4.5", ..<4.5 ~= doubleOptional) + assertSQL("(\"double\" < 4.5)", ..<4.5 ~= double) + assertSQL("(\"doubleOptional\" < 4.5)", ..<4.5 ~= doubleOptional) } func test_patternMatchingOperator_withComparablePartialRangeFrom_buildsBooleanExpression() { - assertSQL("\"double\" >= 4.5", 4.5... ~= double) - assertSQL("\"doubleOptional\" >= 4.5", 4.5... ~= doubleOptional) + assertSQL("(\"double\" >= 4.5)", 4.5... ~= double) + assertSQL("(\"doubleOptional\" >= 4.5)", 4.5... ~= doubleOptional) } func test_patternMatchingOperator_withComparableClosedRangeString_buildsBetweenBooleanExpression() { - assertSQL("\"string\" BETWEEN 'a' AND 'b'", "a"..."b" ~= string) - assertSQL("\"stringOptional\" BETWEEN 'a' AND 'b'", "a"..."b" ~= stringOptional) + assertSQL("(\"string\" BETWEEN 'a' AND 'b')", "a"..."b" ~= string) + assertSQL("(\"stringOptional\" BETWEEN 'a' AND 'b')", "a"..."b" ~= stringOptional) } func test_doubleAndOperator_withBooleanExpressions_buildsCompoundExpression() { @@ -373,7 +373,7 @@ class OperatorsTests: XCTestCase { let begin = Date(timeIntervalSince1970: 0) let end = Date(timeIntervalSince1970: 5000) assertSQL( - "\"date\" >= '1970-01-01T00:00:00.000' AND \"date\" < '1970-01-01T01:23:20.000'", + "(\"date\" >= '1970-01-01T00:00:00.000' AND \"date\" < '1970-01-01T01:23:20.000')", (begin..