-
Notifications
You must be signed in to change notification settings - Fork 43
More flexible insert builder #135
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
Changes from 11 commits
a23d2cd
48f5817
c843352
0606e55
2a09df9
41ecef1
5c49141
79d2c1d
63284f4
3bd3299
61ef610
b4c01ea
f725b5e
3783b88
29ac227
5e6fab6
49bb4ed
930eab1
2437e26
d130940
8e09c19
3478d4d
d71b8db
4a60641
b23fe1a
a038fa8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,7 +65,7 @@ extension Table { | |
| public static func insert( | ||
| or conflictResolution: ConflictResolution? = nil, | ||
| _ columns: (TableColumns) -> TableColumns = { $0 }, | ||
| @InsertValuesBuilder<Self> values: () -> [Self], | ||
| @InsertValuesBuilder<Self> values: () -> [[QueryFragment]], | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| onConflict updates: ((inout Updates<Self>) -> Void)? | ||
| ) -> InsertOf<Self> { | ||
| insert(or: conflictResolution, columns, values: values, onConflictDoUpdate: updates) | ||
|
|
@@ -75,8 +75,8 @@ extension Table { | |
| public static func insert<V1, each V2>( | ||
| or conflictResolution: ConflictResolution? = nil, | ||
| _ columns: (TableColumns) -> (TableColumn<Self, V1>, repeat TableColumn<Self, each V2>), | ||
| @InsertValuesBuilder<(V1.QueryOutput, repeat (each V2).QueryOutput)> | ||
| values: () -> [(V1.QueryOutput, repeat (each V2).QueryOutput)], | ||
| @InsertValuesBuilder<(V1, repeat each V2)> | ||
| values: () -> [[QueryFragment]], | ||
| onConflict updates: ((inout Updates<Self>) -> Void)? | ||
| ) -> InsertOf<Self> { | ||
| insert(or: conflictResolution, columns, values: values, onConflictDoUpdate: updates) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| /// A virtual table using the FTS5 extension. | ||
| /// | ||
| /// Apply this protocol to a `@Table` declaration to introduce FTS5 helpers. | ||
| public protocol FTS5: Table {} | ||
|
|
||
| extension TableDefinition where QueryValue: FTS5 { | ||
| @available(*, deprecated, message: "Virtual tables are not 'rowid' tables") | ||
| public var rowid: some QueryExpression<Int> { | ||
| SQLQueryExpression( | ||
| """ | ||
| \(QueryValue.self)."rowid" | ||
| """ | ||
| ) | ||
| } | ||
|
|
||
| /// An expression representing the search result's rank. | ||
| public var rank: some QueryExpression<Double?> { | ||
| SQLQueryExpression( | ||
| """ | ||
| \(QueryValue.self)."rank" | ||
| """ | ||
| ) | ||
| } | ||
|
|
||
| /// A predicate expression from this table matched against another _via_ the `MATCH` operator. | ||
| /// | ||
| /// ```swift | ||
| /// ReminderText.where { $0.match("get") } | ||
| /// // SELECT … FROM "reminderTexts" WHERE ("reminderTexts" MATCH 'get') | ||
| /// ``` | ||
| /// | ||
| /// - Parameter pattern: A string expression describing the `MATCH` pattern. | ||
| /// - Returns: A predicate expression. | ||
| public func match(_ pattern: some StringProtocol) -> some QueryExpression<Bool> { | ||
| SQLQueryExpression( | ||
| """ | ||
| (\(QueryValue.self) MATCH \(bind: "\(pattern)")) | ||
| """ | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| extension TableColumnExpression where Root: FTS5 { | ||
| /// A string expression highlighting matches in this column using the given delimiters. | ||
| /// | ||
| /// - Parameters: | ||
| /// - open: An opening delimiter denoting the beginning of a match, _e.g._ `"<b>"`. | ||
| /// - close: A closing delimiter denoting the end of a match, _e.g._, `"</b>"`. | ||
| /// - Returns: A string expression highlighting matches in this column. | ||
| public func highlight( | ||
| _ open: some StringProtocol, | ||
| _ close: some StringProtocol | ||
| ) -> some QueryExpression<String> { | ||
| SQLQueryExpression( | ||
| """ | ||
| highlight(\ | ||
| \(Root.self), \ | ||
| (\ | ||
| SELECT "cid" FROM pragma_table_info(\(quote: Root.tableName, delimiter: .text)) \ | ||
| WHERE "name" = \(quote: name, delimiter: .text)\ | ||
| ), | ||
| \(quote: "\(open)", delimiter: .text), \ | ||
| \(quote: "\(close)", delimiter: .text)\ | ||
| ) | ||
| """ | ||
| ) | ||
| } | ||
|
|
||
| /// A predicate expression from this column matched against another _via_ the `MATCH` operator. | ||
| /// | ||
| /// ```swift | ||
| /// ReminderText.where { $0.title.match("get") } | ||
| /// // SELECT … FROM "reminderTexts" WHERE ("reminderTexts"."title" MATCH 'get') | ||
| /// ``` | ||
| /// | ||
| /// - Parameter pattern: A string expression describing the `MATCH` pattern. | ||
| /// - Returns: A predicate expression. | ||
| public func match(_ pattern: some StringProtocol) -> some QueryExpression<Bool> { | ||
| Root.columns.match("\(name):\(pattern)") | ||
| } | ||
|
|
||
| /// A string expression highlighting matches in text fragments of this column using the given | ||
| /// delimiters. | ||
| /// | ||
| /// - Parameters: | ||
| /// - open: An opening delimiter denoting the beginning of a match, _e.g._ `"<b>"`. | ||
| /// - close: A closing delimiter denoting the end of a match, _e.g._, `"</b>"`. | ||
| /// - ellipsis: Text indicating a truncation of text in the column. | ||
| /// - tokens: The maximum number of tokens in the returned text. | ||
| /// - Returns: A string expression highlighting matches in this column. | ||
| public func snippet( | ||
| _ open: some StringProtocol, | ||
| _ close: some StringProtocol, | ||
| _ ellipsis: some StringProtocol, | ||
| _ tokens: Int | ||
| ) -> some QueryExpression<String> { | ||
| SQLQueryExpression( | ||
| """ | ||
| snippet(\ | ||
| \(Root.self), \ | ||
| (\ | ||
| SELECT "cid" FROM pragma_table_info(\(quote: Root.tableName, delimiter: .text)) \ | ||
| WHERE "name" = \(quote: name, delimiter: .text)\ | ||
| ), | ||
| \(quote: "\(open)", delimiter: .text), \ | ||
| \(quote: "\(close)", delimiter: .text), \ | ||
| \(quote: "\(ellipsis)", delimiter: .text), \ | ||
| \(raw: tokens)\ | ||
| ) | ||
| """ | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,7 +67,7 @@ public struct Seeds: Sequence { | |
| /// [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]) { | ||
| public init(@SeedsBuilder _ build: () -> [any Table]) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main gotcha in this PR is that we need a simultaneous release with SharingGRDB since it defines a |
||
| self.seeds = build() | ||
| } | ||
|
|
||
|
|
@@ -103,3 +103,46 @@ public struct Seeds: Sequence { | |
| } | ||
| } | ||
| } | ||
|
|
||
| @resultBuilder | ||
| public enum SeedsBuilder { | ||
| public static func buildArray(_ components: [[any Table]]) -> [any Table] { | ||
| components.flatMap(\.self) | ||
| } | ||
|
|
||
| public static func buildBlock(_ components: [any Table]) -> [any Table] { | ||
| components | ||
| } | ||
|
|
||
| public static func buildEither(first component: [any Table]) -> [any Table] { | ||
| component | ||
| } | ||
|
|
||
| public static func buildEither(second component: [any Table]) -> [any Table] { | ||
| component | ||
| } | ||
|
|
||
| public static func buildExpression(_ expression: some Table) -> [any Table] { | ||
| [expression] | ||
| } | ||
|
|
||
| public static func buildExpression(_ expression: [any Table]) -> [any Table] { | ||
| expression | ||
| } | ||
|
|
||
| public static func buildLimitedAvailability(_ component: [any Table]) -> [any Table] { | ||
| component | ||
| } | ||
|
|
||
| public static func buildOptional(_ component: [any Table]?) -> [any Table] { | ||
| component ?? [] | ||
| } | ||
|
|
||
| public static func buildPartialBlock(first: [any Table]) -> [any Table] { | ||
| first | ||
| } | ||
|
|
||
| public static func buildPartialBlock(accumulated: [any Table], next: [any Table]) -> [any Table] { | ||
| accumulated + next | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| public protocol Selection: QueryRepresentable { | ||
stephencelis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| associatedtype Columns: SelectedColumns<Self> | ||
| } | ||
|
|
||
| public protocol SelectedColumns<QueryValue>: QueryExpression { | ||
| var selection: [(aliasName: String, expression: QueryFragment)] { get } | ||
| } | ||
|
|
||
| extension SelectedColumns { | ||
| public var queryFragment: QueryFragment { | ||
| selection.map { "\($1) AS \(quote: $0)" as QueryFragment }.joined(separator: ", ") | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Selectionnow has its own conformance so that we can write generic algorithms against them. This is what allows them to be passed toTable.insert.