From 70974274a9e8738a206134c9a279185c9f47d7fc Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Wed, 20 Aug 2025 21:19:34 -0700 Subject: [PATCH 1/5] create SharingGRDBTestSupport with assertQuery from StructuredQueries --- Package.swift | 16 + .../SharingGRDBTestSupport/AssertQuery.swift | 416 ++++++++++++++++++ Tests/SharingGRDBTests/AssertQueryTests.swift | 126 ++++++ 3 files changed, 558 insertions(+) create mode 100644 Sources/SharingGRDBTestSupport/AssertQuery.swift create mode 100644 Tests/SharingGRDBTests/AssertQueryTests.swift diff --git a/Package.swift b/Package.swift index 33ddf08c..d389be9e 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,10 @@ let package = Package( name: "SharingGRDBCore", targets: ["SharingGRDBCore"] ), + .library( + name: "SharingGRDBTestSupport", + targets: ["SharingGRDBTestSupport"] + ), .library( name: "StructuredQueriesGRDB", targets: ["StructuredQueriesGRDB"] @@ -29,10 +33,12 @@ let package = Package( ), ], dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), .package(url: "https://github.com/groue/GRDB.swift", from: "7.4.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"), .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.13.0"), ], targets: [ @@ -55,10 +61,20 @@ let package = Package( name: "SharingGRDBTests", dependencies: [ "SharingGRDB", + "SharingGRDBTestSupport", .product(name: "DependenciesTestSupport", package: "swift-dependencies"), .product(name: "StructuredQueries", package: "swift-structured-queries"), ] ), + .target( + name: "SharingGRDBTestSupport", + dependencies: [ + "SharingGRDB", + .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), + .product(name: "StructuredQueriesTestSupport", package: "swift-structured-queries"), + ] + ), .target( name: "StructuredQueriesGRDBCore", dependencies: [ diff --git a/Sources/SharingGRDBTestSupport/AssertQuery.swift b/Sources/SharingGRDBTestSupport/AssertQuery.swift new file mode 100644 index 00000000..e3630d60 --- /dev/null +++ b/Sources/SharingGRDBTestSupport/AssertQuery.swift @@ -0,0 +1,416 @@ +import CustomDump +import Foundation +import InlineSnapshotTesting +import StructuredQueriesCore +import StructuredQueriesTestSupport + +/// An end-to-end snapshot testing helper for statements. +/// +/// This helper can be used to generate snapshots of both the given query and the results of the +/// query decoded back into Swift. +/// +/// ```swift +/// assertQuery( +/// Reminder.select(\.title).order(by: \.title) +/// ) { +/// try db.execute($0) +/// } sql: { +/// """ +/// SELECT "reminders"."title" FROM "reminders" +/// ORDER BY "reminders"."title" +/// """ +/// } results: { +/// """ +/// ┌────────────────────────────┐ +/// │ "Buy concert tickets" │ +/// │ "Call accountant" │ +/// │ "Doctor appointment" │ +/// │ "Get laundry" │ +/// │ "Groceries" │ +/// │ "Haircut" │ +/// │ "Pick up kids from school" │ +/// │ "Send weekly emails" │ +/// │ "Take a walk" │ +/// │ "Take out trash" │ +/// └────────────────────────────┘ +/// """ +/// } +/// ``` +/// +/// - Parameters: +/// - query: A statement. +/// - execute: A closure responsible for executing the query and returning the results. +/// - sql: A snapshot of the SQL produced by the statement. +/// - results: A snapshot of the results. +/// - snapshotTrailingClosureOffset: The trailing closure offset of the `sql` snapshot. Defaults +/// to `1` for invoking this helper directly, but if you write a wrapper function that automates +/// the `execute` trailing closure, you should pass `0` instead. +/// - assertSql: Whether to snapshot the SQL fragment. Defaults to true, but you may prefer false +/// if you write a wrapper function for other purposes. +/// - fileID: The source `#fileID` associated with the assertion. +/// - filePath: The source `#filePath` associated with the assertion. +/// - function: The source `#function` associated with the assertion +/// - line: The source `#line` associated with the assertion. +/// - column: The source `#column` associated with the assertion. +@_disfavoredOverload +public func assertQuery>( + _ query: S, + execute: (S) throws -> [(repeat (each V).QueryOutput)], + sql: (() -> String)? = nil, + results: (() -> String)? = nil, + snapshotTrailingClosureOffset: Int = 1, + assertSql: Bool = true, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line, + column: UInt = #column +) { + if assertSql { + assertInlineSnapshot( + of: query, + as: .sql, + message: "Query did not match", + syntaxDescriptor: InlineSnapshotSyntaxDescriptor( + trailingClosureLabel: "sql", + trailingClosureOffset: snapshotTrailingClosureOffset + ), + matches: sql, + fileID: fileID, + file: filePath, + function: function, + line: line, + column: column + ) + } + do { + let rows = try execute(query) + var table = "" + printTable(rows, to: &table) + if !table.isEmpty { + assertInlineSnapshot( + of: table, + as: .lines, + message: "Results did not match", + syntaxDescriptor: InlineSnapshotSyntaxDescriptor( + trailingClosureLabel: "results", + trailingClosureOffset: assertSql ? snapshotTrailingClosureOffset + 1 : snapshotTrailingClosureOffset + ), + matches: results, + fileID: fileID, + file: filePath, + function: function, + line: line, + column: column + ) + } else if results != nil { + assertInlineSnapshot( + of: table, + as: .lines, + message: "Results expected to be empty", + syntaxDescriptor: InlineSnapshotSyntaxDescriptor( + trailingClosureLabel: "results", + trailingClosureOffset: assertSql ? snapshotTrailingClosureOffset + 1 : snapshotTrailingClosureOffset + ), + matches: results, + fileID: fileID, + file: filePath, + function: function, + line: line, + column: column + ) + } + } catch { + assertInlineSnapshot( + of: error.localizedDescription, + as: .lines, + message: "Results did not match", + syntaxDescriptor: InlineSnapshotSyntaxDescriptor( + trailingClosureLabel: "results", + trailingClosureOffset: assertSql ? snapshotTrailingClosureOffset + 1 : snapshotTrailingClosureOffset + ), + matches: results, + fileID: fileID, + file: filePath, + function: function, + line: line, + column: column + ) + } +} + +/// An end-to-end snapshot testing helper for statements. +/// +/// This helper can be used to generate snapshots of both the given query and the results of the +/// query decoded back into Swift. +/// +/// ```swift +/// assertQuery( +/// Reminder.select(\.title).order(by: \.title) +/// ) { +/// try db.execute($0) +/// } sql: { +/// """ +/// SELECT "reminders"."title" FROM "reminders" +/// ORDER BY "reminders"."title" +/// """ +/// } results: { +/// """ +/// ┌────────────────────────────┐ +/// │ "Buy concert tickets" │ +/// │ "Call accountant" │ +/// │ "Doctor appointment" │ +/// │ "Get laundry" │ +/// │ "Groceries" │ +/// │ "Haircut" │ +/// │ "Pick up kids from school" │ +/// │ "Send weekly emails" │ +/// │ "Take a walk" │ +/// │ "Take out trash" │ +/// └────────────────────────────┘ +/// """ +/// } +/// ``` +/// +/// - Parameters: +/// - query: A statement. +/// - execute: A closure responsible for executing the query and returning the results. +/// - sql: A snapshot of the SQL produced by the statement. +/// - results: A snapshot of the results. +/// - snapshotTrailingClosureOffset: The trailing closure offset of the `sql` snapshot. Defaults +/// to `1` for invoking this helper directly, but if you write a wrapper function that automates +/// the `execute` trailing closure, you should pass `0` instead. +/// - fileID: The source `#fileID` associated with the assertion. +/// - filePath: The source `#filePath` associated with the assertion. +/// - function: The source `#function` associated with the assertion +/// - line: The source `#line` associated with the assertion. +/// - column: The source `#column` associated with the assertion. +public func assertQuery( + _ query: S, + execute: (Select<(S.From, repeat each J), S.From, (repeat each J)>) throws -> [( + S.From.QueryOutput, repeat (each J).QueryOutput + )], + sql: (() -> String)? = nil, + results: (() -> String)? = nil, + snapshotTrailingClosureOffset: Int = 1, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line, + column: UInt = #column +) where S.QueryValue == (), S.Joins == (repeat each J) { + assertQuery( + query.selectStar(), + execute: execute, + sql: sql, + results: results, + snapshotTrailingClosureOffset: snapshotTrailingClosureOffset, + fileID: fileID, + filePath: filePath, + function: function, + line: line, + column: column + ) +} + +/// A snapshot testing helper for database content. +/// +/// This helper can be used to generate snapshots of results of the query decoded back into Swift. +/// +/// ```swift +/// assertSelect( +/// Reminder.select(\.title).order(by: \.title) +/// ) { +/// try db.execute($0) +/// } results: { +/// """ +/// ┌────────────────────────────┐ +/// │ "Buy concert tickets" │ +/// │ "Call accountant" │ +/// │ "Doctor appointment" │ +/// │ "Get laundry" │ +/// │ "Groceries" │ +/// │ "Haircut" │ +/// │ "Pick up kids from school" │ +/// │ "Send weekly emails" │ +/// │ "Take a walk" │ +/// │ "Take out trash" │ +/// └────────────────────────────┘ +/// """ +/// } +/// ``` +/// +/// - Parameters: +/// - query: A statement. +/// - execute: A closure responsible for executing the query and returning the results. +/// - results: A snapshot of the results. +/// - snapshotTrailingClosureOffset: The trailing closure offset of the `sql` snapshot. Defaults +/// to `1` for invoking this helper directly, but if you write a wrapper function that automates +/// the `execute` trailing closure, you should pass `0` instead. +/// - fileID: The source `#fileID` associated with the assertion. +/// - filePath: The source `#filePath` associated with the assertion. +/// - function: The source `#function` associated with the assertion +/// - line: The source `#line` associated with the assertion. +/// - column: The source `#column` associated with the assertion. +@_disfavoredOverload +public func assertSelect>( + _ query: S, + execute: (S) throws -> [(repeat (each V).QueryOutput)], + results: (() -> String)? = nil, + snapshotTrailingClosureOffset: Int = 1, + assertSql: Bool = true, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line, + column: UInt = #column +) { + assertQuery( + query, + execute: execute, + sql: nil, + results: results, + snapshotTrailingClosureOffset: snapshotTrailingClosureOffset, + assertSql: false, + fileID: fileID, + filePath: filePath, + function: function, + line: line, + column: column + ) +} + +/// A snapshot testing helper for database content. +/// +/// This helper can be used to generate snapshots of results of the query decoded back into Swift. +/// +/// ```swift +/// assertSelect( +/// Reminder.select(\.title).order(by: \.title) +/// ) { +/// try db.execute($0) +/// } results: { +/// """ +/// ┌────────────────────────────┐ +/// │ "Buy concert tickets" │ +/// │ "Call accountant" │ +/// │ "Doctor appointment" │ +/// │ "Get laundry" │ +/// │ "Groceries" │ +/// │ "Haircut" │ +/// │ "Pick up kids from school" │ +/// │ "Send weekly emails" │ +/// │ "Take a walk" │ +/// │ "Take out trash" │ +/// └────────────────────────────┘ +/// """ +/// } +/// ``` +/// +/// - Parameters: +/// - query: A statement. +/// - execute: A closure responsible for executing the query and returning the results. +/// - results: A snapshot of the results. +/// - snapshotTrailingClosureOffset: The trailing closure offset of the `sql` snapshot. Defaults +/// to `1` for invoking this helper directly, but if you write a wrapper function that automates +/// the `execute` trailing closure, you should pass `0` instead. +/// - fileID: The source `#fileID` associated with the assertion. +/// - filePath: The source `#filePath` associated with the assertion. +/// - function: The source `#function` associated with the assertion +/// - line: The source `#line` associated with the assertion. +/// - column: The source `#column` associated with the assertion. +public func assertSelect( + _ query: S, + execute: (Select<(S.From, repeat each J), S.From, (repeat each J)>) throws -> [( + S.From.QueryOutput, repeat (each J).QueryOutput + )], + results: (() -> String)? = nil, + snapshotTrailingClosureOffset: Int = 1, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line, + column: UInt = #column +) where S.QueryValue == (), S.Joins == (repeat each J) { + assertQuery( + query.selectStar(), + execute: execute, + sql: nil, + results: results, + snapshotTrailingClosureOffset: snapshotTrailingClosureOffset, + assertSql: false, + fileID: fileID, + filePath: filePath, + function: function, + line: line, + column: column + ) +} + +private func printTable(_ rows: [(repeat each C)], to output: inout some TextOutputStream) { + var maxColumnSpan: [Int] = [] + var hasMultiLineRows = false + for _ in repeat (each C).self { + maxColumnSpan.append(0) + } + var table: [([[Substring]], maxRowSpan: Int)] = [] + for row in rows { + var columns: [[Substring]] = [] + var index = 0 + var maxRowSpan = 0 + for column in repeat each row { + defer { index += 1 } + var cell = "" + customDump(column, to: &cell) + let lines = cell.split(separator: "\n") + hasMultiLineRows = hasMultiLineRows || lines.count > 1 + maxRowSpan = max(maxRowSpan, lines.count) + maxColumnSpan[index] = max(maxColumnSpan[index], lines.map(\.count).max() ?? 0) + columns.append(lines) + } + table.append((columns, maxRowSpan)) + } + guard !table.isEmpty else { return } + output.write("┌─") + output.write( + maxColumnSpan + .map { String(repeating: "─", count: $0) } + .joined(separator: "─┬─") + ) + output.write("─┐\n") + for (offset, rowAndMaxRowSpan) in table.enumerated() { + let (row, maxRowSpan) = rowAndMaxRowSpan + for rowOffset in 0.. DatabaseQueue { + let database = try DatabaseQueue() + try database.write { db in + try #sql( + """ + CREATE TABLE "records" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "date" INTEGER NOT NULL DEFAULT 42 + ) + """ + ) + .execute(db) + for _ in 1...3 { + _ = try Record.insert { Record.Draft() }.execute(db) + } + } + return database + } +} From d859526bec4d9cfd57b25899a3bab7aeda64a5ab Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Wed, 20 Aug 2025 21:31:37 -0700 Subject: [PATCH 2/5] rework the api to assertQuery(includeSQL) --- .../SharingGRDBTestSupport/AssertQuery.swift | 164 ++---------------- Tests/SharingGRDBTests/AssertQueryTests.swift | 39 +++-- 2 files changed, 32 insertions(+), 171 deletions(-) diff --git a/Sources/SharingGRDBTestSupport/AssertQuery.swift b/Sources/SharingGRDBTestSupport/AssertQuery.swift index e3630d60..47573032 100644 --- a/Sources/SharingGRDBTestSupport/AssertQuery.swift +++ b/Sources/SharingGRDBTestSupport/AssertQuery.swift @@ -4,7 +4,7 @@ import InlineSnapshotTesting import StructuredQueriesCore import StructuredQueriesTestSupport -/// An end-to-end snapshot testing helper for statements. +/// An end-to-end snapshot testing helper for database content. /// /// This helper can be used to generate snapshots of both the given query and the results of the /// query decoded back into Swift. @@ -14,11 +14,6 @@ import StructuredQueriesTestSupport /// Reminder.select(\.title).order(by: \.title) /// ) { /// try db.execute($0) -/// } sql: { -/// """ -/// SELECT "reminders"."title" FROM "reminders" -/// ORDER BY "reminders"."title" -/// """ /// } results: { /// """ /// ┌────────────────────────────┐ @@ -38,6 +33,7 @@ import StructuredQueriesTestSupport /// ``` /// /// - Parameters: +/// - includeSQL: Whether to snapshot the SQL fragment in addition to the results. /// - query: A statement. /// - execute: A closure responsible for executing the query and returning the results. /// - sql: A snapshot of the SQL produced by the statement. @@ -45,8 +41,6 @@ import StructuredQueriesTestSupport /// - snapshotTrailingClosureOffset: The trailing closure offset of the `sql` snapshot. Defaults /// to `1` for invoking this helper directly, but if you write a wrapper function that automates /// the `execute` trailing closure, you should pass `0` instead. -/// - assertSql: Whether to snapshot the SQL fragment. Defaults to true, but you may prefer false -/// if you write a wrapper function for other purposes. /// - fileID: The source `#fileID` associated with the assertion. /// - filePath: The source `#filePath` associated with the assertion. /// - function: The source `#function` associated with the assertion @@ -54,19 +48,19 @@ import StructuredQueriesTestSupport /// - column: The source `#column` associated with the assertion. @_disfavoredOverload public func assertQuery>( + includeSQL: Bool = false, _ query: S, execute: (S) throws -> [(repeat (each V).QueryOutput)], sql: (() -> String)? = nil, results: (() -> String)? = nil, snapshotTrailingClosureOffset: Int = 1, - assertSql: Bool = true, fileID: StaticString = #fileID, filePath: StaticString = #filePath, function: StaticString = #function, line: UInt = #line, column: UInt = #column ) { - if assertSql { + if includeSQL { assertInlineSnapshot( of: query, as: .sql, @@ -94,7 +88,7 @@ public func assertQuery( + includeSQL: Bool = false, _ query: S, execute: (Select<(S.From, repeat each J), S.From, (repeat each J)>) throws -> [( S.From.QueryOutput, repeat (each J).QueryOutput @@ -200,6 +191,7 @@ public func assertQuery( column: UInt = #column ) where S.QueryValue == (), S.Joins == (repeat each J) { assertQuery( + includeSQL: includeSQL, query.selectStar(), execute: execute, sql: sql, @@ -213,140 +205,6 @@ public func assertQuery( ) } -/// A snapshot testing helper for database content. -/// -/// This helper can be used to generate snapshots of results of the query decoded back into Swift. -/// -/// ```swift -/// assertSelect( -/// Reminder.select(\.title).order(by: \.title) -/// ) { -/// try db.execute($0) -/// } results: { -/// """ -/// ┌────────────────────────────┐ -/// │ "Buy concert tickets" │ -/// │ "Call accountant" │ -/// │ "Doctor appointment" │ -/// │ "Get laundry" │ -/// │ "Groceries" │ -/// │ "Haircut" │ -/// │ "Pick up kids from school" │ -/// │ "Send weekly emails" │ -/// │ "Take a walk" │ -/// │ "Take out trash" │ -/// └────────────────────────────┘ -/// """ -/// } -/// ``` -/// -/// - Parameters: -/// - query: A statement. -/// - execute: A closure responsible for executing the query and returning the results. -/// - results: A snapshot of the results. -/// - snapshotTrailingClosureOffset: The trailing closure offset of the `sql` snapshot. Defaults -/// to `1` for invoking this helper directly, but if you write a wrapper function that automates -/// the `execute` trailing closure, you should pass `0` instead. -/// - fileID: The source `#fileID` associated with the assertion. -/// - filePath: The source `#filePath` associated with the assertion. -/// - function: The source `#function` associated with the assertion -/// - line: The source `#line` associated with the assertion. -/// - column: The source `#column` associated with the assertion. -@_disfavoredOverload -public func assertSelect>( - _ query: S, - execute: (S) throws -> [(repeat (each V).QueryOutput)], - results: (() -> String)? = nil, - snapshotTrailingClosureOffset: Int = 1, - assertSql: Bool = true, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - column: UInt = #column -) { - assertQuery( - query, - execute: execute, - sql: nil, - results: results, - snapshotTrailingClosureOffset: snapshotTrailingClosureOffset, - assertSql: false, - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: column - ) -} - -/// A snapshot testing helper for database content. -/// -/// This helper can be used to generate snapshots of results of the query decoded back into Swift. -/// -/// ```swift -/// assertSelect( -/// Reminder.select(\.title).order(by: \.title) -/// ) { -/// try db.execute($0) -/// } results: { -/// """ -/// ┌────────────────────────────┐ -/// │ "Buy concert tickets" │ -/// │ "Call accountant" │ -/// │ "Doctor appointment" │ -/// │ "Get laundry" │ -/// │ "Groceries" │ -/// │ "Haircut" │ -/// │ "Pick up kids from school" │ -/// │ "Send weekly emails" │ -/// │ "Take a walk" │ -/// │ "Take out trash" │ -/// └────────────────────────────┘ -/// """ -/// } -/// ``` -/// -/// - Parameters: -/// - query: A statement. -/// - execute: A closure responsible for executing the query and returning the results. -/// - results: A snapshot of the results. -/// - snapshotTrailingClosureOffset: The trailing closure offset of the `sql` snapshot. Defaults -/// to `1` for invoking this helper directly, but if you write a wrapper function that automates -/// the `execute` trailing closure, you should pass `0` instead. -/// - fileID: The source `#fileID` associated with the assertion. -/// - filePath: The source `#filePath` associated with the assertion. -/// - function: The source `#function` associated with the assertion -/// - line: The source `#line` associated with the assertion. -/// - column: The source `#column` associated with the assertion. -public func assertSelect( - _ query: S, - execute: (Select<(S.From, repeat each J), S.From, (repeat each J)>) throws -> [( - S.From.QueryOutput, repeat (each J).QueryOutput - )], - results: (() -> String)? = nil, - snapshotTrailingClosureOffset: Int = 1, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - function: StaticString = #function, - line: UInt = #line, - column: UInt = #column -) where S.QueryValue == (), S.Joins == (repeat each J) { - assertQuery( - query.selectStar(), - execute: execute, - sql: nil, - results: results, - snapshotTrailingClosureOffset: snapshotTrailingClosureOffset, - assertSql: false, - fileID: fileID, - filePath: filePath, - function: function, - line: line, - column: column - ) -} - private func printTable(_ rows: [(repeat each C)], to output: inout some TextOutputStream) { var maxColumnSpan: [Int] = [] var hasMultiLineRows = false diff --git a/Tests/SharingGRDBTests/AssertQueryTests.swift b/Tests/SharingGRDBTests/AssertQueryTests.swift index a09a41f1..2c3b4736 100644 --- a/Tests/SharingGRDBTests/AssertQueryTests.swift +++ b/Tests/SharingGRDBTests/AssertQueryTests.swift @@ -11,21 +11,17 @@ import Testing @Suite( .dependency(\.defaultDatabase, try .database()), - .snapshots(record: .failed) + .snapshots(record: .failed), + .serialized ) struct AssertQueryTests { @Dependency(\.defaultDatabase) var database - @Test func assertQueryBasicType() throws { + @Test func assertQueryBasic() throws { try database.read { db in assertQuery( Record.all.select(\.id) ) { try $0.fetchAll(db) - } sql: { - """ - SELECT "records"."id" - FROM "records" - """ } results: { """ ┌───┐ @@ -37,18 +33,12 @@ struct AssertQueryTests { } } } - @Test func assertQueryComplexType() throws { + @Test func assertQueryRecord() throws { try database.read { db in assertQuery( Record.where { $0.id == 1 } ) { try $0.fetchAll(db) - } sql: { - """ - SELECT "records"."id", "records"."date" - FROM "records" - WHERE ("records"."id" = 1) - """ } results: { """ ┌────────────────────────────────────────┐ @@ -61,12 +51,18 @@ struct AssertQueryTests { } } } - @Test func assertSelectBasicType() throws { + @Test func assertQueryBasicIncludeSQL() throws { try database.read { db in - assertSelect( + assertQuery( + includeSQL: true, Record.all.select(\.id) ) { try $0.fetchAll(db) + } sql: { + """ + SELECT "records"."id" + FROM "records" + """ } results: { """ ┌───┐ @@ -78,12 +74,19 @@ struct AssertQueryTests { } } } - @Test func assertSelectComplexType() throws { + @Test func assertQueryRecordIncludeSQL() throws { try database.read { db in - assertSelect( + assertQuery( + includeSQL: true, Record.where { $0.id == 1 } ) { try $0.fetchAll(db) + } sql: { + """ + SELECT "records"."id", "records"."date" + FROM "records" + WHERE ("records"."id" = 1) + """ } results: { """ ┌────────────────────────────────────────┐ From ff6d8dc72b9229b8a8811a622facb2acca5c7361 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Wed, 20 Aug 2025 22:05:24 -0700 Subject: [PATCH 3/5] remove execute arg, using the defaultDatabase instead --- .../SharingGRDBTestSupport/AssertQuery.swift | 42 +++--- Tests/SharingGRDBTests/AssertQueryTests.swift | 128 ++++++++---------- 2 files changed, 71 insertions(+), 99 deletions(-) diff --git a/Sources/SharingGRDBTestSupport/AssertQuery.swift b/Sources/SharingGRDBTestSupport/AssertQuery.swift index 47573032..34cc9613 100644 --- a/Sources/SharingGRDBTestSupport/AssertQuery.swift +++ b/Sources/SharingGRDBTestSupport/AssertQuery.swift @@ -1,7 +1,10 @@ import CustomDump +import Dependencies import Foundation +import GRDB import InlineSnapshotTesting import StructuredQueriesCore +import StructuredQueriesGRDBCore import StructuredQueriesTestSupport /// An end-to-end snapshot testing helper for database content. @@ -12,8 +15,6 @@ import StructuredQueriesTestSupport /// ```swift /// assertQuery( /// Reminder.select(\.title).order(by: \.title) -/// ) { -/// try db.execute($0) /// } results: { /// """ /// ┌────────────────────────────┐ @@ -35,10 +36,8 @@ import StructuredQueriesTestSupport /// - Parameters: /// - includeSQL: Whether to snapshot the SQL fragment in addition to the results. /// - query: A statement. -/// - execute: A closure responsible for executing the query and returning the results. /// - sql: A snapshot of the SQL produced by the statement. /// - results: A snapshot of the results. -/// - snapshotTrailingClosureOffset: The trailing closure offset of the `sql` snapshot. Defaults /// to `1` for invoking this helper directly, but if you write a wrapper function that automates /// the `execute` trailing closure, you should pass `0` instead. /// - fileID: The source `#fileID` associated with the assertion. @@ -46,14 +45,13 @@ import StructuredQueriesTestSupport /// - function: The source `#function` associated with the assertion /// - line: The source `#line` associated with the assertion. /// - column: The source `#column` associated with the assertion. +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @_disfavoredOverload -public func assertQuery>( +public func assertQuery>( includeSQL: Bool = false, _ query: S, - execute: (S) throws -> [(repeat (each V).QueryOutput)], sql: (() -> String)? = nil, results: (() -> String)? = nil, - snapshotTrailingClosureOffset: Int = 1, fileID: StaticString = #fileID, filePath: StaticString = #filePath, function: StaticString = #function, @@ -67,7 +65,7 @@ public func assertQuery( +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +public func assertQuery( includeSQL: Bool = false, _ query: S, - execute: (Select<(S.From, repeat each J), S.From, (repeat each J)>) throws -> [( - S.From.QueryOutput, repeat (each J).QueryOutput - )], sql: (() -> String)? = nil, results: (() -> String)? = nil, - snapshotTrailingClosureOffset: Int = 1, fileID: StaticString = #fileID, filePath: StaticString = #filePath, function: StaticString = #function, @@ -193,10 +185,8 @@ public func assertQuery( assertQuery( includeSQL: includeSQL, query.selectStar(), - execute: execute, sql: sql, results: results, - snapshotTrailingClosureOffset: snapshotTrailingClosureOffset, fileID: fileID, filePath: filePath, function: function, diff --git a/Tests/SharingGRDBTests/AssertQueryTests.swift b/Tests/SharingGRDBTests/AssertQueryTests.swift index 2c3b4736..0b414769 100644 --- a/Tests/SharingGRDBTests/AssertQueryTests.swift +++ b/Tests/SharingGRDBTests/AssertQueryTests.swift @@ -12,91 +12,73 @@ import Testing @Suite( .dependency(\.defaultDatabase, try .database()), .snapshots(record: .failed), - .serialized ) struct AssertQueryTests { - @Dependency(\.defaultDatabase) var database @Test func assertQueryBasic() throws { - try database.read { db in - assertQuery( - Record.all.select(\.id) - ) { - try $0.fetchAll(db) - } results: { - """ - ┌───┐ - │ 1 │ - │ 2 │ - │ 3 │ - └───┘ - """ - } + assertQuery( + Record.all.select(\.id) + ) { + """ + ┌───┐ + │ 1 │ + │ 2 │ + │ 3 │ + └───┘ + """ } } @Test func assertQueryRecord() throws { - try database.read { db in - assertQuery( - Record.where { $0.id == 1 } - ) { - try $0.fetchAll(db) - } results: { - """ - ┌────────────────────────────────────────┐ - │ Record( │ - │ id: 1, │ - │ date: Date(1970-01-01T00:00:42.000Z) │ - │ ) │ - └────────────────────────────────────────┘ - """ - } + assertQuery( + Record.where { $0.id == 1 } + ) { + """ + ┌────────────────────────────────────────┐ + │ Record( │ + │ id: 1, │ + │ date: Date(1970-01-01T00:00:42.000Z) │ + │ ) │ + └────────────────────────────────────────┘ + """ } } @Test func assertQueryBasicIncludeSQL() throws { - try database.read { db in - assertQuery( - includeSQL: true, - Record.all.select(\.id) - ) { - try $0.fetchAll(db) - } sql: { - """ - SELECT "records"."id" - FROM "records" - """ - } results: { - """ - ┌───┐ - │ 1 │ - │ 2 │ - │ 3 │ - └───┘ - """ - } + assertQuery( + includeSQL: true, + Record.all.select(\.id) + ) { + """ + SELECT "records"."id" + FROM "records" + """ + } results: { + """ + ┌───┐ + │ 1 │ + │ 2 │ + │ 3 │ + └───┘ + """ } } @Test func assertQueryRecordIncludeSQL() throws { - try database.read { db in - assertQuery( - includeSQL: true, - Record.where { $0.id == 1 } - ) { - try $0.fetchAll(db) - } sql: { - """ - SELECT "records"."id", "records"."date" - FROM "records" - WHERE ("records"."id" = 1) - """ - } results: { - """ - ┌────────────────────────────────────────┐ - │ Record( │ - │ id: 1, │ - │ date: Date(1970-01-01T00:00:42.000Z) │ - │ ) │ - └────────────────────────────────────────┘ - """ - } + assertQuery( + includeSQL: true, + Record.where { $0.id == 1 } + ) { + """ + SELECT "records"."id", "records"."date" + FROM "records" + WHERE ("records"."id" = 1) + """ + } results: { + """ + ┌────────────────────────────────────────┐ + │ Record( │ + │ id: 1, │ + │ date: Date(1970-01-01T00:00:42.000Z) │ + │ ) │ + └────────────────────────────────────────┘ + """ } } } From 53ca71339175d997d632a6773bdb98eb838d9d80 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Thu, 21 Aug 2025 14:31:26 -0700 Subject: [PATCH 4/5] add database parameter, defaulting to dependency --- Sources/SharingGRDBTestSupport/AssertQuery.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/SharingGRDBTestSupport/AssertQuery.swift b/Sources/SharingGRDBTestSupport/AssertQuery.swift index 34cc9613..9ec1b2b8 100644 --- a/Sources/SharingGRDBTestSupport/AssertQuery.swift +++ b/Sources/SharingGRDBTestSupport/AssertQuery.swift @@ -36,6 +36,8 @@ import StructuredQueriesTestSupport /// - Parameters: /// - includeSQL: Whether to snapshot the SQL fragment in addition to the results. /// - query: A statement. +/// - database: The database to read from. A value of `nil` will use +/// `@Dependency(\.defaultDatabase)`. /// - sql: A snapshot of the SQL produced by the statement. /// - results: A snapshot of the results. /// to `1` for invoking this helper directly, but if you write a wrapper function that automates @@ -50,6 +52,7 @@ import StructuredQueriesTestSupport public func assertQuery>( includeSQL: Bool = false, _ query: S, + database: (any DatabaseReader)? = nil, sql: (() -> String)? = nil, results: (() -> String)? = nil, fileID: StaticString = #fileID, @@ -76,8 +79,8 @@ public func assertQuery( includeSQL: Bool = false, _ query: S, + database: (any DatabaseReader)? = nil, sql: (() -> String)? = nil, results: (() -> String)? = nil, fileID: StaticString = #fileID, @@ -185,6 +191,7 @@ public func assertQuery assertQuery( includeSQL: includeSQL, query.selectStar(), + database: database, sql: sql, results: results, fileID: fileID, From 13da8985b79dd649fe965c9052dcd28a4fed67a5 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Tue, 26 Aug 2025 10:31:14 -0700 Subject: [PATCH 5/5] wrap sql pretty-printing in DEBUG --- Tests/SharingGRDBTests/AssertQueryTests.swift | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/Tests/SharingGRDBTests/AssertQueryTests.swift b/Tests/SharingGRDBTests/AssertQueryTests.swift index 0b414769..a913e27e 100644 --- a/Tests/SharingGRDBTests/AssertQueryTests.swift +++ b/Tests/SharingGRDBTests/AssertQueryTests.swift @@ -41,46 +41,50 @@ struct AssertQueryTests { """ } } - @Test func assertQueryBasicIncludeSQL() throws { - assertQuery( - includeSQL: true, - Record.all.select(\.id) - ) { - """ - SELECT "records"."id" - FROM "records" - """ - } results: { - """ - ┌───┐ - │ 1 │ - │ 2 │ - │ 3 │ - └───┘ - """ + #if DEBUG + @Test func assertQueryBasicIncludeSQL() throws { + assertQuery( + includeSQL: true, + Record.all.select(\.id) + ) { + """ + SELECT "records"."id" + FROM "records" + """ + } results: { + """ + ┌───┐ + │ 1 │ + │ 2 │ + │ 3 │ + └───┘ + """ + } } - } - @Test func assertQueryRecordIncludeSQL() throws { - assertQuery( - includeSQL: true, - Record.where { $0.id == 1 } - ) { - """ - SELECT "records"."id", "records"."date" - FROM "records" - WHERE ("records"."id" = 1) - """ - } results: { - """ - ┌────────────────────────────────────────┐ - │ Record( │ - │ id: 1, │ - │ date: Date(1970-01-01T00:00:42.000Z) │ - │ ) │ - └────────────────────────────────────────┘ - """ + #endif + #if DEBUG + @Test func assertQueryRecordIncludeSQL() throws { + assertQuery( + includeSQL: true, + Record.where { $0.id == 1 } + ) { + """ + SELECT "records"."id", "records"."date" + FROM "records" + WHERE ("records"."id" = 1) + """ + } results: { + """ + ┌────────────────────────────────────────┐ + │ Record( │ + │ id: 1, │ + │ date: Date(1970-01-01T00:00:42.000Z) │ + │ ) │ + └────────────────────────────────────────┘ + """ + } } - } + #endif } @Table