Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,7 @@ clause:
### Statement types

- ``Insert``

### Seeding a database

- ``Seeds``
7 changes: 7 additions & 0 deletions Sources/StructuredQueriesCore/SQLQueryExpression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,11 @@ public struct SQLQueryExpression<QueryValue>: Statement {
public init(_ expression: some QueryExpression<QueryValue>) {
self.queryFragment = expression.queryFragment
}

/// Creates a type erased query expression from a statement.
///
/// - Parameter statement: A statement.
public init(_ statement: some Statement<QueryValue>) {
self.queryFragment = statement.query
}
}
105 changes: 105 additions & 0 deletions Sources/StructuredQueriesCore/Seeds.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/// A type that can prepare statements to seed a database's initial state.
public struct Seeds: Sequence {
let seeds: [any Table]

/// Prepares a number of batched insert statements to be executed.
///
/// ```swift
/// Seeds {
/// SyncUp(id: 1, seconds: 60, theme: .appOrange, title: "Design")
/// SyncUp(id: 2, seconds: 60 * 10, theme: .periwinkle, title: "Engineering")
/// SyncUp(id: 3, seconds: 60 * 30, theme: .poppy, title: "Product")
///
/// for name in ["Blob", "Blob Jr", "Blob Sr", "Blob Esq", "Blob III", "Blob I"] {
/// Attendee.Draft(name: name, syncUpID: 1)
/// }
/// for name in ["Blob", "Blob Jr"] {
/// Attendee.Draft(name: name, syncUpID: 2)
/// }
/// for name in ["Blob Sr", "Blob Jr"] {
/// Attendee.Draft(name: name, syncUpID: 3)
/// }
/// }
/// // INSERT INTO "syncUps"
/// // ("id", "seconds", "theme", "title")
/// // VALUES
/// // (1, 60, 'appOrange', 'Design'),
/// // (2, 600, 'periwinkle', 'Engineering'),
/// // (3, 1800, 'poppy', 'Product');
/// // INSERT INTO "attendees"
/// // ("id", "name", "syncUpID")
/// // VALUES
/// // (NULL, 'Blob', 1),
/// // (NULL, 'Blob Jr', 1),
/// // (NULL, 'Blob Sr', 1),
/// // (NULL, 'Blob Esq', 1),
/// // (NULL, 'Blob III', 1),
/// // (NULL, 'Blob I', 1),
/// // (NULL, 'Blob', 2),
/// // (NULL, 'Blob Jr', 2),
/// // (NULL, 'Blob Sr', 3),
/// // (NULL, 'Blob Jr', 3);
/// ```
///
/// And then you can iterate over each insert statement and execute it given a database
/// connection. For example, using the [SharingGRDB][] driver:
///
/// ```swift
/// try database.write { db in
/// let seeds = Seeds {
/// // ...
/// }
/// for insert in seeds {
/// try db.execute(insert)
/// }
/// }
/// ```
///
/// > Tip: [SharingGRDB][] extends GRDB's `Database` connection with a `seed` method that can
/// > build and insert records in a single step:
/// >
/// > ```swift
/// > try db.seed {
/// > // ...
/// > }
/// > ```
///
/// [SharingGRDB]: https://github.com/pointfreeco/sharing-grdb
///
/// - Parameter build: A result builder closure that prepares statements to insert every built row.
public init(@InsertValuesBuilder<any Table> _ build: () -> [any Table]) {
self.seeds = build()
}

public func makeIterator() -> Iterator {
Iterator(seeds: seeds)
}

public struct Iterator: IteratorProtocol {
var seeds: [any Table]

public mutating func next() -> SQLQueryExpression<Void>? {
guard let first = seeds.first else { return nil }

let firstType = type(of: first)

if let firstType = firstType as? any TableDraft.Type {
func insertBatch<T: TableDraft>(_: T.Type) -> SQLQueryExpression<Void> {
let batch = Array(seeds.lazy.prefix { $0 is T }.compactMap { $0 as? T })
defer { seeds.removeFirst(batch.count) }
return SQLQueryExpression(T.PrimaryTable.insert(batch))
}

return insertBatch(firstType)
} else {
func insertBatch<T: StructuredQueriesCore.Table>(_: T.Type) -> SQLQueryExpression<Void> {
let batch = Array(seeds.lazy.prefix { $0 is T }.compactMap { $0 as? T })
defer { seeds.removeFirst(batch.count) }
return SQLQueryExpression(T.insert(batch))
}

return insertBatch(firstType)
}
}
}
}
233 changes: 102 additions & 131 deletions Tests/StructuredQueriesTests/Support/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ extension Database {
static func `default`() throws -> Database {
let db = try Database()
try db.migrate()
try db.createMockData()
try db.seedDatabase()
return db
}

Expand Down Expand Up @@ -145,136 +145,107 @@ extension Database {
)
}

func createMockData() throws {
try createDebugUsers()
try createDebugRemindersLists()
try createDebugReminders()
try createDebugTags()
}

