Skip to content

Commit d14372b

Browse files
authored
Add Seeds type for preparings seeds for a database (#29)
* Add `Seeds` type for preparings seeds for a database This type is mainly to simplify downstream integrations like `SharingGRDB`, which already provides such a tool defined directly on `GRDB.Database`. That tool will be able to simply use this tool, instead, and future integrations can also leverage it. * wip * Leverage seeds in test suite
1 parent 31b9cce commit d14372b

File tree

4 files changed

+218
-131
lines changed

4 files changed

+218
-131
lines changed

Sources/StructuredQueriesCore/Documentation.docc/Articles/InsertStatements.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,7 @@ clause:
447447
### Statement types
448448

449449
- ``Insert``
450+
451+
### Seeding a database
452+
453+
- ``Seeds``

Sources/StructuredQueriesCore/SQLQueryExpression.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,11 @@ public struct SQLQueryExpression<QueryValue>: Statement {
3535
public init(_ expression: some QueryExpression<QueryValue>) {
3636
self.queryFragment = expression.queryFragment
3737
}
38+
39+
/// Creates a type erased query expression from a statement.
40+
///
41+
/// - Parameter statement: A statement.
42+
public init(_ statement: some Statement<QueryValue>) {
43+
self.queryFragment = statement.query
44+
}
3845
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/// A type that can prepare statements to seed a database's initial state.
2+
public struct Seeds: Sequence {
3+
let seeds: [any Table]
4+
5+
/// Prepares a number of batched insert statements to be executed.
6+
///
7+
/// ```swift
8+
/// Seeds {
9+
/// SyncUp(id: 1, seconds: 60, theme: .appOrange, title: "Design")
10+
/// SyncUp(id: 2, seconds: 60 * 10, theme: .periwinkle, title: "Engineering")
11+
/// SyncUp(id: 3, seconds: 60 * 30, theme: .poppy, title: "Product")
12+
///
13+
/// for name in ["Blob", "Blob Jr", "Blob Sr", "Blob Esq", "Blob III", "Blob I"] {
14+
/// Attendee.Draft(name: name, syncUpID: 1)
15+
/// }
16+
/// for name in ["Blob", "Blob Jr"] {
17+
/// Attendee.Draft(name: name, syncUpID: 2)
18+
/// }
19+
/// for name in ["Blob Sr", "Blob Jr"] {
20+
/// Attendee.Draft(name: name, syncUpID: 3)
21+
/// }
22+
/// }
23+
/// // INSERT INTO "syncUps"
24+
/// // ("id", "seconds", "theme", "title")
25+
/// // VALUES
26+
/// // (1, 60, 'appOrange', 'Design'),
27+
/// // (2, 600, 'periwinkle', 'Engineering'),
28+
/// // (3, 1800, 'poppy', 'Product');
29+
/// // INSERT INTO "attendees"
30+
/// // ("id", "name", "syncUpID")
31+
/// // VALUES
32+
/// // (NULL, 'Blob', 1),
33+
/// // (NULL, 'Blob Jr', 1),
34+
/// // (NULL, 'Blob Sr', 1),
35+
/// // (NULL, 'Blob Esq', 1),
36+
/// // (NULL, 'Blob III', 1),
37+
/// // (NULL, 'Blob I', 1),
38+
/// // (NULL, 'Blob', 2),
39+
/// // (NULL, 'Blob Jr', 2),
40+
/// // (NULL, 'Blob Sr', 3),
41+
/// // (NULL, 'Blob Jr', 3);
42+
/// ```
43+
///
44+
/// And then you can iterate over each insert statement and execute it given a database
45+
/// connection. For example, using the [SharingGRDB][] driver:
46+
///
47+
/// ```swift
48+
/// try database.write { db in
49+
/// let seeds = Seeds {
50+
/// // ...
51+
/// }
52+
/// for insert in seeds {
53+
/// try db.execute(insert)
54+
/// }
55+
/// }
56+
/// ```
57+
///
58+
/// > Tip: [SharingGRDB][] extends GRDB's `Database` connection with a `seed` method that can
59+
/// > build and insert records in a single step:
60+
/// >
61+
/// > ```swift
62+
/// > try db.seed {
63+
/// > // ...
64+
/// > }
65+
/// > ```
66+
///
67+
/// [SharingGRDB]: https://github.com/pointfreeco/sharing-grdb
68+
///
69+
/// - Parameter build: A result builder closure that prepares statements to insert every built row.
70+
public init(@InsertValuesBuilder<any Table> _ build: () -> [any Table]) {
71+
self.seeds = build()
72+
}
73+
74+
public func makeIterator() -> Iterator {
75+
Iterator(seeds: seeds)
76+
}
77+
78+
public struct Iterator: IteratorProtocol {
79+
var seeds: [any Table]
80+
81+
public mutating func next() -> SQLQueryExpression<Void>? {
82+
guard let first = seeds.first else { return nil }
83+
84+
let firstType = type(of: first)
85+
86+
if let firstType = firstType as? any TableDraft.Type {
87+
func insertBatch<T: TableDraft>(_: T.Type) -> SQLQueryExpression<Void> {
88+
let batch = Array(seeds.lazy.prefix { $0 is T }.compactMap { $0 as? T })
89+
defer { seeds.removeFirst(batch.count) }
90+
return SQLQueryExpression(T.PrimaryTable.insert(batch))
91+
}
92+
93+
return insertBatch(firstType)
94+
} else {
95+
func insertBatch<T: StructuredQueriesCore.Table>(_: T.Type) -> SQLQueryExpression<Void> {
96+
let batch = Array(seeds.lazy.prefix { $0 is T }.compactMap { $0 as? T })
97+
defer { seeds.removeFirst(batch.count) }
98+
return SQLQueryExpression(T.insert(batch))
99+
}
100+
101+
return insertBatch(firstType)
102+
}
103+
}
104+
}
105+
}

Tests/StructuredQueriesTests/Support/Schema.swift

Lines changed: 102 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension Database {
7070
static func `default`() throws -> Database {
7171
let db = try Database()
7272
try db.migrate()
73-
try db.createMockData()
73+
try db.seedDatabase()
7474
return db
7575
}
7676

@@ -145,136 +145,107 @@ extension Database {
145145
)
146146
}
147147

148-
func createMockData() throws {
149-
try createDebugUsers()
150-
try createDebugRemindersLists()
151-
try createDebugReminders()
152-
try createDebugTags()
153-
}
154-
155-
func createDebugUsers() throws {
156-
try execute(
157-
User.insert {
158-
$0.name
159-
} values: {
160-
"Blob"
161-
"Blob Jr"
162-
"Blob Sr"
163-
}
164-
)
165-
}
166-
167-
func createDebugRemindersLists() throws {
168-
try execute(
169-
RemindersList.insert {
170-
($0.color, $0.title)
171-
} values: {
172-
(0x4a99ef, "Personal")
173-
(0xed8935, "Family")
174-
(0xb25dd3, "Business")
175-
}
176-
)
177-
}
178-
179-
func createDebugReminders() throws {
180-
let now = Date(timeIntervalSinceReferenceDate: 0)
181-
try execute(
182-
Reminder.insert([
183-
Reminder.Draft(
184-
assignedUserID: 1,
185-
dueDate: now,
186-
notes: """
187-
Milk, Eggs, Apples
188-
""",
189-
remindersListID: 1,
190-
title: "Groceries"
191-
),
192-
Reminder.Draft(
193-
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
194-
isFlagged: true,
195-
remindersListID: 1,
196-
title: "Haircut"
197-
),
198-
Reminder.Draft(
199-
dueDate: now,
200-
notes: "Ask about diet",
201-
priority: .high,
202-
remindersListID: 1,
203-
title: "Doctor appointment"
204-
),
205-
Reminder.Draft(
206-
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 190),
207-
isCompleted: true,
208-
remindersListID: 1,
209-
title: "Take a walk"
210-
),
211-
Reminder.Draft(
212-
remindersListID: 1,
213-
title: "Buy concert tickets"
214-
),
215-
Reminder.Draft(
216-
dueDate: now.addingTimeInterval(60 * 60 * 24 * 2),
217-
isFlagged: true,
218-
priority: .high,
219-
remindersListID: 2,
220-
title: "Pick up kids from school"
221-
),
222-
Reminder.Draft(
223-
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
224-
isCompleted: true,
225-
priority: .low,
226-
remindersListID: 2,
227-
title: "Get laundry"
228-
),
229-
Reminder.Draft(
230-
dueDate: now.addingTimeInterval(60 * 60 * 24 * 4),
231-
isCompleted: false,
232-
priority: .high,
233-
remindersListID: 2,
234-
title: "Take out trash"
235-
),
236-
Reminder.Draft(
237-
dueDate: now.addingTimeInterval(60 * 60 * 24 * 2),
238-
notes: """
239-
Status of tax return
240-
Expenses for next year
241-
Changing payroll company
242-
""",
243-
remindersListID: 3,
244-
title: "Call accountant"
245-
),
246-
Reminder.Draft(
247-
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
248-
isCompleted: true,
249-
priority: .medium,
250-
remindersListID: 3,
251-
title: "Send weekly emails"
252-
),
253-
])
254-
)
255-
}
256-
257-
func createDebugTags() throws {
258-
try execute(
259-
Tag.insert(\.title) {
260-
"car"
261-
"kids"
262-
"someday"
263-
"optional"
264-
}
265-
)
266-
try execute(
267-
ReminderTag.insert {
268-
($0.reminderID, $0.tagID)
269-
} values: {
270-
(1, 3)
271-
(1, 4)
272-
(2, 3)
273-
(2, 4)
274-
(4, 1)
275-
(4, 2)
276-
}
277-
)
148+
func seedDatabase() throws {
149+
try Seeds {
150+
User(id: 1, name: "Blob")
151+
User(id: 2, name: "Blob Jr")
152+
User(id: 3, name: "Blob Sr")
153+
RemindersList(id: 1, color: 0x4a99ef, title: "Personal")
154+
RemindersList(id: 2, color: 0xed8935, title: "Family")
155+
RemindersList(id: 3, color: 0xb25dd3, title: "Business")
156+
let now = Date(timeIntervalSinceReferenceDate: 0)
157+
Reminder(
158+
id: 1,
159+
assignedUserID: 1,
160+
dueDate: now,
161+
notes: """
162+
Milk, Eggs, Apples
163+
""",
164+
remindersListID: 1,
165+
title: "Groceries"
166+
)
167+
Reminder(
168+
id: 2,
169+
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
170+
isFlagged: true,
171+
remindersListID: 1,
172+
title: "Haircut"
173+
)
174+
Reminder(
175+
id: 3,
176+
dueDate: now,
177+
notes: "Ask about diet",
178+
priority: .high,
179+
remindersListID: 1,
180+
title: "Doctor appointment"
181+
)
182+
Reminder(
183+
id: 4,
184+
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 190),
185+
isCompleted: true,
186+
remindersListID: 1,
187+
title: "Take a walk"
188+
)
189+
Reminder(
190+
id: 5,
191+
remindersListID: 1,
192+
title: "Buy concert tickets"
193+
)
194+
Reminder(
195+
id: 6,
196+
dueDate: now.addingTimeInterval(60 * 60 * 24 * 2),
197+
isFlagged: true,
198+
priority: .high,
199+
remindersListID: 2,
200+
title: "Pick up kids from school"
201+
)
202+
Reminder(
203+
id: 7,
204+
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
205+
isCompleted: true,
206+
priority: .low,
207+
remindersListID: 2,
208+
title: "Get laundry"
209+
)
210+
Reminder(
211+
id: 8,
212+
dueDate: now.addingTimeInterval(60 * 60 * 24 * 4),
213+
isCompleted: false,
214+
priority: .high,
215+
remindersListID: 2,
216+
title: "Take out trash"
217+
)
218+
Reminder(
219+
id: 9,
220+
dueDate: now.addingTimeInterval(60 * 60 * 24 * 2),
221+
notes: """
222+
Status of tax return
223+
Expenses for next year
224+
Changing payroll company
225+
""",
226+
remindersListID: 3,
227+
title: "Call accountant"
228+
)
229+
Reminder(
230+
id: 10,
231+
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
232+
isCompleted: true,
233+
priority: .medium,
234+
remindersListID: 3,
235+
title: "Send weekly emails"
236+
)
237+
Tag(id: 1, title: "car")
238+
Tag(id: 2, title: "kids")
239+
Tag(id: 3, title: "someday")
240+
Tag(id: 4, title: "optional")
241+
ReminderTag(reminderID: 1, tagID: 3)
242+
ReminderTag(reminderID: 1, tagID: 4)
243+
ReminderTag(reminderID: 2, tagID: 3)
244+
ReminderTag(reminderID: 2, tagID: 4)
245+
ReminderTag(reminderID: 4, tagID: 1)
246+
ReminderTag(reminderID: 4, tagID: 2)
247+
}
248+
.forEach(execute)
278249
}
279250
}
280251

0 commit comments

Comments
 (0)