Skip to content

Commit 54ac51f

Browse files
authored
Add type-safe API for creating temporary views (#172)
* Add type-safe API for creating temporary views Like temporary triggers (#82), temporary views can be created in a completely type-safe manner due to their transitive nature. * wip
1 parent d715ae3 commit 54ac51f

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Views
2+
3+
Learn how to create views that can be queried.
4+
5+
## Overview
6+
7+
[Views](https://www.sqlite.org/lang_createview.html) are pre-packaged select statements that can
8+
be queried like a table. StructuredQueries comes with tools to create _temporary_ views in a
9+
type-safe and schema-safe fashion.
10+
11+
## Topics
12+
13+
### Creating temporary views
14+
15+
- ``StructuredQueriesCore/Table/createTemporaryView(ifNotExists:as:)``
16+
17+
### Views
18+
19+
- ``TemporaryView``

Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ custom database functions, and more.
1616
- <doc:BuiltinFunctions>
1717
- <doc:CustomFunctions>
1818
- <doc:Triggers>
19+
- <doc:Views>
1920
- <doc:FullTextSearch>
2021

2122
### Query representations
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
extension Table where Self: _Selection {
2+
/// A `CREATE TEMPORARY VIEW` statement.
3+
///
4+
/// See <doc:Views> for more information.
5+
///
6+
/// - Parameters:
7+
/// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE VIEW` statement.
8+
/// - select: A statement describing the contents of the view.
9+
/// - Returns: A temporary trigger.
10+
public static func createTemporaryView<Selection: SelectStatement>(
11+
ifNotExists: Bool = false,
12+
as select: Selection
13+
) -> TemporaryView<Self, Selection>
14+
where Selection.QueryValue == Columns.QueryValue {
15+
TemporaryView(ifNotExists: ifNotExists, select: select)
16+
}
17+
}
18+
19+
/// A `CREATE TEMPORARY VIEW` statement.
20+
///
21+
/// This type of statement is returned from ``Table/createTemporaryView(ifNotExists:as:)``.
22+
///
23+
/// To learn more, see <doc:Views>.
24+
public struct TemporaryView<View: Table & _Selection, Selection: SelectStatement>: Statement
25+
where Selection.QueryValue == View {
26+
public typealias QueryValue = ()
27+
public typealias From = Never
28+
29+
fileprivate let ifNotExists: Bool
30+
fileprivate let select: Selection
31+
32+
/// Returns a `DROP VIEW` statement for this trigger.
33+
///
34+
/// - Parameter ifExists: Adds an `IF EXISTS` condition to the `DROP VIEW`.
35+
/// - Returns: A `DROP VIEW` statement for this trigger.
36+
public func drop(ifExists: Bool = false) -> some Statement<()> {
37+
var query: QueryFragment = "DROP VIEW"
38+
if ifExists {
39+
query.append(" IF EXISTS")
40+
}
41+
query.append(" ")
42+
if let schemaName = View.schemaName {
43+
query.append("\(quote: schemaName).")
44+
}
45+
query.append(View.tableFragment)
46+
return SQLQueryExpression(query)
47+
}
48+
49+
public var query: QueryFragment {
50+
var query: QueryFragment = "CREATE TEMPORARY VIEW"
51+
if ifNotExists {
52+
query.append(" IF NOT EXISTS")
53+
}
54+
query.append(.newlineOrSpace)
55+
if let schemaName = View.schemaName {
56+
query.append("\(quote: schemaName).")
57+
}
58+
query.append(View.tableFragment)
59+
let columnNames: [QueryFragment] = View.TableColumns.allColumns
60+
.map { "\(quote: $0.name)" }
61+
query.append("\(.newlineOrSpace)(\(columnNames.joined(separator: ", ")))")
62+
query.append("\(.newlineOrSpace)AS")
63+
query.append("\(.newlineOrSpace)\(select)")
64+
return query
65+
}
66+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Dependencies
2+
import Foundation
3+
import InlineSnapshotTesting
4+
import StructuredQueries
5+
import Testing
6+
import _StructuredQueriesSQLite
7+
8+
extension SnapshotTests {
9+
@Suite struct ViewsTests {
10+
@Test func basics() {
11+
let query = CompletedReminder.createTemporaryView(
12+
as: Reminder
13+
.where(\.isCompleted)
14+
.select { CompletedReminder.Columns(reminderID: $0.id, title: $0.title) }
15+
)
16+
assertQuery(
17+
query
18+
) {
19+
"""
20+
CREATE TEMPORARY VIEW
21+
"completedReminders"
22+
("reminderID", "title")
23+
AS
24+
SELECT "reminders"."id" AS "reminderID", "reminders"."title" AS "title"
25+
FROM "reminders"
26+
WHERE "reminders"."isCompleted"
27+
"""
28+
} results: {
29+
"""
30+
31+
"""
32+
}
33+
assertQuery(
34+
CompletedReminder.limit(2)
35+
) {
36+
"""
37+
SELECT "completedReminders"."reminderID", "completedReminders"."title"
38+
FROM "completedReminders"
39+
LIMIT 2
40+
"""
41+
} results: {
42+
"""
43+
┌────────────────────────┐
44+
│ CompletedReminder( │
45+
│ reminderID: 4, │
46+
│ title: "Take a walk"
47+
│ ) │
48+
├────────────────────────┤
49+
│ CompletedReminder( │
50+
│ reminderID: 7, │
51+
│ title: "Get laundry"
52+
│ ) │
53+
└────────────────────────┘
54+
"""
55+
}
56+
assertQuery(
57+
query.drop()
58+
) {
59+
"""
60+
DROP VIEW "completedReminders"
61+
"""
62+
}
63+
}
64+
}
65+
}
66+
67+
@Table @Selection
68+
private struct CompletedReminder {
69+
let reminderID: Reminder.ID
70+
let title: String
71+
}
72+
73+
extension Table where Self: _Selection {
74+
static func foo() {}
75+
}

0 commit comments

Comments
 (0)