func createDebugUsers() throws {
try execute(
User.insert {
$0.name
} values: {
"Blob"
"Blob Jr"
"Blob Sr"
}
)
}

func createDebugRemindersLists() throws {
try execute(
RemindersList.insert {
($0.color, $0.title)
} values: {
(0x4a99ef, "Personal")
(0xed8935, "Family")
(0xb25dd3, "Business")
}
)
}

func createDebugReminders() throws {
let now = Date(timeIntervalSinceReferenceDate: 0)
try execute(
Reminder.insert([
Reminder.Draft(
assignedUserID: 1,
dueDate: now,
notes: """
Milk, Eggs, Apples
""",
remindersListID: 1,
title: "Groceries"
),
Reminder.Draft(
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
isFlagged: true,
remindersListID: 1,
title: "Haircut"
),
Reminder.Draft(
dueDate: now,
notes: "Ask about diet",
priority: .high,
remindersListID: 1,
title: "Doctor appointment"
),
Reminder.Draft(
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 190),
isCompleted: true,
remindersListID: 1,
title: "Take a walk"
),
Reminder.Draft(
remindersListID: 1,
title: "Buy concert tickets"
),
Reminder.Draft(
dueDate: now.addingTimeInterval(60 * 60 * 24 * 2),
isFlagged: true,
priority: .high,
remindersListID: 2,
title: "Pick up kids from school"
),
Reminder.Draft(
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
isCompleted: true,
priority: .low,
remindersListID: 2,
title: "Get laundry"
),
Reminder.Draft(
dueDate: now.addingTimeInterval(60 * 60 * 24 * 4),
isCompleted: false,
priority: .high,
remindersListID: 2,
title: "Take out trash"
),
Reminder.Draft(
dueDate: now.addingTimeInterval(60 * 60 * 24 * 2),
notes: """
Status of tax return
Expenses for next year
Changing payroll company
""",
remindersListID: 3,
title: "Call accountant"
),
Reminder.Draft(
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
isCompleted: true,
priority: .medium,
remindersListID: 3,
title: "Send weekly emails"
),
])
)
}

func createDebugTags() throws {
try execute(
Tag.insert(\.title) {
"car"
"kids"
"someday"
"optional"
}
)
try execute(
ReminderTag.insert {
($0.reminderID, $0.tagID)
} values: {
(1, 3)
(1, 4)
(2, 3)
(2, 4)
(4, 1)
(4, 2)
}
)
func seedDatabase() throws {
try Seeds {
User(id: 1, name: "Blob")
User(id: 2, name: "Blob Jr")
User(id: 3, name: "Blob Sr")
RemindersList(id: 1, color: 0x4a99ef, title: "Personal")
RemindersList(id: 2, color: 0xed8935, title: "Family")
RemindersList(id: 3, color: 0xb25dd3, title: "Business")
let now = Date(timeIntervalSinceReferenceDate: 0)
Reminder(
id: 1,
assignedUserID: 1,
dueDate: now,
notes: """
Milk, Eggs, Apples
""",
remindersListID: 1,
title: "Groceries"
)
Reminder(
id: 2,
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
isFlagged: true,
remindersListID: 1,
title: "Haircut"
)
Reminder(
id: 3,
dueDate: now,
notes: "Ask about diet",
priority: .high,
remindersListID: 1,
title: "Doctor appointment"
)
Reminder(
id: 4,
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 190),
isCompleted: true,
remindersListID: 1,
title: "Take a walk"
)
Reminder(
id: 5,
remindersListID: 1,
title: "Buy concert tickets"
)
Reminder(
id: 6,
dueDate: now.addingTimeInterval(60 * 60 * 24 * 2),
isFlagged: true,
priority: .high,
remindersListID: 2,
title: "Pick up kids from school"
)
Reminder(
id: 7,
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
isCompleted: true,
priority: .low,
remindersListID: 2,
title: "Get laundry"
)
Reminder(
id: 8,
dueDate: now.addingTimeInterval(60 * 60 * 24 * 4),
isCompleted: false,
priority: .high,
remindersListID: 2,
title: "Take out trash"
)
Reminder(
id: 9,
dueDate: now.addingTimeInterval(60 * 60 * 24 * 2),
notes: """
Status of tax return
Expenses for next year
Changing payroll company
""",
remindersListID: 3,
title: "Call accountant"
)
Reminder(
id: 10,
dueDate: now.addingTimeInterval(-60 * 60 * 24 * 2),
isCompleted: true,
priority: .medium,
remindersListID: 3,
title: "Send weekly emails"
)
Tag(id: 1, title: "car")
Tag(id: 2, title: "kids")
Tag(id: 3, title: "someday")
Tag(id: 4, title: "optional")
ReminderTag(reminderID: 1, tagID: 3)
ReminderTag(reminderID: 1, tagID: 4)
ReminderTag(reminderID: 2, tagID: 3)
ReminderTag(reminderID: 2, tagID: 4)
ReminderTag(reminderID: 4, tagID: 1)
ReminderTag(reminderID: 4, tagID: 2)
}
.forEach(execute)
}
}

Expand Down