Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion Sources/StructuredQueries/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public macro Ephemeral() =
/// or common table expression.
@attached(
extension,
conformances: QueryRepresentable,
conformances: Selection,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Selection now has its own conformance so that we can write generic algorithms against them. This is what allows them to be passed to Table.insert.

names: named(Columns),
named(init(decoder:))
)
Expand Down
6 changes: 3 additions & 3 deletions Sources/StructuredQueriesCore/Internal/Deprecations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @InsertValuesBuilder is now responsible for building up an array of query fragments to insert.

onConflict updates: ((inout Updates<Self>) -> Void)?
) -> InsertOf<Self> {
insert(or: conflictResolution, columns, values: values, onConflictDoUpdate: updates)
Expand All @@ -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)
Expand Down
18 changes: 0 additions & 18 deletions Sources/StructuredQueriesCore/Operators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -733,24 +733,6 @@ extension QueryExpression where QueryValue == String {
LikeOperator(string: self, pattern: "\(pattern)", escape: escape)
}

/// A predicate expression from this string expression matched against another _via_ the `MATCH`
/// operator.
///
/// ```swift
/// Reminder.where { $0.title.match("get") }
/// // SELECT … FROM "reminders" WHERE ("reminders"."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> {
BinaryOperator(
lhs: self,
operator: "MATCH",
rhs: "\(pattern)"
)
}

/// A predicate expression from this string expression matched against another _via_ the `LIKE`
/// operator given a prefix.
///
Expand Down
113 changes: 113 additions & 0 deletions Sources/StructuredQueriesCore/SQLite/FTS5.swift
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)\
)
"""
)
}
}
45 changes: 44 additions & 1 deletion Sources/StructuredQueriesCore/Seeds.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Copy link
Member Author

Choose a reason for hiding this comment

The 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 seeds helper that will be incompatible with this feature.

self.seeds = build()
}

Expand Down Expand Up @@ -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
}
}
13 changes: 13 additions & 0 deletions Sources/StructuredQueriesCore/Selection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
public protocol Selection: QueryRepresentable {
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: ", ")
}
}
Loading