diff --git a/.spi.yml b/.spi.yml index 4eaeee37..b6f7e3f2 100644 --- a/.spi.yml +++ b/.spi.yml @@ -4,6 +4,8 @@ builder: - documentation_targets: - StructuredQueriesCore - StructuredQueries + - StructuredQueriesSQLiteCore + - StructuredQueriesSQLite custom_documentation_parameters: - '--enable-experimental-overloaded-symbol-presentation' # - '--enable-experimental-combined-documentation' diff --git a/Package.swift b/Package.swift index 814aa2bf..063c047a 100644 --- a/Package.swift +++ b/Package.swift @@ -21,12 +21,16 @@ let package = Package( targets: ["StructuredQueriesCore"] ), .library( - name: "StructuredQueriesTestSupport", - targets: ["StructuredQueriesTestSupport"] + name: "StructuredQueriesSQLite", + targets: ["StructuredQueriesSQLite"] ), .library( - name: "_StructuredQueriesSQLite", - targets: ["StructuredQueriesSQLite"] + name: "StructuredQueriesSQLiteCore", + targets: ["StructuredQueriesSQLiteCore"] + ), + .library( + name: "StructuredQueriesTestSupport", + targets: ["StructuredQueriesTestSupport"] ), ], traits: [ @@ -46,6 +50,13 @@ let package = Package( .package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"602.0.0"), ], targets: [ + .target( + name: "StructuredQueries", + dependencies: [ + "StructuredQueriesCore", + "StructuredQueriesMacros", + ] + ), .target( name: "StructuredQueriesCore", dependencies: [ @@ -58,13 +69,6 @@ let package = Package( ], exclude: ["Symbolic Links/README.md"] ), - .target( - name: "StructuredQueries", - dependencies: [ - "StructuredQueriesCore", - "StructuredQueriesMacros", - ] - ), .macro( name: "StructuredQueriesMacros", dependencies: [ @@ -73,12 +77,30 @@ let package = Package( ], exclude: ["Symbolic Links/README.md"] ), + .target( name: "StructuredQueriesSQLite", dependencies: [ - "StructuredQueries" + "StructuredQueries", + "StructuredQueriesSQLiteCore", + "StructuredQueriesSQLiteMacros", + ] + ), + .target( + name: "StructuredQueriesSQLiteCore", + dependencies: [ + "StructuredQueriesCore", + .product(name: "IssueReporting", package: "xctest-dynamic-overlay") + ] + ), + .macro( + name: "StructuredQueriesSQLiteMacros", + dependencies: [ + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), ] ), + .target( name: "StructuredQueriesTestSupport", dependencies: [ @@ -90,8 +112,8 @@ let package = Package( .testTarget( name: "StructuredQueriesMacrosTests", dependencies: [ - "StructuredQueries", "StructuredQueriesMacros", + "StructuredQueriesSQLiteMacros", .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), .product(name: "MacroTesting", package: "swift-macro-testing"), ] @@ -102,11 +124,19 @@ let package = Package( "StructuredQueries", "StructuredQueriesSQLite", "StructuredQueriesTestSupport", + "_StructuredQueriesSQLite", .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), ] ), + + .target( + name: "_StructuredQueriesSQLite", + dependencies: [ + "StructuredQueriesSQLite" + ] + ), ], swiftLanguageModes: [.v6] ) @@ -128,14 +158,14 @@ for index in package.targets.indices { #if !canImport(Darwin) package.targets.append( .systemLibrary( - name: "StructuredQueriesSQLite3", + name: "_StructuredQueriesSQLite3", providers: [.apt(["libsqlite3-dev"])] ) ) for index in package.targets.indices { - if package.targets[index].name == "StructuredQueriesSQLite" { - package.targets[index].dependencies.append("StructuredQueriesSQLite3") + if package.targets[index].name == "_StructuredQueriesSQLite" { + package.targets[index].dependencies.append("_StructuredQueriesSQLite3") } } #endif diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index fddcde8c..d3f83fcc 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -25,9 +25,17 @@ let package = Package( targets: ["StructuredQueriesTestSupport"] ), .library( - name: "_StructuredQueriesSQLite", + name: "StructuredQueriesSQLite", targets: ["StructuredQueriesSQLite"] ), + .library( + name: "StructuredQueriesSQLiteCore", + targets: ["StructuredQueriesSQLiteCore"] + ), + .library( + name: "StructuredQueriesTestSupport", + targets: ["StructuredQueriesTestSupport"] + ), ], dependencies: [ .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), @@ -38,13 +46,6 @@ let package = Package( .package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"602.0.0"), ], targets: [ - .target( - name: "StructuredQueriesCore", - dependencies: [ - .product(name: "IssueReporting", package: "xctest-dynamic-overlay") - ], - exclude: ["Symbolic Links/README.md"] - ), .target( name: "StructuredQueries", dependencies: [ @@ -52,6 +53,13 @@ let package = Package( "StructuredQueriesMacros", ] ), + .target( + name: "StructuredQueriesCore", + dependencies: [ + .product(name: "IssueReporting", package: "xctest-dynamic-overlay") + ], + exclude: ["Symbolic Links/README.md"] + ), .macro( name: "StructuredQueriesMacros", dependencies: [ @@ -60,12 +68,30 @@ let package = Package( ], exclude: ["Symbolic Links/README.md"] ), + .target( name: "StructuredQueriesSQLite", dependencies: [ - "StructuredQueries" + "StructuredQueries", + "StructuredQueriesSQLiteCore", + "StructuredQueriesSQLiteMacros", + ] + ), + .target( + name: "StructuredQueriesSQLiteCore", + dependencies: [ + "StructuredQueriesCore", + .product(name: "IssueReporting", package: "xctest-dynamic-overlay") + ] + ), + .macro( + name: "StructuredQueriesSQLiteMacros", + dependencies: [ + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), ] ), + .target( name: "StructuredQueriesTestSupport", dependencies: [ @@ -74,11 +100,12 @@ let package = Package( .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), ] ), + .testTarget( name: "StructuredQueriesMacrosTests", dependencies: [ - "StructuredQueries", "StructuredQueriesMacros", + "StructuredQueriesSQLiteMacros", .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), .product(name: "MacroTesting", package: "swift-macro-testing"), ] @@ -87,13 +114,20 @@ let package = Package( name: "StructuredQueriesTests", dependencies: [ "StructuredQueries", - "StructuredQueriesSQLite", "StructuredQueriesTestSupport", + "_StructuredQueriesSQLite", .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), ] ), + + .target( + name: "_StructuredQueriesSQLite", + dependencies: [ + "StructuredQueriesSQLite" + ] + ), ], swiftLanguageModes: [.v6] ) @@ -115,14 +149,14 @@ for index in package.targets.indices { #if !os(Darwin) package.targets.append( .systemLibrary( - name: "StructuredQueriesSQLite3", + name: "_StructuredQueriesSQLite3", providers: [.apt(["libsqlite3-dev"])] ) ) for index in package.targets.indices { - if package.targets[index].name == "StructuredQueriesSQLite" { - package.targets[index].dependencies.append("StructuredQueriesSQLite3") + if package.targets[index].name == "_StructuredQueriesSQLite" { + package.targets[index].dependencies.append("_StructuredQueriesSQLite3") } } #endif diff --git a/Sources/StructuredQueriesCore/Optional.swift b/Sources/StructuredQueriesCore/Optional.swift index 85afcd9d..af025eb6 100644 --- a/Sources/StructuredQueriesCore/Optional.swift +++ b/Sources/StructuredQueriesCore/Optional.swift @@ -27,6 +27,10 @@ extension Optional: QueryBindable where Wrapped: QueryBindable { public var queryBinding: QueryBinding { self?.queryBinding ?? .null } + + public init?(queryBinding: QueryBinding) { + self = Wrapped(queryBinding: queryBinding) + } } extension Optional: QueryDecodable where Wrapped: QueryDecodable { diff --git a/Sources/StructuredQueriesCore/QueryBindable.swift b/Sources/StructuredQueriesCore/QueryBindable.swift index 863e7594..6fab9f1f 100644 --- a/Sources/StructuredQueriesCore/QueryBindable.swift +++ b/Sources/StructuredQueriesCore/QueryBindable.swift @@ -9,14 +9,26 @@ public protocol QueryBindable: QueryRepresentable, QueryExpression where QueryVa /// A value that can be bound to a parameter of a SQL statement. var queryBinding: QueryBinding { get } + + /// Initializes a bindable type from a binding. + init?(queryBinding: QueryBinding) } extension QueryBindable { public var queryFragment: QueryFragment { "\(queryBinding)" } + + public init?(queryBinding: QueryBinding) { + guard let queryValue = QueryValue(queryBinding: queryBinding) else { return nil } + self.init(queryBinding: queryValue.queryBinding) + } } extension [UInt8]: QueryBindable, QueryExpression { public var queryBinding: QueryBinding { .blob(self) } + public init?(queryBinding: QueryBinding) { + guard case .blob(let value) = queryBinding else { return nil } + self = value + } } extension Bool: QueryBindable { @@ -25,10 +37,18 @@ extension Bool: QueryBindable { extension Double: QueryBindable { public var queryBinding: QueryBinding { .double(self) } + public init?(queryBinding: QueryBinding) { + guard case .double(let value) = queryBinding else { return nil } + self = value + } } extension Date: QueryBindable { public var queryBinding: QueryBinding { .date(self) } + public init?(queryBinding: QueryBinding) { + guard case .date(let value) = queryBinding else { return nil } + self = value + } } extension Float: QueryBindable { @@ -53,10 +73,18 @@ extension Int32: QueryBindable { extension Int64: QueryBindable { public var queryBinding: QueryBinding { .int(self) } + public init?(queryBinding: QueryBinding) { + guard case .int(let value) = queryBinding else { return nil } + self = value + } } extension String: QueryBindable { public var queryBinding: QueryBinding { .text(self) } + public init?(queryBinding: QueryBinding) { + guard case let .text(value) = queryBinding else { return nil } + self = value + } } extension UInt8: QueryBindable { @@ -83,6 +111,10 @@ extension UInt64: QueryBindable { extension UUID: QueryBindable { public var queryBinding: QueryBinding { .uuid(self) } + public init?(queryBinding: QueryBinding) { + guard case .uuid(let value) = queryBinding else { return nil } + self = value + } } extension DefaultStringInterpolation { diff --git a/Sources/StructuredQueriesSQLite/Documentation.docc/StructuredQueriesSQLite.md b/Sources/StructuredQueriesSQLite/Documentation.docc/StructuredQueriesSQLite.md new file mode 100644 index 00000000..14313d69 --- /dev/null +++ b/Sources/StructuredQueriesSQLite/Documentation.docc/StructuredQueriesSQLite.md @@ -0,0 +1,19 @@ +# ``StructuredQueriesSQLite`` + +Core SQLite extensions to StructuredQueries. + +## Overview + +The core functionality of this library is defined in +[`StructuredQueriesSQLiteCore`](), which this module automatically +exports. + +This module also contains all of the macros that support the core functionality of the library. + +See [`StructuredQueriesSQLiteCore`]() for general library usage. + +## Topics + +### Macros + +- ``DatabaseFunction(_:isDeterministic:)`` diff --git a/Sources/StructuredQueriesSQLite/Exports.swift b/Sources/StructuredQueriesSQLite/Exports.swift new file mode 100644 index 00000000..008993ed --- /dev/null +++ b/Sources/StructuredQueriesSQLite/Exports.swift @@ -0,0 +1,2 @@ +@_exported import StructuredQueries +@_exported import StructuredQueriesSQLiteCore diff --git a/Sources/StructuredQueriesSQLite/Macros.swift b/Sources/StructuredQueriesSQLite/Macros.swift new file mode 100644 index 00000000..4db431e6 --- /dev/null +++ b/Sources/StructuredQueriesSQLite/Macros.swift @@ -0,0 +1,18 @@ +import StructuredQueriesSQLiteCore + +/// Defines and implements a conformance to the ``/StructuredQueriesSQLiteCore/DatabaseFunction`` +/// protocol. +/// +/// - Parameters +/// - name: The function's name. Defaults to the name of the function the macro is applied to. +/// - isDeterministic: Whether or not the function is deterministic (or "pure" or "referentially +/// transparent"), _i.e._ given an input it will always return the same output. +@attached(peer, names: overloaded, prefixed(`$`)) +public macro DatabaseFunction( + _ name: String = "", + isDeterministic: Bool = false +) = + #externalMacro( + module: "StructuredQueriesSQLiteMacros", + type: "DatabaseFunctionMacro" + ) diff --git a/Sources/StructuredQueriesSQLite3/StructuredQueriesSQLite3.h b/Sources/StructuredQueriesSQLite3/StructuredQueriesSQLite3.h deleted file mode 100644 index 8e211139..00000000 --- a/Sources/StructuredQueriesSQLite3/StructuredQueriesSQLite3.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef StructuredQueriesSQLite3 -#define StructuredQueriesSQLite3 -#include -#endif diff --git a/Sources/StructuredQueriesSQLite3/module.modulemap b/Sources/StructuredQueriesSQLite3/module.modulemap deleted file mode 100644 index bbb8d7df..00000000 --- a/Sources/StructuredQueriesSQLite3/module.modulemap +++ /dev/null @@ -1,5 +0,0 @@ -module StructuredQueriesSQLite3 [system] { - link "sqlite3" - header "StructuredQueriesSQLite3.h" - export * -} diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift new file mode 100644 index 00000000..6200897f --- /dev/null +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -0,0 +1,45 @@ +/// A type representing a database function. +/// +/// Don't conform to this protocol directly. Instead, use the `@DatabaseFunction` macro to generate +/// a conformance. +public protocol DatabaseFunction { + /// A type representing the function's arguments. + associatedtype Input + + /// A type representing the function's return value. + associatedtype Output + + /// The name of the function. + var name: String { get } + + /// The number of arguments the function accepts. + var argumentCount: Int? { get } + + /// Whether or not the function is deterministic (or "pure" or "referentially transparent"), + /// _i.e._ given an input it will always return the same output. + var isDeterministic: Bool { get } +} + +public protocol ScalarDatabaseFunction: DatabaseFunction { + /// The function body. Transforms an array of bindings handed to the function into a binding + /// returned to the query. + /// + /// - Parameter arguments: Arguments passed to the database function. + /// - Returns: A value returned from the database function. + func invoke(_ arguments: [QueryBinding]) -> QueryBinding +} + +extension ScalarDatabaseFunction { + /// A function call expression. + /// + /// - Parameter input: Expressions representing the arguments of the function. + /// - Returns: An expression representing the function call. + public func callAsFunction( + _ input: repeat each T + ) -> some QueryExpression + where Input == (repeat (each T).QueryValue) { + SQLQueryExpression( + "\(quote: name)(\(Array(repeat each input).joined(separator: ", ")))" + ) + } +} diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md new file mode 100644 index 00000000..f6a79406 --- /dev/null +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md @@ -0,0 +1,15 @@ +# ``StructuredQueriesSQLiteCore`` + +Core SQLite extensions to StructuredQueries. + +## Overview + +StructuredQueriesSQLite extends StructuredQueries with SQLite functionality, including support for +custom database functions, and more. + +## Topics + +### Custom functions + +- ``DatabaseFunction`` +- ``ScalarDatabaseFunction`` diff --git a/Sources/StructuredQueriesSQLiteCore/Exports.swift b/Sources/StructuredQueriesSQLiteCore/Exports.swift new file mode 100644 index 00000000..320bdbac --- /dev/null +++ b/Sources/StructuredQueriesSQLiteCore/Exports.swift @@ -0,0 +1 @@ +@_exported import StructuredQueriesCore diff --git a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift new file mode 100644 index 00000000..7f2f6068 --- /dev/null +++ b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift @@ -0,0 +1,248 @@ +import SwiftBasicFormat +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +internal import SwiftParser + +public enum DatabaseFunctionMacro {} + +extension DatabaseFunctionMacro: PeerMacro { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: D, + in context: C + ) throws -> [DeclSyntax] { + guard let declaration = declaration.as(FunctionDeclSyntax.self) else { + context.diagnose( + Diagnostic( + node: declaration, + message: MacroExpansionErrorMessage( + "'@DatabaseFunction' must be applied to functions" + ) + ) + ) + return [] + } + + guard declaration.signature.returnClause != nil else { + context.diagnose( + Diagnostic( + node: declaration.signature, + position: declaration.signature.endPositionBeforeTrailingTrivia, + message: MacroExpansionErrorMessage( + "Missing required return type" + ), + fixIt: .replaceChild( + message: MacroExpansionFixItMessage("Insert '-> <#QueryBindable#>'"), + parent: declaration.signature, + replacingChildAt: \.returnClause, + with: ReturnClauseSyntax( + type: IdentifierTypeSyntax(name: "<#QueryBindable#>") + .with(\.leadingTrivia, .space) + .with(\.trailingTrivia, .space) + ) + ) + ) + ) + return [] + } + + let declarationName = declaration.name.trimmedDescription.trimmingBackticks() + var functionName = declarationName + var isDeterministic = false + if case .argumentList(let arguments) = node.arguments { + for argumentIndex in arguments.indices { + let argument = arguments[argumentIndex] + switch argument.label { + case nil: + guard + let string = argument.expression.as(StringLiteralExprSyntax.self)? + .representedLiteralValue + else { + context.diagnose( + Diagnostic( + node: argument.expression, + message: MacroExpansionErrorMessage("Argument must be a non-empty string literal") + ) + ) + return [] + } + functionName = string + + case .some(let label) where label.text == "isDeterministic": + guard + let bool = argument.expression.as(BooleanLiteralExprSyntax.self) + else { + context.diagnose( + Diagnostic( + node: argument.expression, + message: MacroExpansionErrorMessage("Argument must be a boolean literal") + ) + ) + return [] + } + isDeterministic = bool.literal.tokenKind == .keyword(.true) + + case let argument?: + fatalError("Unexpected argument: \(argument)") + } + } + } + + let functionTypeName = context.makeUniqueName(declarationName) + let databaseFunctionName = StringLiteralExprSyntax(content: functionName) + let argumentCount = declaration.signature.parameterClause.parameters.count + + var bodyArguments: [String] = [] + var signature = declaration.signature + var invocationArgumentTypes: [TypeSyntax] = [] + var parameters: [String] = [] + var argumentBindings: [String] = [] + var offset = 0 + for index in signature.parameterClause.parameters.indices { + defer { offset += 1 } + var parameter = signature.parameterClause.parameters[index] + if let ellipsis = parameter.ellipsis { + context.diagnose( + Diagnostic( + node: ellipsis, + message: MacroExpansionErrorMessage("Variadic arguments are not supported") + ) + ) + return [] + } + let type = parameter.type.trimmed + bodyArguments.append("\(type)") + parameter.type = parameter.type.asQueryExpression() + if let defaultValue = parameter.defaultValue, + defaultValue.value.is(NilLiteralExprSyntax.self) + { + parameter.defaultValue?.value = "\(type).none" + } + signature.parameterClause.parameters[index] = parameter + invocationArgumentTypes.append(type) + parameters.append("\(parameter.secondName ?? parameter.firstName)") + argumentBindings.append("let n\(offset) = \(type)(queryBinding: arguments[\(offset)])") + } + var inputType = bodyArguments.joined(separator: ", ") + let bodyReturnClause: String + let outputType: TypeSyntax + if let returnClause = signature.returnClause { + outputType = returnClause.type.trimmed + signature.returnClause?.type = returnClause.type.asQueryExpression() + bodyReturnClause = " \(returnClause.trimmedDescription)" + } else { + outputType = "Void" + bodyReturnClause = " -> Void" + } + let bodyType = """ + (\(inputType))\ + \(declaration.signature.effectSpecifiers?.trimmedDescription ?? "")\ + \(bodyReturnClause) + """ + // TODO: Diagnose 'asyncClause'? + signature.effectSpecifiers?.throwsClause = nil + + var invocationBody = """ + body(\(argumentBindings.indices.map { "n\($0)" }.joined(separator: ", "))).queryBinding + """ + if declaration.signature.effectSpecifiers?.throwsClause != nil { + invocationBody = """ + do { + return try \(invocationBody) + } catch { + return .invalid(error) + } + """ + } else { + invocationBody = "return \(invocationBody)" + } + + var attributes = declaration.attributes + if let index = attributes.firstIndex(where: { + $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text + == "DatabaseFunction" + }) { + attributes.remove(at: index) + } + var access: TokenSyntax? + var `static`: TokenSyntax? + for modifier in declaration.modifiers { + switch modifier.name.tokenKind { + case .keyword(.private), .keyword(.internal), .keyword(.package), .keyword(.public): + access = modifier.name + case .keyword(.static): + `static` = modifier.name + default: + continue + } + } + inputType = bodyArguments.count == 1 ? inputType : "(\(inputType))" + + return [ + """ + \(attributes)\(access)\(`static`)var $\(raw: declarationName): \(functionTypeName) { + \(functionTypeName)(\(declaration.name.trimmed)) + } + """, + """ + \(attributes)\(access)struct \(functionTypeName): \ + StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = \(raw: inputType) + public typealias Output = \(outputType) + public let name = \(databaseFunctionName) + public let argumentCount: Int? = \(raw: argumentCount) + public let isDeterministic = \(raw: isDeterministic) + public let body: \(raw: bodyType) + public init(_ body: @escaping \(raw: bodyType)) { + self.body = body + } + public func callAsFunction\(signature.trimmed) { + StructuredQueriesCore.SQLQueryExpression( + "\\(quote: name)(\(raw: parameters.map { "\\(\($0))" }.joined(separator: ", ")))" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount\ + \(raw: argumentBindings.map { ", \($0)" }.joined()) \ + else { + return .invalid(InvalidInvocation()) + } + \(raw: invocationBody) + } + private struct InvalidInvocation: Error {} + } + """, + ] + } +} + +extension ExprSyntax { + fileprivate var isNonEmptyStringLiteral: Bool { + guard let literal = self.as(StringLiteralExprSyntax.self)?.representedLiteralValue + else { return false } + return !literal.isEmpty + } +} + +extension String { + fileprivate func trimmingBackticks() -> String { + var result = self[...] + if result.first == "`" && result.dropFirst().last == "`" { + result = result.dropFirst().dropLast() + } + return String(result) + } +} + +extension TypeSyntaxProtocol { + fileprivate func asQueryExpression(any: Bool = false) -> TypeSyntax { + """ + \(raw: `any` ? "any" : "some") \ + StructuredQueriesCore.QueryExpression<\(trimmed)>\(trailingTrivia) + """ + } +} diff --git a/Sources/StructuredQueriesSQLiteMacros/Plugin.swift b/Sources/StructuredQueriesSQLiteMacros/Plugin.swift new file mode 100644 index 00000000..14e82bf3 --- /dev/null +++ b/Sources/StructuredQueriesSQLiteMacros/Plugin.swift @@ -0,0 +1,9 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct StructuredQueriesPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + DatabaseFunctionMacro.self, + ] +} diff --git a/Sources/StructuredQueriesSQLite/Database.swift b/Sources/_StructuredQueriesSQLite/Database.swift similarity index 95% rename from Sources/StructuredQueriesSQLite/Database.swift rename to Sources/_StructuredQueriesSQLite/Database.swift index e3dced2c..7a519b7c 100644 --- a/Sources/StructuredQueriesSQLite/Database.swift +++ b/Sources/_StructuredQueriesSQLite/Database.swift @@ -1,16 +1,18 @@ import Foundation -import StructuredQueries - -#if canImport(Darwin) - import SQLite3 -#else - import StructuredQueriesSQLite3 -#endif public struct Database { @usableFromInline let storage: Storage + public var handle: OpaquePointer { + switch storage { + case .owned(let storage): + return storage.handle + case .unowned(let handle): + return handle + } + } + public init(_ ptr: OpaquePointer) { self.storage = .unowned(ptr) } @@ -190,7 +192,7 @@ public struct Database { private struct InvalidBindingError: Error {} -private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) +let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) @usableFromInline struct SQLiteError: LocalizedError { diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift new file mode 100644 index 00000000..65a18caa --- /dev/null +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -0,0 +1,88 @@ +import Foundation + +extension ScalarDatabaseFunction { + public func install(_ db: OpaquePointer) { + let box = Unmanaged.passRetained(ScalarDatabaseFunctionBox(self)).toOpaque() + sqlite3_create_function_v2( + db, + name, + Int32(argumentCount ?? -1), + SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), + box, + { context, argumentCount, arguments in + Unmanaged + .fromOpaque(sqlite3_user_data(context)) + .takeUnretainedValue() + .function + .invoke([QueryBinding](argumentCount: argumentCount, arguments: arguments)) + .result(db: context) + }, + nil, + nil, + { context in + guard let context else { return } + Unmanaged.fromOpaque(context).release() + } + ) + } +} + +private final class ScalarDatabaseFunctionBox { + let function: any ScalarDatabaseFunction + init(_ function: some ScalarDatabaseFunction) { + self.function = function + } +} + +extension [QueryBinding] { + fileprivate init(argumentCount: Int32, arguments: UnsafeMutablePointer?) { + self = (0.. +#endif diff --git a/Sources/_StructuredQueriesSQLite3/module.modulemap b/Sources/_StructuredQueriesSQLite3/module.modulemap new file mode 100644 index 00000000..f54a1234 --- /dev/null +++ b/Sources/_StructuredQueriesSQLite3/module.modulemap @@ -0,0 +1,5 @@ +module _StructuredQueriesSQLite3 [system] { + link "sqlite3" + header "_StructuredQueriesSQLite3.h" + export * +} diff --git a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift new file mode 100644 index 00000000..f9632612 --- /dev/null +++ b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift @@ -0,0 +1,841 @@ +import MacroTesting +import StructuredQueriesSQLiteMacros +import Testing + +extension SnapshotTests { + @MainActor + @Suite struct DatabaseFunctionMacroTests { + @Test func basics() { + assertMacro { + """ + @DatabaseFunction + func currentDate() -> Date { + Date() + } + """ + } expansion: { + #""" + func currentDate() -> Date { + Date() + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Date + public let name = "currentDate" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () -> Date + public init(_ body: @escaping () -> Date) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + return body().queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func customName() { + assertMacro { + """ + @DatabaseFunction("current_date") + func currentDate() -> Date { + Date() + } + """ + } expansion: { + #""" + func currentDate() -> Date { + Date() + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Date + public let name = "current_date" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () -> Date + public init(_ body: @escaping () -> Date) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + return body().queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func customDeterminism() { + assertMacro { + """ + @DatabaseFunction(isDeterministic: true) + func fortyTwo() -> Int { + 42 + } + """ + } expansion: { + #""" + func fortyTwo() -> Int { + 42 + } + + var $fortyTwo: __macro_local_8fortyTwofMu_ { + __macro_local_8fortyTwofMu_(fortyTwo) + } + + struct __macro_local_8fortyTwofMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Int + public let name = "fortyTwo" + public let argumentCount: Int? = 0 + public let isDeterministic = true + public let body: () -> Int + public init(_ body: @escaping () -> Int) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + return body().queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func unnamedArgument() { + assertMacro { + """ + @DatabaseFunction + func currentDate(_ format: String) -> Date? { + dateFormatter.date(from: format) + } + """ + } expansion: { + #""" + func currentDate(_ format: String) -> Date? { + dateFormatter.date(from: format) + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = String + public typealias Output = Date? + public let name = "currentDate" + public let argumentCount: Int? = 1 + public let isDeterministic = false + public let body: (String) -> Date? + public init(_ body: @escaping (String) -> Date?) { + self.body = body + } + public func callAsFunction(_ format: some StructuredQueriesCore.QueryExpression) -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)(\(format))" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount, let n0 = String(queryBinding: arguments[0]) else { + return .invalid(InvalidInvocation()) + } + return body(n0).queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func namedArgument() { + assertMacro { + """ + @DatabaseFunction + func currentDate(format: String) -> Date? { + dateFormatter.date(from: format) + } + """ + } expansion: { + #""" + func currentDate(format: String) -> Date? { + dateFormatter.date(from: format) + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = String + public typealias Output = Date? + public let name = "currentDate" + public let argumentCount: Int? = 1 + public let isDeterministic = false + public let body: (String) -> Date? + public init(_ body: @escaping (String) -> Date?) { + self.body = body + } + public func callAsFunction(format: some StructuredQueriesCore.QueryExpression) -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)(\(format))" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount, let n0 = String(queryBinding: arguments[0]) else { + return .invalid(InvalidInvocation()) + } + return body(n0).queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func unnamedArgumentDefault() { + assertMacro { + """ + @DatabaseFunction + func currentDate(_ format: String = "") -> Date? { + dateFormatter.date(from: format) + } + """ + } expansion: { + #""" + func currentDate(_ format: String = "") -> Date? { + dateFormatter.date(from: format) + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = String + public typealias Output = Date? + public let name = "currentDate" + public let argumentCount: Int? = 1 + public let isDeterministic = false + public let body: (String) -> Date? + public init(_ body: @escaping (String) -> Date?) { + self.body = body + } + public func callAsFunction(_ format: some StructuredQueriesCore.QueryExpression = "") -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)(\(format))" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount, let n0 = String(queryBinding: arguments[0]) else { + return .invalid(InvalidInvocation()) + } + return body(n0).queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func namedArgumentDefault() { + assertMacro { + """ + @DatabaseFunction + func currentDate(format: String = "") -> Date? { + dateFormatter.date(from: format) + } + """ + } expansion: { + #""" + func currentDate(format: String = "") -> Date? { + dateFormatter.date(from: format) + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = String + public typealias Output = Date? + public let name = "currentDate" + public let argumentCount: Int? = 1 + public let isDeterministic = false + public let body: (String) -> Date? + public init(_ body: @escaping (String) -> Date?) { + self.body = body + } + public func callAsFunction(format: some StructuredQueriesCore.QueryExpression = "") -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)(\(format))" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount, let n0 = String(queryBinding: arguments[0]) else { + return .invalid(InvalidInvocation()) + } + return body(n0).queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func multipleArguments() { + assertMacro { + """ + @DatabaseFunction + func concat(first: String = "", second: String = "") -> String { + first + second + } + """ + } expansion: { + #""" + func concat(first: String = "", second: String = "") -> String { + first + second + } + + var $concat: __macro_local_6concatfMu_ { + __macro_local_6concatfMu_(concat) + } + + struct __macro_local_6concatfMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = (String, String) + public typealias Output = String + public let name = "concat" + public let argumentCount: Int? = 2 + public let isDeterministic = false + public let body: (String, String) -> String + public init(_ body: @escaping (String, String) -> String) { + self.body = body + } + public func callAsFunction(first: some StructuredQueriesCore.QueryExpression = "", second: some StructuredQueriesCore.QueryExpression = "") -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)(\(first), \(second))" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount, let n0 = String(queryBinding: arguments[0]), let n1 = String(queryBinding: arguments[1]) else { + return .invalid(InvalidInvocation()) + } + return body(n0, n1).queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func wrongDeclDiagnostic() { + assertMacro { + """ + @DatabaseFunction + struct Foo { + } + """ + } diagnostics: { + """ + @DatabaseFunction + ╰─ 🛑 '@DatabaseFunction' must be applied to functions + struct Foo { + } + """ + } + } + + @Test func unnamedArgumentNilDefault() { + assertMacro { + """ + @DatabaseFunction + func currentDate(_ format: String? = nil) -> Date? { + dateFormatter.date(from: format) + } + """ + } expansion: { + #""" + func currentDate(_ format: String? = nil) -> Date? { + dateFormatter.date(from: format) + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = String? + public typealias Output = Date? + public let name = "currentDate" + public let argumentCount: Int? = 1 + public let isDeterministic = false + public let body: (String?) -> Date? + public init(_ body: @escaping (String?) -> Date?) { + self.body = body + } + public func callAsFunction(_ format: some StructuredQueriesCore.QueryExpression = String?.none) -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)(\(format))" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount, let n0 = String?(queryBinding: arguments[0]) else { + return .invalid(InvalidInvocation()) + } + return body(n0).queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func untypedThrows() { + assertMacro { + """ + @DatabaseFunction + func currentDate() throws -> Date { + Date() + } + """ + } expansion: { + #""" + func currentDate() throws -> Date { + Date() + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Date + public let name = "currentDate" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () throws -> Date + public init(_ body: @escaping () throws -> Date) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + do { + return try body().queryBinding + } catch { + return .invalid(error) + } + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func typedThrows() { + assertMacro { + """ + @DatabaseFunction + func currentDate() throws(MyError) -> Date { + Date() + } + """ + } expansion: { + #""" + func currentDate() throws(MyError) -> Date { + Date() + } + + var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Date + public let name = "currentDate" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () throws(MyError) -> Date + public init(_ body: @escaping () throws(MyError) -> Date) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + do { + return try body().queryBinding + } catch { + return .invalid(error) + } + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func publicAccess() { + assertMacro { + """ + @DatabaseFunction + public func currentDate() -> Date { + Date() + } + """ + } expansion: { + #""" + public func currentDate() -> Date { + Date() + } + + public var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + public struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Date + public let name = "currentDate" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () -> Date + public init(_ body: @escaping () -> Date) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + return body().queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func staticAccess() { + assertMacro { + """ + @DatabaseFunction + static func currentDate() -> Date { + Date() + } + """ + } expansion: { + #""" + static func currentDate() -> Date { + Date() + } + + static var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Date + public let name = "currentDate" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () -> Date + public init(_ body: @escaping () -> Date) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + return body().queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + // TODO: Get working + @Test func variadic() { + assertMacro { + """ + @DatabaseFunction + func concat(_ strings: String...) -> String { + strings.joined() + } + """ + } diagnostics: { + """ + @DatabaseFunction + func concat(_ strings: String...) -> String { + ┬── + ╰─ 🛑 Variadic arguments are not supported + strings.joined() + } + """ + } + } + + @Test func availability() { + assertMacro { + """ + @available(*, unavailable) + @DatabaseFunction + func currentDate() -> Date { + Date() + } + """ + } expansion: { + #""" + @available(*, unavailable) + func currentDate() -> Date { + Date() + } + + @available(*, unavailable) var $currentDate: __macro_local_11currentDatefMu_ { + __macro_local_11currentDatefMu_(currentDate) + } + + @available(*, unavailable) struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Date + public let name = "currentDate" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () -> Date + public init(_ body: @escaping () -> Date) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + return body().queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func backticks() { + assertMacro { + """ + @DatabaseFunction + public func `default`() -> Int { + 42 + } + """ + } expansion: { + #""" + public func `default`() -> Int { + 42 + } + + public var $default: __macro_local_7defaultfMu_ { + __macro_local_7defaultfMu_(`default`) + } + + public struct __macro_local_7defaultfMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = Int + public let name = "default" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () -> Int + public init(_ body: @escaping () -> Int) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + return body().queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + + @Test func returnTypeDiagnostic() { + assertMacro { + """ + @DatabaseFunction + public func void() { + print("...") + } + """ + } diagnostics: { + """ + @DatabaseFunction + public func void() { + ──┬ + ╰─ 🛑 Missing required return type + ✏️ Insert '-> <#QueryBindable#>' + print("...") + } + """ + } fixes: { + """ + @DatabaseFunction + public func void() -> <#QueryBindable#> { + print("...") + } + """ + } expansion: { + #""" + public func void() -> <#QueryBindable#> { + print("...") + } + + public var $void: __macro_local_4voidfMu_ { + __macro_local_4voidfMu_(void) + } + + public struct __macro_local_4voidfMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = () + public typealias Output = <#QueryBindable#> + public let name = "void" + public let argumentCount: Int? = 0 + public let isDeterministic = false + public let body: () -> <#QueryBindable#> + public init(_ body: @escaping () -> <#QueryBindable#>) { + self.body = body + } + public func callAsFunction() -> some StructuredQueriesCore.QueryExpression<<#QueryBindable#>> { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: name)()" + ) + } + public func invoke( + _ arguments: [StructuredQueriesCore.QueryBinding] + ) -> StructuredQueriesCore.QueryBinding { + guard arguments.count == argumentCount else { + return .invalid(InvalidInvocation()) + } + return body().queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } + } +} diff --git a/Tests/StructuredQueriesMacrosTests/Support/SnapshotTests.swift b/Tests/StructuredQueriesMacrosTests/Support/SnapshotTests.swift index d174079e..f4542224 100644 --- a/Tests/StructuredQueriesMacrosTests/Support/SnapshotTests.swift +++ b/Tests/StructuredQueriesMacrosTests/Support/SnapshotTests.swift @@ -1,7 +1,7 @@ import MacroTesting import SnapshotTesting -import StructuredQueries import StructuredQueriesMacros +import StructuredQueriesSQLiteMacros import Testing @MainActor @@ -12,6 +12,7 @@ import Testing "_Draft": TableMacro.self, "bind": BindMacro.self, "Column": ColumnMacro.self, + "DatabaseFunction": DatabaseFunctionMacro.self, "Ephemeral": EphemeralMacro.self, "Selection": SelectionMacro.self, "sql": SQLMacro.self, @@ -20,9 +21,3 @@ import Testing record: .failed ) ) struct SnapshotTests {} - -extension Snapshotting where Value: QueryExpression { - static var sql: Snapshotting { - SimplySnapshotting.lines.pullback(\.queryFragment.debugDescription) - } -} diff --git a/Tests/StructuredQueriesTests/BindingTests.swift b/Tests/StructuredQueriesTests/BindingTests.swift index ab81c005..f8944901 100644 --- a/Tests/StructuredQueriesTests/BindingTests.swift +++ b/Tests/StructuredQueriesTests/BindingTests.swift @@ -2,9 +2,9 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import StructuredQueriesTestSupport import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct BindingTests { diff --git a/Tests/StructuredQueriesTests/CaseTests.swift b/Tests/StructuredQueriesTests/CaseTests.swift index d166d768..e62eda02 100644 --- a/Tests/StructuredQueriesTests/CaseTests.swift +++ b/Tests/StructuredQueriesTests/CaseTests.swift @@ -2,9 +2,9 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import StructuredQueriesTestSupport import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct CaseTests { diff --git a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift index 4ec67b1a..612d1d83 100644 --- a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift +++ b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift @@ -2,8 +2,8 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct CommonTableExpressionTests { diff --git a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift new file mode 100644 index 00000000..a5fc6372 --- /dev/null +++ b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift @@ -0,0 +1,127 @@ +import Dependencies +import Foundation +import InlineSnapshotTesting +import SQLite3 +import StructuredQueries +import StructuredQueriesSQLite +import StructuredQueriesTestSupport +import Testing +import _StructuredQueriesSQLite + +extension SnapshotTests { + @Suite struct DatabaseFunctionTests { + @DatabaseFunction + func isEnabled() -> Bool { + true + } + @Test func customIsEnabled() { + @Dependency(\.defaultDatabase) var database + $isEnabled.install(database.handle) + assertQuery( + Values($isEnabled()) + ) { + """ + SELECT "isEnabled"() + """ + } results: { + """ + ┌──────┐ + │ true │ + └──────┘ + """ + } + } + + @DatabaseFunction + func dateTime(_ format: String? = nil) -> Date? { + Date(timeIntervalSince1970: 0) + } + @Test func customDateTime() { + @Dependency(\.defaultDatabase) var database + $dateTime.install(database.handle) + assertQuery( + Values($dateTime()) + ) { + """ + SELECT "dateTime"(NULL) + """ + } results: { + """ + ┌────────────────────────────────┐ + │ Date(1970-01-01T00:00:00.000Z) │ + └────────────────────────────────┘ + """ + } + } + + @DatabaseFunction + func concat(first: String = "", second: String = "") -> String { + first + second + } + @Test func customConcat() { + @Dependency(\.defaultDatabase) var database + $concat.install(database.handle) + assertQuery( + Values($concat(first: "foo", second: "bar")) + ) { + """ + SELECT "concat"('foo', 'bar') + """ + } results: { + """ + ┌──────────┐ + │ "foobar" │ + └──────────┘ + """ + } + } + + @Test func erasedConcat() { + @Dependency(\.defaultDatabase) var database + $concat.install(database.handle) + assertQuery( + Values($concat("foo", "bar")) + ) { + """ + SELECT "concat"('foo', 'bar') + """ + } results: { + """ + ┌──────────┐ + │ "foobar" │ + └──────────┘ + """ + } + } + + @DatabaseFunction + func throwing() throws -> String { + struct Failure: LocalizedError { + var errorDescription: String? { + "Oops!" + } + } + throw Failure() + } + @Test func customThrowing() { + @Dependency(\.defaultDatabase) var database + $throwing.install(database.handle) + assertQuery( + Values($throwing()) + ) { + """ + SELECT "throwing"() + """ + } results: { + """ + Oops! + """ + } + } + + @DatabaseFunction(isDeterministic: true) + func `default`() -> Int { + 42 + } + } +} diff --git a/Tests/StructuredQueriesTests/DecodingTests.swift b/Tests/StructuredQueriesTests/DecodingTests.swift index 2b2036d5..4d2f012d 100644 --- a/Tests/StructuredQueriesTests/DecodingTests.swift +++ b/Tests/StructuredQueriesTests/DecodingTests.swift @@ -1,7 +1,7 @@ import Foundation import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { struct DecodingTests { diff --git a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift index 24a84ef0..afd5fbbb 100644 --- a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift +++ b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift @@ -2,8 +2,8 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @MainActor diff --git a/Tests/StructuredQueriesTests/KitchenSinkTests.swift b/Tests/StructuredQueriesTests/KitchenSinkTests.swift index 74709c76..720416fa 100644 --- a/Tests/StructuredQueriesTests/KitchenSinkTests.swift +++ b/Tests/StructuredQueriesTests/KitchenSinkTests.swift @@ -2,8 +2,8 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @MainActor diff --git a/Tests/StructuredQueriesTests/LiveTests.swift b/Tests/StructuredQueriesTests/LiveTests.swift index 62d2d79e..a2ab4bb7 100644 --- a/Tests/StructuredQueriesTests/LiveTests.swift +++ b/Tests/StructuredQueriesTests/LiveTests.swift @@ -1,8 +1,8 @@ import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct LiveTests { diff --git a/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift b/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift index 7b8554b2..e5615c38 100644 --- a/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift +++ b/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift @@ -2,8 +2,8 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { struct PrimaryKeyedTableTests { diff --git a/Tests/StructuredQueriesTests/Support/AssertQuery.swift b/Tests/StructuredQueriesTests/Support/AssertQuery.swift index 77f546cb..b34fdcd4 100644 --- a/Tests/StructuredQueriesTests/Support/AssertQuery.swift +++ b/Tests/StructuredQueriesTests/Support/AssertQuery.swift @@ -1,7 +1,7 @@ import Dependencies import StructuredQueries -import StructuredQueriesSQLite import StructuredQueriesTestSupport +import _StructuredQueriesSQLite func assertQuery( _ query: S, diff --git a/Tests/StructuredQueriesTests/Support/Schema.swift b/Tests/StructuredQueriesTests/Support/Schema.swift index 360f4507..325025a6 100644 --- a/Tests/StructuredQueriesTests/Support/Schema.swift +++ b/Tests/StructuredQueriesTests/Support/Schema.swift @@ -1,7 +1,7 @@ import Dependencies import Foundation import StructuredQueries -import StructuredQueriesSQLite +import _StructuredQueriesSQLite @Table struct RemindersList: Codable, Equatable, Identifiable { diff --git a/Tests/StructuredQueriesTests/TableTests.swift b/Tests/StructuredQueriesTests/TableTests.swift index 9fc77808..2d6db820 100644 --- a/Tests/StructuredQueriesTests/TableTests.swift +++ b/Tests/StructuredQueriesTests/TableTests.swift @@ -2,8 +2,8 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct TableTests { diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift index 49071dce..56f6f796 100644 --- a/Tests/StructuredQueriesTests/TriggersTests.swift +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -2,8 +2,8 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct TriggersTests { diff --git a/Tests/StructuredQueriesTests/UpdateTests.swift b/Tests/StructuredQueriesTests/UpdateTests.swift index f16f08d4..83d7f066 100644 --- a/Tests/StructuredQueriesTests/UpdateTests.swift +++ b/Tests/StructuredQueriesTests/UpdateTests.swift @@ -2,9 +2,9 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import StructuredQueriesTestSupport import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct UpdateTests { diff --git a/Tests/StructuredQueriesTests/WhereTests.swift b/Tests/StructuredQueriesTests/WhereTests.swift index 57357646..9425d6bf 100644 --- a/Tests/StructuredQueriesTests/WhereTests.swift +++ b/Tests/StructuredQueriesTests/WhereTests.swift @@ -2,8 +2,8 @@ import Dependencies import Foundation import InlineSnapshotTesting import StructuredQueries -import StructuredQueriesSQLite import Testing +import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct WhereTests {