Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ With that you can insert reminders with notes like so:
}
}

> Tip: If you are using SQLite and would like to store your field as a `BLOB` of JSONB, you can
> annotate this field with `JSONBRepresentation`, instead.
#### Tagged identifiers

The [Tagged](https://github.com/pointfreeco/swift-tagged) library provides lightweight syntax for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- ``QueryOutput``
- ``init(queryOutput:)``
- ``queryOutput``
- ``queryFragment(decoding:)``

### Conformances

Expand Down
21 changes: 21 additions & 0 deletions Sources/StructuredQueriesCore/Internal/JSONCoders.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package import Foundation

package let jsonDecoder: JSONDecoder = {
var decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom {
try Date(iso8601String: $0.singleValueContainer().decode(String.self))
}
return decoder
}()

package let jsonEncoder: JSONEncoder = {
var encoder = JSONEncoder()
encoder.dateEncodingStrategy = .custom { date, encoder in
var container = encoder.singleValueContainer()
try container.encode(date.iso8601String)
}
#if DEBUG
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
#endif
return encoder
}()
5 changes: 5 additions & 0 deletions Sources/StructuredQueriesCore/Optional.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ extension Optional: QueryRepresentable where Wrapped: QueryRepresentable {
public var queryOutput: Wrapped.QueryOutput? {
self?.queryOutput
}

@inlinable
public static func queryFragment(decoding queryFragment: QueryFragment) -> QueryFragment {
Wrapped.queryFragment(decoding: queryFragment)
}
}

extension Optional: Table, PartialSelectStatement, Statement where Wrapped: Table {
Expand Down
17 changes: 17 additions & 0 deletions Sources/StructuredQueriesCore/QueryRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ public protocol QueryRepresentable<QueryOutput>: QueryDecodable {

/// Unwraps a value from this representation.
var queryOutput: QueryOutput { get }

/// A query fragment that prepares a stored expression for decoding.
///
/// Result columns in a `SELECT` or `RETURNING` clause are rendered through this function. The
/// default implementation returns the fragment unchanged.
///
/// - Parameter queryFragment: A fragment representing a stored expression.
/// - Returns: A fragment that can be decoded by this representation.
static func queryFragment(decoding queryFragment: QueryFragment) -> QueryFragment
}

extension QueryRepresentable {
@inlinable
@inline(__always)
public static func queryFragment(decoding queryFragment: QueryFragment) -> QueryFragment {
queryFragment
}
}

extension QueryRepresentable where Self: QueryDecodable, Self == QueryOutput {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,3 @@ extension _CodableJSONRepresentation: QueryDecodable {
)
}
}

private let jsonDecoder: JSONDecoder = {
var decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom {
try Date(iso8601String: $0.singleValueContainer().decode(String.self))
}
return decoder
}()

private let jsonEncoder: JSONEncoder = {
var encoder = JSONEncoder()
encoder.dateEncodingStrategy = .custom { date, encoder in
var container = encoder.singleValueContainer()
try container.encode(date.iso8601String)
}
#if DEBUG
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
#endif
return encoder
}()
6 changes: 3 additions & 3 deletions Sources/StructuredQueriesCore/Statements/Delete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ public struct Delete<From: Table, Returning> {
) -> Delete<From, (repeat each QueryValue)> {
var returning: [QueryFragment] = []
for resultColumn in repeat each selection(From.columns) {
returning.append("\(quote: resultColumn.name)")
returning.append(resultColumn.returningFragment)
}
return Delete<From, (repeat each QueryValue)>(
isEmpty: isEmpty,
where: `where`,
returning: Array(repeat each selection(From.columns))
returning: returning
)
}

Expand All @@ -125,7 +125,7 @@ public struct Delete<From: Table, Returning> {
) -> Delete<From, From> {
var returning: [QueryFragment] = []
for resultColumn in From.TableColumns.allColumns {
returning.append("\(quote: resultColumn.name)")
returning.append(resultColumn.returningFragment)
}
return Delete<From, From>(
isEmpty: isEmpty,
Expand Down
4 changes: 2 additions & 2 deletions Sources/StructuredQueriesCore/Statements/Insert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ public struct Insert<Into: Table, Returning> {
) -> Insert<Into, (repeat each QueryValue)> {
var returning: [QueryFragment] = []
for resultColumn in repeat each selection(From.columns) {
returning.append("\(quote: resultColumn.name)")
returning.append(resultColumn.returningFragment)
}
return Insert<Into, (repeat each QueryValue)>(
conflictResolution: conflictResolution,
Expand All @@ -798,7 +798,7 @@ public struct Insert<Into: Table, Returning> {
) -> Insert<Into, Into> {
var returning: [QueryFragment] = []
for resultColumn in From.TableColumns.allColumns {
returning.append("\(quote: resultColumn.name)")
returning.append(resultColumn.returningFragment)
}
return Insert<Into, Into>(
conflictResolution: conflictResolution,
Expand Down
5 changes: 3 additions & 2 deletions Sources/StructuredQueriesCore/Statements/Select.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1751,7 +1751,8 @@ extension Select: SelectStatement {
var query: QueryFragment = "SELECT"
let columns =
columns.isEmpty
? [From.columns.queryFragment] + joins.map { $0.tableColumns }
? $_isSelecting.withValue(true) { [From.columns.queryFragment] }
+ joins.map { $0.tableColumns }
: columns
if distinct {
query.append(" DISTINCT")
Expand Down Expand Up @@ -1817,7 +1818,7 @@ public struct _JoinClause: QueryExpression, Sendable {
self.constraint = constraint.queryFragment
self.operator = `operator`?.queryFragment
tableAlias = table.tableAlias
tableColumns = table.columns.queryFragment
tableColumns = $_isSelecting.withValue(true) { table.columns.queryFragment }
tableName = table.tableFragment
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/StructuredQueriesCore/Statements/Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public struct Update<From: Table, Returning> {
) -> Update<From, (repeat each QueryValue)> {
var returning: [QueryFragment] = []
for resultColumn in repeat each selection(From.columns) {
returning.append("\(quote: resultColumn.name)")
returning.append(resultColumn.returningFragment)
}
return Update<From, (repeat each QueryValue)>(
isEmpty: false,
Expand All @@ -187,7 +187,7 @@ public struct Update<From: Table, Returning> {
) -> Update<From, From> {
var returning: [QueryFragment] = []
for resultColumn in From.TableColumns.allColumns {
returning.append("\(quote: resultColumn.name)")
returning.append(resultColumn.returningFragment)
}
return Update<From, From>(
isEmpty: isEmpty,
Expand Down
10 changes: 8 additions & 2 deletions Sources/StructuredQueriesCore/TableColumn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ where Value: QueryBindable {

extension TableColumnExpression {
public var _names: [String] { [name] }

var returningFragment: QueryFragment {
Value.queryFragment(decoding: "\(quote: name)")
}
}

/// A type representing a _writable_ table column, _i.e._ not a generated column.
Expand Down Expand Up @@ -84,7 +88,8 @@ public struct TableColumn<Root: Table, Value: QueryRepresentable & QueryBindable
}

public var queryFragment: QueryFragment {
"\(Root.self).\(quote: name)"
let column: QueryFragment = "\(Root.self).\(quote: name)"
return _isSelecting ? Value.queryFragment(decoding: column) : column
}

public func _aliased<Name>(
Expand Down Expand Up @@ -182,7 +187,8 @@ public struct GeneratedColumn<Root: Table, Value: QueryRepresentable & QueryBind
}

public var queryFragment: QueryFragment {
"\(Root.self).\(quote: name)"
let column: QueryFragment = "\(Root.self).\(quote: name)"
return _isSelecting ? Value.queryFragment(decoding: column) : column
}

public func _aliased<Name>(
Expand Down
4 changes: 4 additions & 0 deletions Sources/StructuredQueriesCore/Traits/Tagged.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@
public init(queryOutput: QueryOutput) {
self.init(rawValue: RawValue(queryOutput: queryOutput.rawValue))
}

public static func queryFragment(decoding queryFragment: QueryFragment) -> QueryFragment {
RawValue.queryFragment(decoding: queryFragment)
}
}
#endif
33 changes: 24 additions & 9 deletions Sources/StructuredQueriesMacros/ColumnCheckMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ private func diagnoseUnrepresentableColumn(
Diagnostic(
node: Syntax(declaration),
message: MacroExpansionErrorMessage(
"\(defaultValue.map { "'\($0)'" } ?? "Type") is not representable as a column"
"""
\(defaultValue.map { "'\($0)'" } ?? "Type") is not a '@Selection' or representable as a \
column
"""
),
fixIts: fixIts
)
Expand All @@ -136,15 +139,27 @@ private func diagnoseUnrepresentableColumn(

if suggestingJSON {
fixIts.insert(
.replace(
message: MacroExpansionFixItMessage(
"Apply '@Column(as: \(type).JSONRepresentation.self)' to store as JSON"
contentsOf: [
.replace(
message: MacroExpansionFixItMessage(
"Apply '@Column(as: \(type).JSONRepresentation.self)' to store as JSON"
),
oldNode: declaration,
newNode: declaration.applyingColumnFixIt(
"@Column(as: \(raw: type).JSONRepresentation.self)"
)
),
oldNode: declaration,
newNode: declaration.applyingColumnFixIt(
"@Column(as: \(raw: type).JSONRepresentation.self)"
)
),
// TODO: Hide behind 'SQLite' trait when introduced in the future
.replace(
message: MacroExpansionFixItMessage(
"Apply '@Column(as: \(type).JSONBRepresentation.self)' to store as JSONB"
),
oldNode: declaration,
newNode: declaration.applyingColumnFixIt(
"@Column(as: \(raw: type).JSONBRepresentation.self)"
)
),
],
at: 0
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/StructuredQueriesSQLiteCore/Cast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ extension RawRepresentable where RawValue: SQLiteType {

extension _CodableJSONRepresentation: SQLiteType {
public static var typeAffinity: SQLiteTypeAffinity {
String.typeAffinity
.text
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ custom database functions, and more.
- ``Foundation/Date/UnixTimeRepresentation``
- ``Foundation/UUID/BytesRepresentation``
- ``Foundation/UUID/UppercasedRepresentation``
- ``Swift/Decodable/JSONBRepresentation``

### Custom collations

Expand Down
Loading