diff --git a/Package.resolved b/Package.resolved index 97f86198..279fb07e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b5899dec35ccc82d161bdd4fd1114afbc74a459e1a742760fd9c65bc0582a716", + "originHash" : "06aba47229f3efe31fff79bc09af1ad2bc07a7df36bf0aa5433087c9340ac408", "pins" : [ { "identity" : "combine-schedulers", @@ -91,15 +91,6 @@ "version" : "601.0.1" } }, - { - "identity" : "swift-tagged", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-tagged", - "state" : { - "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", - "version" : "0.10.0" - } - }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/Sources/StructuredQueriesCore/QueryBindable.swift b/Sources/StructuredQueriesCore/QueryBindable.swift index 8e8f3679..558a31ab 100644 --- a/Sources/StructuredQueriesCore/QueryBindable.swift +++ b/Sources/StructuredQueriesCore/QueryBindable.swift @@ -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 { diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift index ce8cedfa..14df554b 100644 --- a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -38,6 +38,7 @@ extension ScalarDatabaseFunction { /// /// - Parameter input: Expressions representing the arguments of the function. /// - Returns: An expression representing the function call. + @_disfavoredOverload public func callAsFunction( _ input: repeat each T ) -> some QueryExpression diff --git a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift index 91c1304f..2142e564 100644 --- a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift +++ b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift @@ -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] = [] @@ -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) @@ -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) """ @@ -198,7 +201,9 @@ extension DatabaseFunctionMacro: PeerMacro { continue } } - inputType = bodyArguments.count == 1 ? inputType : "(\(inputType))" + representableInputType = representableInputTypes.count == 1 + ? representableInputType + : "(\(representableInputType))" return [ """ @@ -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) diff --git a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift index e2f1a070..83457ba5 100644 --- a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift +++ b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift @@ -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 public let name = "jsonCapitalize" public let argumentCount: Int? = 1 public let isDeterministic = false diff --git a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift index d88c8fad..ae300f41 100644 --- a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift +++ b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift @@ -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 │ + └──────┘ + """ + } + } } }