From c11f4720bb43c70bfe25827410c7704e49f132f5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 16 Sep 2025 17:16:00 -0700 Subject: [PATCH 1/2] Add type-safe API for creating temporary views Like temporary triggers (https://github.com/pointfreeco/swift-structured-queries/pull/82), temporary views can be created in a completely type-safe manner due to their transitive nature. --- .../StructuredQueriesSQLiteCore/Views.swift | 70 +++++++++++++++++ Tests/StructuredQueriesTests/ViewsTests.swift | 76 +++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 Sources/StructuredQueriesSQLiteCore/Views.swift create mode 100644 Tests/StructuredQueriesTests/ViewsTests.swift diff --git a/Sources/StructuredQueriesSQLiteCore/Views.swift b/Sources/StructuredQueriesSQLiteCore/Views.swift new file mode 100644 index 00000000..528b65dd --- /dev/null +++ b/Sources/StructuredQueriesSQLiteCore/Views.swift @@ -0,0 +1,70 @@ +extension Table where Self: _Selection { + /// A `CREATE TEMPORARY VIEW` statement that executes after a database event. + /// + /// See for more information. + /// + /// > Important: A name for the trigger is automatically derived from the arguments if one is not + /// > provided. If you build your own trigger helper that call this function, then your helper + /// > should also take `fileID`, `line` and `column` arguments and pass them to this function. + /// + /// - Parameters: + /// - name: The trigger's name. By default a unique name is generated depending using the table, + /// operation, and source location. + /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE TRIGGER` statement. + /// - operation: The trigger's operation. + /// - fileID: The source `#fileID` associated with the trigger. + /// - line: The source `#line` associated with the trigger. + /// - column: The source `#column` associated with the trigger. + /// - Returns: A temporary trigger. + public static func createTemporaryView( + ifNotExists: Bool = false, + select: () -> Selection + ) -> DatabaseView + where Selection.QueryValue == Columns.QueryValue { + DatabaseView(ifNotExists: ifNotExists, select: select()) + } +} + +public struct DatabaseView: Statement +where Selection.QueryValue == View { + public typealias QueryValue = () + public typealias From = Never + + fileprivate let ifNotExists: Bool + fileprivate let select: Selection + + /// Returns a `DROP VIEW` statement for this trigger. + /// + /// - Parameter ifExists: Adds an `IF EXISTS` condition to the `DROP VIEW`. + /// - Returns: A `DROP VIEW` statement for this trigger. + public func drop(ifExists: Bool = false) -> some Statement<()> { + var query: QueryFragment = "DROP VIEW" + if ifExists { + query.append(" IF EXISTS") + } + query.append(" ") + if let schemaName = View.schemaName { + query.append("\(quote: schemaName).") + } + query.append(View.tableFragment) + return SQLQueryExpression(query) + } + + public var query: QueryFragment { + var query: QueryFragment = "CREATE TEMPORARY VIEW" + if ifNotExists { + query.append(" IF NOT EXISTS") + } + query.append(.newlineOrSpace) + if let schemaName = View.schemaName { + query.append("\(quote: schemaName).") + } + query.append(View.tableFragment) + let columnNames: [QueryFragment] = View.TableColumns.allColumns + .map { "\(quote: $0.name)" } + query.append("\(.newlineOrSpace)(\(columnNames.joined(separator: ", ")))") + query.append("\(.newlineOrSpace)AS") + query.append("\(.newlineOrSpace)\(select)") + return query + } +} diff --git a/Tests/StructuredQueriesTests/ViewsTests.swift b/Tests/StructuredQueriesTests/ViewsTests.swift new file mode 100644 index 00000000..e08f449a --- /dev/null +++ b/Tests/StructuredQueriesTests/ViewsTests.swift @@ -0,0 +1,76 @@ +import Dependencies +import Foundation +import InlineSnapshotTesting +import StructuredQueries +import Testing +import _StructuredQueriesSQLite + +extension SnapshotTests { + @Suite struct ViewsTests { + @Test func basics() { + let query = CompletedReminder.createTemporaryView { + Reminder + .where(\.isCompleted) + .select { CompletedReminder.Columns(reminderID: $0.id, title: $0.title) } + } + assertQuery( + query + ) { + """ + CREATE TEMPORARY VIEW + "completedReminders" + ("reminderID", "title") + AS + SELECT "reminders"."id" AS "reminderID", "reminders"."title" AS "title" + FROM "reminders" + WHERE "reminders"."isCompleted" + """ + } results: { + """ + + """ + } + assertQuery( + CompletedReminder.limit(2) + ) { + """ + SELECT "completedReminders"."reminderID", "completedReminders"."title" + FROM "completedReminders" + LIMIT 2 + """ + } results: { + """ + ┌────────────────────────┐ + │ CompletedReminder( │ + │ reminderID: 4, │ + │ title: "Take a walk" │ + │ ) │ + ├────────────────────────┤ + │ CompletedReminder( │ + │ reminderID: 7, │ + │ title: "Get laundry" │ + │ ) │ + └────────────────────────┘ + """ + } + assertQuery( + query.drop() + ) { + """ + DROP VIEW + "completedReminders" + """ + } + } + } +} + +@Table @Selection +private struct CompletedReminder { + let reminderID: Reminder.ID + let title: String +} + +extension Table where Self: _Selection { + static func foo() {} +} From ba235ba873d3bafbcd17a97ad27b9c9cc3b5f275 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 16 Sep 2025 17:53:21 -0700 Subject: [PATCH 2/2] wip --- .../Documentation.docc/Articles/Views.md | 19 ++++++++++++ .../StructuredQueriesSQLiteCore.md | 1 + .../StructuredQueriesSQLiteCore/Views.swift | 30 ++++++++----------- Tests/StructuredQueriesTests/ViewsTests.swift | 9 +++--- 4 files changed, 37 insertions(+), 22 deletions(-) create mode 100644 Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md new file mode 100644 index 00000000..b5bedcc8 --- /dev/null +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md @@ -0,0 +1,19 @@ +# Views + +Learn how to create views that can be queried. + +## Overview + +[Views](https://www.sqlite.org/lang_createview.html) are pre-packaged select statements that can +be queried like a table. StructuredQueries comes with tools to create _temporary_ views in a +type-safe and schema-safe fashion. + +## Topics + +### Creating temporary views + +- ``StructuredQueriesCore/Table/createTemporaryView(ifNotExists:as:)`` + +### Views + +- ``TemporaryView`` diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md index 651a332f..145638fc 100644 --- a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md @@ -16,6 +16,7 @@ custom database functions, and more. - - - +- - ### Query representations diff --git a/Sources/StructuredQueriesSQLiteCore/Views.swift b/Sources/StructuredQueriesSQLiteCore/Views.swift index 528b65dd..1bcf85c6 100644 --- a/Sources/StructuredQueriesSQLiteCore/Views.swift +++ b/Sources/StructuredQueriesSQLiteCore/Views.swift @@ -1,31 +1,27 @@ extension Table where Self: _Selection { - /// A `CREATE TEMPORARY VIEW` statement that executes after a database event. + /// A `CREATE TEMPORARY VIEW` statement. /// - /// See for more information. - /// - /// > Important: A name for the trigger is automatically derived from the arguments if one is not - /// > provided. If you build your own trigger helper that call this function, then your helper - /// > should also take `fileID`, `line` and `column` arguments and pass them to this function. + /// See for more information. /// /// - Parameters: - /// - name: The trigger's name. By default a unique name is generated depending using the table, - /// operation, and source location. - /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE TRIGGER` statement. - /// - operation: The trigger's operation. - /// - fileID: The source `#fileID` associated with the trigger. - /// - line: The source `#line` associated with the trigger. - /// - column: The source `#column` associated with the trigger. + /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE VIEW` statement. + /// - select: A statement describing the contents of the view. /// - Returns: A temporary trigger. public static func createTemporaryView( ifNotExists: Bool = false, - select: () -> Selection - ) -> DatabaseView + as select: Selection + ) -> TemporaryView where Selection.QueryValue == Columns.QueryValue { - DatabaseView(ifNotExists: ifNotExists, select: select()) + TemporaryView(ifNotExists: ifNotExists, select: select) } } -public struct DatabaseView: Statement +/// A `CREATE TEMPORARY VIEW` statement. +/// +/// This type of statement is returned from ``Table/createTemporaryView(ifNotExists:as:)``. +/// +/// To learn more, see . +public struct TemporaryView: Statement where Selection.QueryValue == View { public typealias QueryValue = () public typealias From = Never diff --git a/Tests/StructuredQueriesTests/ViewsTests.swift b/Tests/StructuredQueriesTests/ViewsTests.swift index e08f449a..b348bd31 100644 --- a/Tests/StructuredQueriesTests/ViewsTests.swift +++ b/Tests/StructuredQueriesTests/ViewsTests.swift @@ -8,11 +8,11 @@ import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct ViewsTests { @Test func basics() { - let query = CompletedReminder.createTemporaryView { - Reminder + let query = CompletedReminder.createTemporaryView( + as: Reminder .where(\.isCompleted) .select { CompletedReminder.Columns(reminderID: $0.id, title: $0.title) } - } + ) assertQuery( query ) { @@ -57,8 +57,7 @@ extension SnapshotTests { query.drop() ) { """ - DROP VIEW - "completedReminders" + DROP VIEW "completedReminders" """ } }