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
11 changes: 1 addition & 10 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Sources/StructuredQueriesCore/QueryBindable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ extension [UInt8]: QueryBindable, QueryExpression {

extension Bool: QueryBindable {
public var queryBinding: QueryBinding { .int(self ? 1 : 0) }
public init?(queryBinding: QueryBinding) {
guard case .int(let value) = queryBinding else { return nil }
self = value != 0
}
}

extension Double: QueryBindable {
Expand Down
1 change: 1 addition & 0 deletions Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ extension ScalarDatabaseFunction {
///
/// - Parameter input: Expressions representing the arguments of the function.
/// - Returns: An expression representing the function call.
@_disfavoredOverload
Copy link
Member Author

Choose a reason for hiding this comment

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

This is for better compiler error messaging.

public func callAsFunction<each T: QueryExpression>(
_ input: repeat each T
) -> some QueryExpression<Output>
Expand Down
19 changes: 12 additions & 7 deletions Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ extension DatabaseFunctionMacro: PeerMacro {
let argumentCount = declaration.signature.parameterClause.parameters.count

var bodyArguments: [String] = []
var representableInputTypes: [String] = []
var signature = declaration.signature
var invocationArgumentTypes: [TypeSyntax] = []
var parameters: [String] = []
Expand All @@ -125,6 +126,7 @@ extension DatabaseFunctionMacro: PeerMacro {
}
bodyArguments.append("\(parameter.type.trimmed)")
let type = (functionRepresentationIterator?.next()?.type ?? parameter.type).trimmed
representableInputTypes.append(type.trimmedDescription)
parameter.type = type.asQueryExpression()
if let defaultValue = parameter.defaultValue,
defaultValue.value.is(NilLiteralExprSyntax.self)
Expand All @@ -137,15 +139,16 @@ extension DatabaseFunctionMacro: PeerMacro {
parameters.append(parameterName)
argumentBindings.append((parameterName, "\(type)(queryBinding: arguments[\(offset)])"))
}
var inputType = bodyArguments.joined(separator: ", ")
var representableInputType = representableInputTypes.joined(separator: ", ")
let isVoidReturning = signature.returnClause == nil
let outputType = returnClause.type.trimmed
signature.returnClause = returnClause
signature.returnClause?.type = (functionRepresentation?.returnClause ?? returnClause).type
.asQueryExpression()
let representableOutputType = (functionRepresentation?.returnClause ?? returnClause).type
.trimmed
signature.returnClause?.type = representableOutputType.asQueryExpression()
let bodyReturnClause = " \(returnClause.trimmedDescription)"
let bodyType = """
(\(inputType))\
(\(bodyArguments.joined(separator: ", ")))\
\(declaration.signature.effectSpecifiers?.trimmedDescription ?? "")\
\(bodyReturnClause)
"""
Expand Down Expand Up @@ -198,7 +201,9 @@ extension DatabaseFunctionMacro: PeerMacro {
continue
}
}
inputType = bodyArguments.count == 1 ? inputType : "(\(inputType))"
representableInputType = representableInputTypes.count == 1
? representableInputType
: "(\(representableInputType))"

return [
"""
Expand All @@ -209,8 +214,8 @@ extension DatabaseFunctionMacro: PeerMacro {
"""
\(attributes)\(access)struct \(functionTypeName): \
StructuredQueriesSQLiteCore.ScalarDatabaseFunction {
public typealias Input = \(raw: inputType)
public typealias Output = \(outputType)
public typealias Input = \(raw: representableInputType)
public typealias Output = \(representableOutputType)
public let name = \(databaseFunctionName)
public let argumentCount: Int? = \(raw: argumentCount)
public let isDeterministic = \(raw: isDeterministic)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ extension SnapshotTests {
}

struct __macro_local_14jsonCapitalizefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction {
public typealias Input = [String]
public typealias Output = [String]
public typealias Input = [String].JSONRepresentation
public typealias Output = [String].JSONRepresentation
Comment on lines +129 to +130
Copy link
Member Author

Choose a reason for hiding this comment

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

This was allowing the generic callAsFunction to be called with invalid input when a representation was involved.

public let name = "jsonCapitalize"
public let argumentCount: Int? = 1
public let isDeterministic = false
Expand Down
64 changes: 64 additions & 0 deletions Tests/StructuredQueriesTests/DatabaseFunctionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,5 +271,69 @@ extension SnapshotTests {

#expect(logger.messages == ["Hello, world!"])
}

@DatabaseFunction(as: (([Tag].JSONRepresentation) -> String).self)
func joinTags(_ tags: [Tag]) -> String {
tags.map(\.title).joined(separator: ", ")
}

@Test func jsonArray() {
$joinTags.install(database.handle)

assertQuery(
Reminder
.group(by: \.id)
.leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) }
.leftJoin(Tag.all) { $1.tagID.eq($2.id) }
.select { $joinTags($2.jsonGroupArray()) }
) {
"""
SELECT "joinTags"(json_group_array(CASE WHEN ("tags"."rowid" IS NOT NULL) THEN json_object('id', json_quote("tags"."id"), 'title', json_quote("tags"."title")) END) FILTER (WHERE ("tags"."id" IS NOT NULL)))
FROM "reminders"
LEFT JOIN "remindersTags" ON ("reminders"."id" = "remindersTags"."reminderID")
LEFT JOIN "tags" ON ("remindersTags"."tagID" = "tags"."id")
GROUP BY "reminders"."id"
"""
} results: {
"""
┌─────────────────────┐
│ "someday, optional" │
│ "someday, optional" │
│ "" │
│ "car, kids" │
│ "" │
│ "" │
│ "" │
│ "" │
│ "" │
│ "" │
└─────────────────────┘
"""
}
}

@DatabaseFunction(as: ((Reminder.JSONRepresentation, Bool) -> Bool).self)
func isValid(_ reminder: Reminder, _ override: Bool = false) -> Bool {
!reminder.title.isEmpty || override
}
@Test func jsonObject() {
$isValid.install(database.handle)

assertQuery(
Reminder.select { $isValid($0.jsonObject(), true) }.limit(1)
) {
"""
SELECT "isValid"(json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title"), 'updatedAt', json_quote("reminders"."updatedAt")), 1)
FROM "reminders"
LIMIT 1
"""
} results: {
"""
┌──────┐
│ true │
└──────┘
"""
}
}
}
}