From cb85c77b265de27a74f2be1a4a82f74be3e8ca19 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Jul 2025 15:13:43 -0700 Subject: [PATCH 1/3] Add `Table.none` Acts as a "black hole" query, rendering as an empty SQL string and not executed by the database. --- Sources/StructuredQueries/Macros.swift | 1 + .../Internal/Scope.swift | 5 +++ .../Statements/CompoundSelect.swift | 4 +- .../Statements/Delete.swift | 12 ++++++ .../Statements/Select.swift | 41 ++++++++++++++++++- .../Statements/Update.swift | 13 +++++- .../Statements/Where.swift | 24 ++++++++--- Sources/StructuredQueriesCore/Table.swift | 18 ++++---- .../StructuredQueriesMacros/TableMacro.swift | 9 ++-- .../StructuredQueriesTests/DeleteTests.swift | 10 +++++ .../StructuredQueriesTests/SelectTests.swift | 18 ++++++++ Tests/StructuredQueriesTests/TableTests.swift | 4 ++ Tests/StructuredQueriesTests/UnionTests.swift | 20 +++++++++ .../StructuredQueriesTests/UpdateTests.swift | 10 +++++ Tests/StructuredQueriesTests/WhereTests.swift | 4 ++ 15 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 Sources/StructuredQueriesCore/Internal/Scope.swift diff --git a/Sources/StructuredQueries/Macros.swift b/Sources/StructuredQueries/Macros.swift index 80eac032..843538f9 100644 --- a/Sources/StructuredQueries/Macros.swift +++ b/Sources/StructuredQueries/Macros.swift @@ -10,6 +10,7 @@ import StructuredQueriesCore PartialSelectStatement, PrimaryKeyedTable, names: named(From), + // named(all), named(columns), named(init(_:)), named(init(decoder:)), diff --git a/Sources/StructuredQueriesCore/Internal/Scope.swift b/Sources/StructuredQueriesCore/Internal/Scope.swift new file mode 100644 index 00000000..c4465430 --- /dev/null +++ b/Sources/StructuredQueriesCore/Internal/Scope.swift @@ -0,0 +1,5 @@ +enum Scope { + case unscoped + case `default` + case empty +} diff --git a/Sources/StructuredQueriesCore/Statements/CompoundSelect.swift b/Sources/StructuredQueriesCore/Statements/CompoundSelect.swift index 10abdb65..4f9f8b2b 100644 --- a/Sources/StructuredQueriesCore/Statements/CompoundSelect.swift +++ b/Sources/StructuredQueriesCore/Statements/CompoundSelect.swift @@ -66,6 +66,8 @@ private struct CompoundSelect: PartialSelectStatement { } var query: QueryFragment { - "\(lhs)\(.newlineOrSpace)\(`operator`.indented())\(.newlineOrSpace)\(rhs)" + guard !lhs.isEmpty else { return rhs } + guard !rhs.isEmpty else { return lhs } + return "\(lhs)\(.newlineOrSpace)\(`operator`.indented())\(.newlineOrSpace)\(rhs)" } } diff --git a/Sources/StructuredQueriesCore/Statements/Delete.swift b/Sources/StructuredQueriesCore/Statements/Delete.swift index 4595dbe7..ecc2fe24 100644 --- a/Sources/StructuredQueriesCore/Statements/Delete.swift +++ b/Sources/StructuredQueriesCore/Statements/Delete.swift @@ -38,6 +38,7 @@ extension PrimaryKeyedTable { public struct Delete { var `where`: [QueryFragment] = [] var returning: [QueryFragment] = [] + var isEmpty = false /// Adds a condition to a delete statement. /// @@ -130,6 +131,16 @@ public struct Delete { returning: returning ) } + + public var unscoped: Delete { + From.unscoped.delete() + } + + public var none: Self { + var delete = self + delete.isEmpty = true + return delete + } } /// A convenience type alias for a non-`RETURNING ``Delete``. @@ -139,6 +150,7 @@ extension Delete: Statement { public typealias QueryValue = Returning public var query: QueryFragment { + guard !isEmpty else { return "" } var query: QueryFragment = "DELETE FROM " if let schemaName = From.schemaName { query.append("\(quote: schemaName).") diff --git a/Sources/StructuredQueriesCore/Statements/Select.swift b/Sources/StructuredQueriesCore/Statements/Select.swift index 2a356537..818114c3 100644 --- a/Sources/StructuredQueriesCore/Statements/Select.swift +++ b/Sources/StructuredQueriesCore/Statements/Select.swift @@ -297,6 +297,7 @@ extension Table { } public struct _SelectClauses: Sendable { + var isEmpty = false var distinct = false var columns: [any QueryExpression] = [] var joins: [_JoinClause] = [] @@ -320,6 +321,11 @@ public struct Select { // NB: A parameter pack compiler crash forces us to heap-allocate this storage. @CopyOnWrite var clauses = _SelectClauses() + fileprivate var isEmpty: Bool { + get { clauses.isEmpty } + set { clauses.isEmpty = newValue } + _modify { yield &clauses.isEmpty } + } fileprivate var distinct: Bool { get { clauses.distinct } set { clauses.distinct = newValue } @@ -362,6 +368,7 @@ public struct Select { } fileprivate init( + isEmpty: Bool, distinct: Bool, columns: [any QueryExpression], joins: [_JoinClause], @@ -371,6 +378,7 @@ public struct Select { order: [QueryFragment], limit: _LimitClause? ) { + self.isEmpty = isEmpty self.columns = columns self.distinct = distinct self.joins = joins @@ -387,7 +395,8 @@ public struct Select { } extension Select { - init(where: [QueryFragment] = []) { + init(isEmpty: Bool = false, where: [QueryFragment] = []) { + self.isEmpty = isEmpty self.where = `where` } @@ -558,6 +567,7 @@ extension Select { Joins == (repeat each J) { Select<(repeat each C1, repeat (each C2).QueryValue), From, (repeat each J)>( + isEmpty: isEmpty, distinct: distinct, columns: columns + Array(repeat each selection((From.columns, repeat (each J).columns))), joins: joins, @@ -612,6 +622,7 @@ extension Select { ) ) return Select<(repeat each C1, repeat each C2), From, (repeat each J1, F, repeat each J2)>( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -651,6 +662,7 @@ extension Select { ) ) return Select<(repeat each C1, repeat each C2), From, (repeat each J, F)>( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -686,6 +698,7 @@ extension Select { ) ) return Select( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -738,6 +751,7 @@ extension Select { From, (repeat each J1, F._Optionalized, repeat (each J2)._Optionalized) >( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -785,6 +799,7 @@ extension Select { From, (repeat each J, F._Optionalized) >( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -822,6 +837,7 @@ extension Select { ) ) return Select( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -874,6 +890,7 @@ extension Select { From._Optionalized, (repeat (each J1)._Optionalized, F, repeat each J2) >( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -921,6 +938,7 @@ extension Select { From._Optionalized, (repeat (each J)._Optionalized, F) >( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -958,6 +976,7 @@ extension Select { ) ) return Select( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -1010,6 +1029,7 @@ extension Select { From._Optionalized, (repeat (each J1)._Optionalized, F._Optionalized, repeat (each J2)._Optionalized) >( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -1057,6 +1077,7 @@ extension Select { From._Optionalized, (repeat (each J)._Optionalized, F._Optionalized) >( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -1094,6 +1115,7 @@ extension Select { ) ) return Select( + isEmpty: isEmpty || other.isEmpty, distinct: distinct || other.distinct, columns: columns + other.columns, joins: joins + [join] + other.joins, @@ -1348,6 +1370,7 @@ extension Select { SQLQueryExpression(iterator.next()!.queryFragment) } return Select<(repeat (each C2).QueryValue), From, Joins>( + isEmpty: isEmpty, distinct: distinct, columns: Array(repeat each transform(repeat { _ in next() }((each C1).self))), joins: joins, @@ -1358,6 +1381,20 @@ extension Select { limit: limit ) } + + public var unscoped: Where { + From.unscoped + } + + public var all: Self { + self + } + + public var none: Self { + var select = self + select.isEmpty = true + return select + } } /// Combines two select statements of the same table type together. @@ -1387,6 +1424,7 @@ public func + < return Select< (repeat each C1, repeat each C2), From, (repeat each J1, repeat each J2) >( + isEmpty: lhs.isEmpty || rhs.isEmpty, distinct: lhs.distinct || rhs.distinct, columns: lhs.columns + rhs.columns, joins: lhs.joins + rhs.joins, @@ -1406,6 +1444,7 @@ extension Select: SelectStatement { } public var query: QueryFragment { + guard !isEmpty else { return "" } var query: QueryFragment = "SELECT" let columns = columns.isEmpty diff --git a/Sources/StructuredQueriesCore/Statements/Update.swift b/Sources/StructuredQueriesCore/Statements/Update.swift index 55e93ad5..ad1aa5df 100644 --- a/Sources/StructuredQueriesCore/Statements/Update.swift +++ b/Sources/StructuredQueriesCore/Statements/Update.swift @@ -95,6 +95,7 @@ public struct Update { var updates: Updates var `where`: [QueryFragment] = [] var returning: [QueryFragment] = [] + var isEmpty = false /// Adds a condition to an update statement. /// @@ -186,6 +187,16 @@ public struct Update { returning: returning ) } + + public var unscoped: Delete { + From.unscoped.delete() + } + + public var none: Self { + var delete = self + delete.isEmpty = true + return delete + } } /// A convenience type alias for a non-`RETURNING ``Update``. @@ -195,7 +206,7 @@ extension Update: Statement { public typealias QueryValue = Returning public var query: QueryFragment { - guard !updates.isEmpty + guard !isEmpty, !updates.isEmpty else { return "" } var query: QueryFragment = "UPDATE " diff --git a/Sources/StructuredQueriesCore/Statements/Where.swift b/Sources/StructuredQueriesCore/Statements/Where.swift index 2e1704d0..243192a5 100644 --- a/Sources/StructuredQueriesCore/Statements/Where.swift +++ b/Sources/StructuredQueriesCore/Statements/Where.swift @@ -64,7 +64,7 @@ public struct Where { } var predicates: [QueryFragment] = [] - var unscoped = false + var scope = Scope.default #if compiler(>=6.1) public static subscript(dynamicMember keyPath: KeyPath) -> Self { @@ -94,12 +94,20 @@ extension Where: SelectStatement { public typealias QueryValue = () public func asSelect() -> SelectOf { - (unscoped ? Select() : Select(clauses: From.all._selectClauses)) - .and(self) + let select: SelectOf + switch scope { + case .default: + select = Select(clauses: From.all._selectClauses) + case .empty: + select = Select(isEmpty: true, where: predicates) + case .unscoped: + select = Select() + } + return select.and(self) } public var _selectClauses: _SelectClauses { - _SelectClauses(where: predicates) + _SelectClauses(isEmpty: scope == .empty, where: predicates) } /// A select statement for a column of the filtered table. @@ -470,7 +478,10 @@ extension Where: SelectStatement { /// A delete statement for the filtered table. public func delete() -> DeleteOf { - Delete(where: unscoped ? predicates : From.all._selectClauses.where + predicates) + Delete( + where: scope == .unscoped ? predicates : From.all._selectClauses.where + predicates, + isEmpty: scope == .empty + ) } /// An update statement for the filtered table. @@ -486,7 +497,8 @@ extension Where: SelectStatement { Update( conflictResolution: conflictResolution, updates: Updates(updates), - where: unscoped ? predicates : From.all._selectClauses.where + predicates + where: scope == .unscoped ? predicates : From.all._selectClauses.where + predicates, + isEmpty: scope == .empty ) } diff --git a/Sources/StructuredQueriesCore/Table.swift b/Sources/StructuredQueriesCore/Table.swift index 5cb66ba4..614f1ba0 100644 --- a/Sources/StructuredQueriesCore/Table.swift +++ b/Sources/StructuredQueriesCore/Table.swift @@ -49,12 +49,6 @@ public protocol Table: QueryRepresentable where TableColumns.QueryValue == Self static var all: DefaultScope { get } } -extension Table where DefaultScope == Where { - public static var all: DefaultScope { - Where() - } -} - extension Table { /// A select statement on the table with no constraints. /// @@ -86,7 +80,11 @@ extension Table { /// // SELECT "reminders"."id" FROM "reminders" /// ``` public static var unscoped: Where { - Where(unscoped: true) + Where(scope: .unscoped) + } + + public static var none: Where { + Where(scope: .empty) } public static var tableAlias: String? { @@ -112,3 +110,9 @@ extension Table { columns[keyPath: keyPath] } } + +extension Table where DefaultScope == Where { + public static var all: DefaultScope { + Where() + } +} diff --git a/Sources/StructuredQueriesMacros/TableMacro.swift b/Sources/StructuredQueriesMacros/TableMacro.swift index f0ac0afc..517ed305 100644 --- a/Sources/StructuredQueriesMacros/TableMacro.swift +++ b/Sources/StructuredQueriesMacros/TableMacro.swift @@ -567,7 +567,7 @@ extension TableMacro: ExtensionMacro { return [] } - var typeAliases: [DeclSyntax] = [] + var statics: [DeclSyntax] = [] var letSchemaName: DeclSyntax? if let schemaName { letSchemaName = """ @@ -577,7 +577,7 @@ extension TableMacro: ExtensionMacro { var initDecoder: DeclSyntax? if declaration.hasMacroApplication("Selection") { conformances.append("\(moduleName).PartialSelectStatement") - typeAliases.append(contentsOf: [ + statics.append(contentsOf: [ """ public typealias QueryValue = Self @@ -585,6 +585,9 @@ extension TableMacro: ExtensionMacro { """ public typealias From = Swift.Never """, + // """ + // public static var all: Where { none } + // """, ]) } else { initDecoder = """ @@ -600,7 +603,7 @@ extension TableMacro: ExtensionMacro { """ \(declaration.attributes.availability)extension \(type)\ \(conformances.isEmpty ? "" : ": \(conformances, separator: ", ")") {\ - \(typeAliases, separator: "\n") + \(statics, separator: "\n") public static let columns = TableColumns() public static let tableName = \(tableName)\(letSchemaName)\(initDecoder)\(initFromOther) } diff --git a/Tests/StructuredQueriesTests/DeleteTests.swift b/Tests/StructuredQueriesTests/DeleteTests.swift index 05eddb93..e3f810d2 100644 --- a/Tests/StructuredQueriesTests/DeleteTests.swift +++ b/Tests/StructuredQueriesTests/DeleteTests.swift @@ -159,6 +159,16 @@ extension SnapshotTests { """ } } + + @Test func empty() { + assertQuery( + Reminder.none.delete() + ) { + """ + + """ + } + } } } diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index e1969db0..0503c124 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -959,6 +959,24 @@ extension SnapshotTests { } } + @Test func none() { + #expect(RemindersList.none.query.isEmpty) + #expect(RemindersList.none.select(\.id).query.isEmpty) + #expect(RemindersList.join(Reminder.none) { _, _ in true }.query.isEmpty) + #expect(RemindersList.none.join(Reminder.all) { _, _ in true }.query.isEmpty) + #expect(RemindersList.leftJoin(Reminder.none) { _, _ in true }.query.isEmpty) + #expect(RemindersList.none.leftJoin(Reminder.all) { _, _ in true }.query.isEmpty) + #expect(RemindersList.rightJoin(Reminder.none) { _, _ in true }.query.isEmpty) + #expect(RemindersList.none.rightJoin(Reminder.all) { _, _ in true }.query.isEmpty) + #expect(RemindersList.fullJoin(Reminder.none) { _, _ in true }.query.isEmpty) + #expect(RemindersList.none.fullJoin(Reminder.all) { _, _ in true }.query.isEmpty) + #expect(Reminder.none.where(\.isCompleted).query.isEmpty) + #expect(Reminder.none.group(by: \.id).query.isEmpty) + #expect(Reminder.none.having(\.isCompleted).query.isEmpty) + #expect(Reminder.none.order(by: \.id).query.isEmpty) + #expect(Reminder.none.limit(1).query.isEmpty) + } + #if compiler(>=6.1) @Test func dynamicMember() { assertQuery( diff --git a/Tests/StructuredQueriesTests/TableTests.swift b/Tests/StructuredQueriesTests/TableTests.swift index 311d9a58..4503f03e 100644 --- a/Tests/StructuredQueriesTests/TableTests.swift +++ b/Tests/StructuredQueriesTests/TableTests.swift @@ -586,6 +586,10 @@ extension SnapshotTests { SELECT "rs"."id" FROM "rows" AS "rs" WHERE CAST("rs"."id" AS TEXT) = '"rs"' + """ + } results: { + """ + """ } } diff --git a/Tests/StructuredQueriesTests/UnionTests.swift b/Tests/StructuredQueriesTests/UnionTests.swift index 67681ca3..2fb8a1ed 100644 --- a/Tests/StructuredQueriesTests/UnionTests.swift +++ b/Tests/StructuredQueriesTests/UnionTests.swift @@ -46,6 +46,26 @@ extension SnapshotTests { } } + @Test func empty() { + assertQuery( + Reminder.none.select { ("reminder", $0.title) } + .union(RemindersList.select { ("list", $0.title) }) + .union(Tag.none.select { ("tag", $0.title) }) + ) { + """ + SELECT 'list', "remindersLists"."title" + FROM "remindersLists" + """ + } results: { + """ + ┌────────┬────────────┐ + │ "list" │ "Business" │ + │ "list" │ "Family" │ + │ "list" │ "Personal" │ + └────────┴────────────┘ + """ + } + } @Test func commonTableExpression() { assertQuery( With { diff --git a/Tests/StructuredQueriesTests/UpdateTests.swift b/Tests/StructuredQueriesTests/UpdateTests.swift index 66b35c5d..f16f08d4 100644 --- a/Tests/StructuredQueriesTests/UpdateTests.swift +++ b/Tests/StructuredQueriesTests/UpdateTests.swift @@ -393,6 +393,16 @@ extension SnapshotTests { """ } } + + @Test func empty() { + assertQuery( + Reminder.none.update { $0.isCompleted.toggle() } + ) { + """ + + """ + } + } } } diff --git a/Tests/StructuredQueriesTests/WhereTests.swift b/Tests/StructuredQueriesTests/WhereTests.swift index c413d694..298267b6 100644 --- a/Tests/StructuredQueriesTests/WhereTests.swift +++ b/Tests/StructuredQueriesTests/WhereTests.swift @@ -151,6 +151,10 @@ extension SnapshotTests { FROM "remindersLists" LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") WHERE ("remindersLists"."id" = 4) AND "reminders"."isCompleted" + """ + } results: { + """ + """ } } From b374583bf5482c6040663c9777df81fd34868a5f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Jul 2025 15:28:44 -0700 Subject: [PATCH 2/3] wip --- Sources/StructuredQueries/Macros.swift | 1 - .../Statements/CommonTableExpression.swift | 12 +++- .../Statements/Delete.swift | 8 ++- .../Statements/Update.swift | 6 +- .../Statements/Where.swift | 8 +-- .../StructuredQueriesMacros/TableMacro.swift | 3 - .../CommonTableExpressionTests.swift | 67 +++++++++++++++++++ 7 files changed, 90 insertions(+), 15 deletions(-) diff --git a/Sources/StructuredQueries/Macros.swift b/Sources/StructuredQueries/Macros.swift index 843538f9..80eac032 100644 --- a/Sources/StructuredQueries/Macros.swift +++ b/Sources/StructuredQueries/Macros.swift @@ -10,7 +10,6 @@ import StructuredQueriesCore PartialSelectStatement, PrimaryKeyedTable, names: named(From), - // named(all), named(columns), named(init(_:)), named(init(decoder:)), diff --git a/Sources/StructuredQueriesCore/Statements/CommonTableExpression.swift b/Sources/StructuredQueriesCore/Statements/CommonTableExpression.swift index 6288e503..77f768a3 100644 --- a/Sources/StructuredQueriesCore/Statements/CommonTableExpression.swift +++ b/Sources/StructuredQueriesCore/Statements/CommonTableExpression.swift @@ -29,20 +29,28 @@ public struct With: Statement { } public var query: QueryFragment { + guard !statement.isEmpty else { return "" } + let cteFragments = ctes.compactMap(\.queryFragment.presence) + guard !cteFragments.isEmpty else { return "" } var query: QueryFragment = "WITH " query.append( - "\(ctes.map(\.queryFragment).joined(separator: ", "))\(.newlineOrSpace)\(statement)" + "\(cteFragments.joined(separator: ", "))\(.newlineOrSpace)\(statement)" ) return query } } +extension QueryFragment { + fileprivate var presence: Self? { isEmpty ? nil : self } +} + public struct CommonTableExpressionClause: QueryExpression { public typealias QueryValue = () let tableName: QueryFragment let select: QueryFragment public var queryFragment: QueryFragment { - "\(tableName) AS (\(.newline)\(select.indented())\(.newline))" + guard !select.isEmpty else { return "" } + return "\(tableName) AS (\(.newline)\(select.indented())\(.newline))" } } diff --git a/Sources/StructuredQueriesCore/Statements/Delete.swift b/Sources/StructuredQueriesCore/Statements/Delete.swift index ecc2fe24..6b162e3e 100644 --- a/Sources/StructuredQueriesCore/Statements/Delete.swift +++ b/Sources/StructuredQueriesCore/Statements/Delete.swift @@ -8,7 +8,7 @@ extension Table { /// /// - Returns: A delete statement. public static func delete() -> DeleteOf { - Delete() + Delete(isEmpty: false) } } @@ -23,7 +23,7 @@ extension PrimaryKeyedTable { /// - Parameter row: A row to delete. /// - Returns: A delete statement. public static func delete(_ row: Self) -> DeleteOf { - Delete() + delete() .where { $0.primaryKey.eq(TableColumns.PrimaryKey(queryOutput: row[keyPath: $0.primaryKey.keyPath])) } @@ -36,9 +36,9 @@ extension PrimaryKeyedTable { /// /// To learn more, see . public struct Delete { + var isEmpty: Bool var `where`: [QueryFragment] = [] var returning: [QueryFragment] = [] - var isEmpty = false /// Adds a condition to a delete statement. /// @@ -108,6 +108,7 @@ public struct Delete { returning.append("\(quote: resultColumn.name)") } return Delete( + isEmpty: isEmpty, where: `where`, returning: Array(repeat each selection(From.columns)) ) @@ -127,6 +128,7 @@ public struct Delete { returning.append("\(quote: resultColumn.name)") } return Delete( + isEmpty: isEmpty, where: `where`, returning: returning ) diff --git a/Sources/StructuredQueriesCore/Statements/Update.swift b/Sources/StructuredQueriesCore/Statements/Update.swift index ad1aa5df..9c503d35 100644 --- a/Sources/StructuredQueriesCore/Statements/Update.swift +++ b/Sources/StructuredQueriesCore/Statements/Update.swift @@ -37,7 +37,7 @@ extension Table { or conflictResolution: ConflictResolution? = nil, set updates: (inout Updates) -> Void ) -> UpdateOf { - Update(conflictResolution: conflictResolution, updates: Updates(updates)) + Update(isEmpty: false, conflictResolution: conflictResolution, updates: Updates(updates)) } } @@ -91,11 +91,11 @@ extension PrimaryKeyedTable { /// /// To learn more, see . public struct Update { + var isEmpty: Bool var conflictResolution: ConflictResolution? var updates: Updates var `where`: [QueryFragment] = [] var returning: [QueryFragment] = [] - var isEmpty = false /// Adds a condition to an update statement. /// @@ -160,6 +160,7 @@ public struct Update { returning.append("\(quote: resultColumn.name)") } return Update( + isEmpty: false, conflictResolution: conflictResolution, updates: updates, where: `where`, @@ -181,6 +182,7 @@ public struct Update { returning.append("\(quote: resultColumn.name)") } return Update( + isEmpty: isEmpty, conflictResolution: conflictResolution, updates: updates, where: `where`, diff --git a/Sources/StructuredQueriesCore/Statements/Where.swift b/Sources/StructuredQueriesCore/Statements/Where.swift index 243192a5..4fed412d 100644 --- a/Sources/StructuredQueriesCore/Statements/Where.swift +++ b/Sources/StructuredQueriesCore/Statements/Where.swift @@ -479,8 +479,8 @@ extension Where: SelectStatement { /// A delete statement for the filtered table. public func delete() -> DeleteOf { Delete( - where: scope == .unscoped ? predicates : From.all._selectClauses.where + predicates, - isEmpty: scope == .empty + isEmpty: scope == .empty, + where: scope == .unscoped ? predicates : From.all._selectClauses.where + predicates ) } @@ -495,10 +495,10 @@ extension Where: SelectStatement { set updates: (inout Updates) -> Void ) -> UpdateOf { Update( + isEmpty: scope == .empty, conflictResolution: conflictResolution, updates: Updates(updates), - where: scope == .unscoped ? predicates : From.all._selectClauses.where + predicates, - isEmpty: scope == .empty + where: scope == .unscoped ? predicates : From.all._selectClauses.where + predicates ) } diff --git a/Sources/StructuredQueriesMacros/TableMacro.swift b/Sources/StructuredQueriesMacros/TableMacro.swift index 517ed305..91f72211 100644 --- a/Sources/StructuredQueriesMacros/TableMacro.swift +++ b/Sources/StructuredQueriesMacros/TableMacro.swift @@ -585,9 +585,6 @@ extension TableMacro: ExtensionMacro { """ public typealias From = Swift.Never """, - // """ - // public static var all: Where { none } - // """, ]) } else { initDecoder = """ diff --git a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift index e5f911bb..6a9b11fd 100644 --- a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift +++ b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift @@ -213,6 +213,73 @@ extension SnapshotTests { } } + @Test func empty() { + assertQuery( + With { + Reminder + .where { !$0.isCompleted } + .select { IncompleteReminder.Columns(isFlagged: $0.isFlagged, title: $0.title) } + } query: { + Reminder + .none + .delete() + .returning(\.title) + } + ) { + """ + + """ + } results: { + """ + + """ + } + assertQuery( + With { + Reminder + .none + .where { !$0.isCompleted } + .select { IncompleteReminder.Columns(isFlagged: $0.isFlagged, title: $0.title) } + } query: { + Reminder + .delete() + .returning(\.title) + } + ) { + """ + + """ + } results: { + """ + + """ + } + assertQuery( + With { + Reminder + .none + .where { !$0.isCompleted } + .select { IncompleteReminder.Columns(isFlagged: $0.isFlagged, title: $0.title) } + Reminder + .where { !$0.isCompleted } + .select { IncompleteReminder.Columns(isFlagged: $0.isFlagged, title: $0.title) } + } query: { + Reminder + .none + .delete() + .returning(\.title) + } + ) { + """ + + """ + } results: { + """ + + """ + } + } + @Test func recursive() { assertQuery( With { From 64a48bb3f23717a613f07407458f8b3e17d02c57 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 18 Jul 2025 15:42:58 -0700 Subject: [PATCH 3/3] wip --- Package.resolved | 6 ++--- .../Documentation.docc/Extensions/Select.md | 3 +++ .../Statements/Delete.swift | 10 --------- .../Statements/Select.swift | 3 +++ .../Statements/Update.swift | 10 --------- .../CommonTableExpressionTests.swift | 22 ++++++++++++++++--- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/Package.resolved b/Package.resolved index f59c9a7b..b4d6bb36 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a9cc8edf77ac5412b54a8aed27b41c2fc10588741da62b1886a48257aa83a50f", + "originHash" : "f41a80cbf357606a2e81ebd0e998e4ef355096560757e1e4921daabddb5e2c56", "pins" : [ { "identity" : "combine-schedulers", @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "37230a37e83f1b7023be08e1b1a2603fcb1567fb", - "version" : "1.18.4" + "revision" : "b198a568ad24c5a22995c5ff0ecf9667634e860e", + "version" : "1.18.5" } }, { diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Extensions/Select.md b/Sources/StructuredQueriesCore/Documentation.docc/Extensions/Select.md index 47be63f9..465336fc 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Extensions/Select.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Extensions/Select.md @@ -22,6 +22,9 @@ ### Transforming queries +- ``unscoped`` +- ``all`` +- ``none`` - ``map(_:)`` - ``subscript(dynamicMember:)`` - ``StructuredQueriesCore/+(_:_:)`` diff --git a/Sources/StructuredQueriesCore/Statements/Delete.swift b/Sources/StructuredQueriesCore/Statements/Delete.swift index 6b162e3e..089cbbf0 100644 --- a/Sources/StructuredQueriesCore/Statements/Delete.swift +++ b/Sources/StructuredQueriesCore/Statements/Delete.swift @@ -133,16 +133,6 @@ public struct Delete { returning: returning ) } - - public var unscoped: Delete { - From.unscoped.delete() - } - - public var none: Self { - var delete = self - delete.isEmpty = true - return delete - } } /// A convenience type alias for a non-`RETURNING ``Delete``. diff --git a/Sources/StructuredQueriesCore/Statements/Select.swift b/Sources/StructuredQueriesCore/Statements/Select.swift index 818114c3..1fa301a4 100644 --- a/Sources/StructuredQueriesCore/Statements/Select.swift +++ b/Sources/StructuredQueriesCore/Statements/Select.swift @@ -1382,14 +1382,17 @@ extension Select { ) } + /// Returns a fully unscoped version of this select statement. public var unscoped: Where { From.unscoped } + /// Returns this select statement unchanged. public var all: Self { self } + /// Returns an empty select statement. public var none: Self { var select = self select.isEmpty = true diff --git a/Sources/StructuredQueriesCore/Statements/Update.swift b/Sources/StructuredQueriesCore/Statements/Update.swift index 9c503d35..3e5f2b7c 100644 --- a/Sources/StructuredQueriesCore/Statements/Update.swift +++ b/Sources/StructuredQueriesCore/Statements/Update.swift @@ -189,16 +189,6 @@ public struct Update { returning: returning ) } - - public var unscoped: Delete { - From.unscoped.delete() - } - - public var none: Self { - var delete = self - delete.isEmpty = true - return delete - } } /// A convenience type alias for a non-`RETURNING ``Update``. diff --git a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift index 6a9b11fd..4ec67b1a 100644 --- a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift +++ b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift @@ -265,17 +265,33 @@ extension SnapshotTests { .select { IncompleteReminder.Columns(isFlagged: $0.isFlagged, title: $0.title) } } query: { Reminder - .none .delete() .returning(\.title) } ) { """ - + WITH "incompleteReminders" AS ( + SELECT "reminders"."isFlagged" AS "isFlagged", "reminders"."title" AS "title" + FROM "reminders" + WHERE NOT ("reminders"."isCompleted") + ) + DELETE FROM "reminders" + RETURNING "reminders"."title" """ } results: { """ - + ┌────────────────────────────┐ + │ "Groceries" │ + │ "Haircut" │ + │ "Doctor appointment" │ + │ "Take a walk" │ + │ "Buy concert tickets" │ + │ "Pick up kids from school" │ + │ "Get laundry" │ + │ "Take out trash" │ + │ "Call accountant" │ + │ "Send weekly emails" │ + └────────────────────────────┘ """ } }