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 new file mode 100644 index 00000000..1bcf85c6 --- /dev/null +++ b/Sources/StructuredQueriesSQLiteCore/Views.swift @@ -0,0 +1,66 @@ +extension Table where Self: _Selection { + /// A `CREATE TEMPORARY VIEW` statement. + /// + /// See for more information. + /// + /// - Parameters: + /// - 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, + as select: Selection + ) -> TemporaryView + where Selection.QueryValue == Columns.QueryValue { + TemporaryView(ifNotExists: ifNotExists, select: select) + } +} + +/// 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 + + 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..b348bd31 --- /dev/null +++ b/Tests/StructuredQueriesTests/ViewsTests.swift @@ -0,0 +1,75 @@ +import Dependencies +import Foundation +import InlineSnapshotTesting +import StructuredQueries +import Testing +import _StructuredQueriesSQLite + +extension SnapshotTests { + @Suite struct ViewsTests { + @Test func basics() { + let query = CompletedReminder.createTemporaryView( + as: 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() {} +}