From 0b3fc862abce1b61f75d9cb963ca708386053673 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Wed, 20 Aug 2025 09:04:30 -0700 Subject: [PATCH] add assertSelect helper which is like assertQuery without snapshotting the sql fragment --- .../AssertQuery.swift | 175 ++++++++++++++++-- .../AssertQueryTests.swift | 109 +++++++++++ 2 files changed, 266 insertions(+), 18 deletions(-) create mode 100644 Tests/StructuredQueriesTests/AssertQueryTests.swift diff --git a/Sources/StructuredQueriesTestSupport/AssertQuery.swift b/Sources/StructuredQueriesTestSupport/AssertQuery.swift index 6ff543f0..9c20be64 100644 --- a/Sources/StructuredQueriesTestSupport/AssertQuery.swift +++ b/Sources/StructuredQueriesTestSupport/AssertQuery.swift @@ -44,6 +44,8 @@ import StructuredQueriesCore /// - 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 @@ -56,27 +58,30 @@ public func assertQuery 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 ) { - 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 - ) + 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 = "" @@ -88,7 +93,7 @@ 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/StructuredQueriesTests/AssertQueryTests.swift b/Tests/StructuredQueriesTests/AssertQueryTests.swift new file mode 100644 index 00000000..2fe98c13 --- /dev/null +++ b/Tests/StructuredQueriesTests/AssertQueryTests.swift @@ -0,0 +1,109 @@ +import Dependencies +import StructuredQueries +import StructuredQueriesSQLite +import StructuredQueriesTestSupport +import Testing + +extension SnapshotTests { + @Suite + struct AssertQueryTests { + @Dependency(\.defaultDatabase) var db + @Test func assertQueryBasicType() { + StructuredQueriesTestSupport.assertQuery( + Reminder.all + .select { ($0.id, $0.assignedUserID) } + .limit(3) + .order(by: \.id) + ) { + try db.execute($0) + } sql: { + """ + SELECT "reminders"."id", "reminders"."assignedUserID" + FROM "reminders" + ORDER BY "reminders"."id" + LIMIT 3 + """ + } results: { + """ + ┌───┬─────┐ + │ 1 │ 1 │ + │ 2 │ nil │ + │ 3 │ nil │ + └───┴─────┘ + """ + } + } + @Test func assertQueryComplexType() { + StructuredQueriesTestSupport.assertQuery( + Reminder.where { $0.id == 1 } + ) { + try db.execute($0) + } sql: { + """ + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" + FROM "reminders" + WHERE ("reminders"."id" = 1) + """ + } results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ + """ + } + } + @Test func assertSelectBasicType() { + StructuredQueriesTestSupport.assertSelect( + Reminder.all + .select { ($0.id, $0.assignedUserID) } + .limit(3) + .order(by: \.id) + ) { + try db.execute($0) + } results: { + """ + ┌───┬─────┐ + │ 1 │ 1 │ + │ 2 │ nil │ + │ 3 │ nil │ + └───┴─────┘ + """ + } + } + @Test func assertSelectComplexType() { + StructuredQueriesTestSupport.assertSelect( + Reminder.where { $0.id == 1 } + ) { + try db.execute($0) + } results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ + """ + } + } + } +}