Skip to content

Insert with specific unique constraint #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
132 changes: 132 additions & 0 deletions Sources/StructuredQueriesCore/Statements/Insert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,72 @@ extension Table {
returning: []
)
}

/// An insert statement for a table row, describing a specific unique constraint.
///
/// - Parameters:
/// - row: Row to insert.
/// - onConflict: The columns to check for conflict.
/// - doUpdate: Updates to perform in an upsert clause should the insert conflict with an
/// existing row.
/// - Returns: An insert statement.
public static func insert<
each Constraint: QueryBindable
>(
_ row: () -> Self,
onConflict: (TableColumns) -> (repeat TableColumn<Self, each Constraint>),
doUpdate: (inout Updates<Self>) -> Void = { _ in }
) -> InsertOf<Self> {
insert(
values: { [row()] },
onConflict: onConflict,
doUpdate: doUpdate
)
}

/// An insert statement for a table row, describing a specific unique constraint.
///
/// - Parameters:
/// - rows: Rows to insert. An empty array will return an empty query.
/// - onConflict: The columns to check for conflict.
/// - doUpdate: Updates to perform in an upsert clause should the insert conflict with an
/// existing row.
/// - Returns: An insert statement.
public static func insert<
each Constraint: QueryBindable
>(
values rows: () -> [Self],
onConflict: (TableColumns) -> (repeat TableColumn<Self, each Constraint>),
doUpdate: (inout Updates<Self>) -> Void = { _ in }
) -> InsertOf<Self> {
var columnNames: [String] = []
for column in TableColumns.allColumns {
columnNames.append(column.name)
}
var values: [[QueryFragment]] = []
for row in rows() {
var value: [QueryFragment] = []
for column in TableColumns.allColumns {
func open<Root, Value>(_ column: some TableColumnExpression<Root, Value>) -> QueryFragment {
Value(queryOutput: (row as! Root)[keyPath: column.keyPath]).queryFragment
}
value.append(open(column))
}
values.append(value)
}
var conflictTargetColumnNames: [String] = []
for column in repeat each onConflict(Self.columns) {
conflictTargetColumnNames.append(column.name)
}
return Insert(
conflictResolution: nil,
columnNames: columnNames,
conflictTargetColumnNames: conflictTargetColumnNames,
values: .values(values),
updates: Updates(doUpdate),
returning: []
)
}
}

extension PrimaryKeyedTable {
Expand Down Expand Up @@ -327,6 +393,72 @@ extension PrimaryKeyedTable {
returning: []
)
}

/// An insert statement for a table row draft, describing a specific unique constraint.
///
/// - Parameters:
/// - row: Row to insert.
/// - onConflict: The columns to check for conflict.
/// - doUpdate: Updates to perform in an upsert clause should the insert conflict with an
/// existing row.
/// - Returns: An insert statement.
public static func insert<
each Constraint: QueryBindable
>(
_ row: () -> Draft,
onConflict: (Draft.TableColumns) -> (repeat TableColumn<Draft, each Constraint>),
doUpdate: (inout Updates<Self>) -> Void = { _ in }
) -> InsertOf<Self> {
insert(
values: { [row()] },
onConflict: onConflict,
doUpdate: doUpdate
)
}

/// An insert statement for a table row draft, describing a specific unique constraint.
///
/// - Parameters:
/// - rows: Rows to insert. An empty array will return an empty query.
/// - onConflict: The columns to check for conflict.
/// - doUpdate: Updates to perform in an upsert clause should the insert conflict with an
/// existing row.
/// - Returns: An insert statement.
public static func insert<
each Constraint: QueryBindable
>(
values rows: () -> [Draft],
onConflict: (Draft.TableColumns) -> (repeat TableColumn<Draft, each Constraint>),
doUpdate: (inout Updates<Self>) -> Void = { _ in }
) -> InsertOf<Self> {
var columnNames: [String] = []
for column in Draft.TableColumns.allColumns {
columnNames.append(column.name)
}
var values: [[QueryFragment]] = []
for row in rows() {
var value: [QueryFragment] = []
for column in Draft.TableColumns.allColumns {
func open<Root, Value>(_ column: some TableColumnExpression<Root, Value>) -> QueryFragment {
Value(queryOutput: (row as! Root)[keyPath: column.keyPath]).queryFragment
}
value.append(open(column))
}
values.append(value)
}
var conflictTargetColumnNames: [String] = []
for column in repeat each onConflict(Draft.columns) {
conflictTargetColumnNames.append(column.name)
}
return Insert(
conflictResolution: nil,
columnNames: columnNames,
conflictTargetColumnNames: conflictTargetColumnNames,
values: .values(values),
updates: Updates(doUpdate),
returning: []
)
}
}

private enum InsertValues {
Expand Down
59 changes: 59 additions & 0 deletions Tests/StructuredQueriesTests/InsertTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,65 @@ extension SnapshotTests {
}
}

@Test func upsertWithoutID_onConflictDoUpdate() {
assertQuery(
RemindersList.insert {
RemindersList.Draft(title: "Personal")
} onConflict: {
($0.title)
} doUpdate: {
$0.color = 0x00ff00
}.returning(\.self)
) {
"""
INSERT INTO "remindersLists"
("id", "color", "title")
VALUES
(NULL, 4889071, 'Personal')
ON CONFLICT ("title") DO UPDATE SET "color" = 65280
RETURNING "id", "color", "title"
"""
} results: {
"""
┌─────────────────────┐
│ RemindersList( │
│ id: 1, │
│ color: 65280, │
│ title: "Personal" │
│ ) │
└─────────────────────┘
"""
}
}

@Test func upsertNonPrimaryKey_onConflictDoUpdate() {
assertQuery(
ReminderTag.insert {
ReminderTag(reminderID: 1, tagID: 3)
} onConflict: {
($0.reminderID, $0.tagID)
}.returning(\.self)
) {
"""
INSERT INTO "remindersTags"
("reminderID", "tagID")
VALUES
(1, 3)
ON CONFLICT ("reminderID", "tagID") DO NOTHING
RETURNING "reminderID", "tagID"
"""
} results: {
"""
┌──────────────────┐
│ ReminderTag( │
│ reminderID: 1, │
│ tagID: 1 │
│ ) │
└──────────────────┘
"""
}
}

@Test func sql() {
assertQuery(
#sql(
Expand Down
3 changes: 2 additions & 1 deletion Tests/StructuredQueriesTests/Support/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ extension Database {
"""
CREATE TABLE "remindersTags" (
"reminderID" INTEGER NOT NULL REFERENCES "reminders"("id") ON DELETE CASCADE,
"tagID" INTEGER NOT NULL REFERENCES "tags"("id") ON DELETE CASCADE
"tagID" INTEGER NOT NULL REFERENCES "tags"("id") ON DELETE CASCADE,
UNIQUE("reminderID", "tagID")
)
"""
)
Expand Down