From d5956154046cb10b15f4986680405b1b77d7256f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 25 Aug 2025 16:19:33 -0700 Subject: [PATCH 01/20] wip --- Sources/StructuredQueries/Exports.swift | 87 +++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/Sources/StructuredQueries/Exports.swift b/Sources/StructuredQueries/Exports.swift index 320bdbac..4f492be9 100644 --- a/Sources/StructuredQueries/Exports.swift +++ b/Sources/StructuredQueries/Exports.swift @@ -1 +1,88 @@ @_exported import StructuredQueriesCore + +// --- +import Foundation + +// @CustomFunction // @DatabaseFunction ? @ScalarFunction (_vs._ @AggregateFunction?) +func dateTime(_ format: String? = nil) -> Date { + Date() +} + +// Macro expansion: +@available(macOS 14, *) +@_disfavoredOverload // Or can/should this be applied the the above? +func dateTime( + _ format: some QueryExpression = String?.none +) -> some QueryExpression { + _$dateTime(format) +} + +@available(macOS 14, *) +var _$dateTime: CustomFunction { + CustomFunction("dateTime", isDeterministic: false, body: dateTime(_:)) +} +// --- + +import SQLite3 + +@available(macOS 14, *) +struct CustomFunction { + let name: String + let isDeterministic: Bool +// private let body: Body + + init( + _ name: String, + isDeterministic: Bool, + body: @escaping (repeat each Input) -> Output + ) { + self.name = name + self.isDeterministic = isDeterministic +// self.body = Body(body) + } + + func callAsFunction(_ input: repeat each T) -> SQLQueryExpression + where repeat each T: QueryExpression { + var arguments: [QueryFragment] = [] + for input in repeat each input { + arguments.append(input.queryFragment) + } + return SQLQueryExpression("\(quote: name)(\(arguments.joined(separator: ", "))") + } + +// func install(_ db: OpaquePointer) { +// // TODO: Should this be `-1`? +// var count: Int32 = 0 +// for _ in repeat (each Input).self { +// count += 1 +// } +// let body = Unmanaged.passRetained(body).toOpaque() +// sqlite3_create_function_v2( +// db, +// name, +// count, +// SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), +// body, +// { ctx, argc, argv in +//// let body = Unmanaged +//// .fromOpaque(sqlite3_user_data(ctx)) +//// .takeUnretainedValue() +// }, +// nil, +// nil, +// { ctx in +//// Unmanaged.fromOpaque(body).release() +// } +// ) +// } +} + + +private final class Body { + let body: ([Any]) -> Any + init(_ body: @escaping (repeat each Input) -> Output) { + fatalError() + } +} + + From e325781689e1ca1537c9da6d347b1ab6dec38df8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 27 Aug 2025 00:09:56 -0700 Subject: [PATCH 02/20] wip --- Sources/StructuredQueries/Exports.swift | 87 -------- Sources/StructuredQueriesCore/Optional.swift | 4 + .../StructuredQueriesCore/QueryBindable.swift | 31 +++ .../StructuredQueriesSQLite/Database.swift | 9 + .../CustomFunctionTests.swift | 186 ++++++++++++++++++ 5 files changed, 230 insertions(+), 87 deletions(-) create mode 100644 Tests/StructuredQueriesTests/CustomFunctionTests.swift diff --git a/Sources/StructuredQueries/Exports.swift b/Sources/StructuredQueries/Exports.swift index 4f492be9..320bdbac 100644 --- a/Sources/StructuredQueries/Exports.swift +++ b/Sources/StructuredQueries/Exports.swift @@ -1,88 +1 @@ @_exported import StructuredQueriesCore - -// --- -import Foundation - -// @CustomFunction // @DatabaseFunction ? @ScalarFunction (_vs._ @AggregateFunction?) -func dateTime(_ format: String? = nil) -> Date { - Date() -} - -// Macro expansion: -@available(macOS 14, *) -@_disfavoredOverload // Or can/should this be applied the the above? -func dateTime( - _ format: some QueryExpression = String?.none -) -> some QueryExpression { - _$dateTime(format) -} - -@available(macOS 14, *) -var _$dateTime: CustomFunction { - CustomFunction("dateTime", isDeterministic: false, body: dateTime(_:)) -} -// --- - -import SQLite3 - -@available(macOS 14, *) -struct CustomFunction { - let name: String - let isDeterministic: Bool -// private let body: Body - - init( - _ name: String, - isDeterministic: Bool, - body: @escaping (repeat each Input) -> Output - ) { - self.name = name - self.isDeterministic = isDeterministic -// self.body = Body(body) - } - - func callAsFunction(_ input: repeat each T) -> SQLQueryExpression - where repeat each T: QueryExpression { - var arguments: [QueryFragment] = [] - for input in repeat each input { - arguments.append(input.queryFragment) - } - return SQLQueryExpression("\(quote: name)(\(arguments.joined(separator: ", "))") - } - -// func install(_ db: OpaquePointer) { -// // TODO: Should this be `-1`? -// var count: Int32 = 0 -// for _ in repeat (each Input).self { -// count += 1 -// } -// let body = Unmanaged.passRetained(body).toOpaque() -// sqlite3_create_function_v2( -// db, -// name, -// count, -// SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), -// body, -// { ctx, argc, argv in -//// let body = Unmanaged -//// .fromOpaque(sqlite3_user_data(ctx)) -//// .takeUnretainedValue() -// }, -// nil, -// nil, -// { ctx in -//// Unmanaged.fromOpaque(body).release() -// } -// ) -// } -} - - -private final class Body { - let body: ([Any]) -> Any - init(_ body: @escaping (repeat each Input) -> Output) { - fatalError() - } -} - - diff --git a/Sources/StructuredQueriesCore/Optional.swift b/Sources/StructuredQueriesCore/Optional.swift index 153e053c..0aa05b2f 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..18fb2807 100644 --- a/Sources/StructuredQueriesCore/QueryBindable.swift +++ b/Sources/StructuredQueriesCore/QueryBindable.swift @@ -9,14 +9,25 @@ public protocol QueryBindable: QueryRepresentable, QueryExpression where QueryVa /// A value that can be bound to a parameter of a SQL statement. var queryBinding: QueryBinding { get } + + 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 = queryBinding else { return nil } + self.init(queryBinding: queryBinding) + } } extension Bool: QueryBindable { @@ -25,10 +36,18 @@ extension Bool: QueryBindable { extension Double: QueryBindable { public var queryBinding: QueryBinding { .double(self) } + public init?(queryBinding: QueryBinding) { + guard case .double = queryBinding else { return nil } + self.init(queryBinding: queryBinding) + } } extension Date: QueryBindable { public var queryBinding: QueryBinding { .date(self) } + public init?(queryBinding: QueryBinding) { + guard case .date = queryBinding else { return nil } + self.init(queryBinding: queryBinding) + } } extension Float: QueryBindable { @@ -53,10 +72,18 @@ extension Int32: QueryBindable { extension Int64: QueryBindable { public var queryBinding: QueryBinding { .int(self) } + public init?(queryBinding: QueryBinding) { + guard case .int = queryBinding else { return nil } + self.init(queryBinding: queryBinding) + } } extension String: QueryBindable { public var queryBinding: QueryBinding { .text(self) } + public init?(queryBinding: QueryBinding) { + guard case .text = queryBinding else { return nil } + self.init(queryBinding: queryBinding) + } } extension UInt8: QueryBindable { @@ -83,6 +110,10 @@ extension UInt64: QueryBindable { extension UUID: QueryBindable { public var queryBinding: QueryBinding { .uuid(self) } + public init?(queryBinding: QueryBinding) { + guard case .uuid = queryBinding else { return nil } + self.init(queryBinding: queryBinding) + } } extension DefaultStringInterpolation { diff --git a/Sources/StructuredQueriesSQLite/Database.swift b/Sources/StructuredQueriesSQLite/Database.swift index e3dced2c..d041abd1 100644 --- a/Sources/StructuredQueriesSQLite/Database.swift +++ b/Sources/StructuredQueriesSQLite/Database.swift @@ -11,6 +11,15 @@ 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) } diff --git a/Tests/StructuredQueriesTests/CustomFunctionTests.swift b/Tests/StructuredQueriesTests/CustomFunctionTests.swift new file mode 100644 index 00000000..1e04500c --- /dev/null +++ b/Tests/StructuredQueriesTests/CustomFunctionTests.swift @@ -0,0 +1,186 @@ +import Dependencies +import Foundation +import InlineSnapshotTesting +import StructuredQueries +import StructuredQueriesSQLite +import StructuredQueriesTestSupport +import Testing + +extension SnapshotTests { + @Suite struct CustomFunctionTests { + @Test func customDate() { + @Dependency(\.defaultDatabase) var database + __$dateTime.install(database.handle) + assertQuery( + Values(_$dateTime()) + ) { + """ + SELECT "dateTime"(NULL) + """ + } results: { + """ + ┌────────────────────────────────┐ + │ Date(1970-01-01T00:00:00.000Z) │ + └────────────────────────────────┘ + """ + } + } + } +} + +// --- +import Foundation +import SQLite3 + +// @CustomFunction // @DatabaseFunction ? @ScalarFunction (_vs._ @AggregateFunction?) +func dateTime(_ format: String? = nil) -> Date { + Date(timeIntervalSince1970: 0) +} + +// Macro expansion: +@available(macOS 14, *) +func _$dateTime( + _ format: some QueryExpression = String?.none +) -> some QueryExpression { + __$dateTime(format) +} + +@available(macOS 14, *) +var __$dateTime: CustomFunction { + CustomFunction("dateTime", isDeterministic: false, body: dateTime(_:)) +} +// --- +// Library code: +@available(macOS 14, *) +struct CustomFunction { + let name: String + let isDeterministic: Bool + let body: (repeat each Input) throws(Failure) -> Output + + init( + _ name: String, + isDeterministic: Bool, + body: @escaping (repeat each Input) throws(Failure) -> Output + ) { + self.name = name + self.isDeterministic = isDeterministic + self.body = body + } + + func callAsFunction(_ input: repeat each T) -> SQLQueryExpression + where repeat each T: QueryExpression { + var arguments: [QueryFragment] = [] + for input in repeat each input { + arguments.append(input.queryFragment) + } + return SQLQueryExpression("\(quote: name)(\(arguments.joined(separator: ", ")))") + } + + fileprivate var anyBody: AnyBody { + AnyBody { argv in + var iterator = argv.makeIterator() + func next() throws -> Element { + guard let queryBinding = iterator.next(), let element = Element(queryBinding: queryBinding) + else { + throw QueryDecodingError.missingRequiredColumn // FIXME: New error case + } + return element + } + return try body(repeat { _ in try next() }((each Input).self)).queryBinding + } + } + + func install(_ db: OpaquePointer) { + // TODO: Should this be `-1`? + var count: Int32 = 0 + for _ in repeat (each Input).self { + count += 1 + } + let body = Unmanaged.passRetained(anyBody).toOpaque() + sqlite3_create_function_v2( + db, + name, + count, + SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), + body, + { ctx, argc, argv in + do { + let body = Unmanaged + .fromOpaque(sqlite3_user_data(ctx)) + .takeUnretainedValue() + let arguments: [QueryBinding] = try (0...fromOpaque(ctx).release() + } + ) + } +} + +private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +private struct UnknownType: Error {} + +private final class AnyBody { + let body: ([QueryBinding]) throws -> QueryBinding + init(body: @escaping ([QueryBinding]) throws -> QueryBinding) { + self.body = body + } + func callAsFunction(_ arguments: [QueryBinding]) throws -> QueryBinding { + try body(arguments) + } +} + +private extension QueryBinding { + func result(db: OpaquePointer?) throws { + switch self { + case .blob(let value): + sqlite3_result_blob(db, Array(value), Int32(value.count), SQLITE_TRANSIENT) + case .double(let value): + sqlite3_result_double(db, value) + case .date(let value): + sqlite3_result_text(db, value.iso8601String, -1, SQLITE_TRANSIENT) + case .int(let value): + sqlite3_result_int64(db, value) + case .null: + sqlite3_result_null(db) + case .text(let value): + sqlite3_result_text(db, value, -1, SQLITE_TRANSIENT) + case .uuid(let value): + sqlite3_result_text(db, value.uuidString.lowercased(), -1, SQLITE_TRANSIENT) + case .invalid(let error): + throw error + } + } +} From 0df44cc908cc7d7602b471b23166dd4d22e5509d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 27 Aug 2025 09:49:14 -0700 Subject: [PATCH 03/20] wip --- .../CustomFunctionTests.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Tests/StructuredQueriesTests/CustomFunctionTests.swift b/Tests/StructuredQueriesTests/CustomFunctionTests.swift index 1e04500c..c36e37ea 100644 --- a/Tests/StructuredQueriesTests/CustomFunctionTests.swift +++ b/Tests/StructuredQueriesTests/CustomFunctionTests.swift @@ -28,6 +28,15 @@ extension SnapshotTests { } } + + + + + + + + + // --- import Foundation import SQLite3 @@ -49,6 +58,34 @@ func _$dateTime( var __$dateTime: CustomFunction { CustomFunction("dateTime", isDeterministic: false, body: dateTime(_:)) } + +//struct DateTime: DatabaseFunction { +// typealias Input = String? +//// +// typealias Output = Date +//// +// typealias Failure = Never +//// +// typealias Result = SQLQueryExpression +// +// let name = "dateTime" +// let isDeterministic = false +// func callAsFunction( +// _ input: some QueryExpression = String?.none +// ) -> Result { +// SQLQueryExpression("\(quote: name)(\(input))") +// } +//} +// --- +//protocol DatabaseFunction { +// associatedtype Input +// associatedtype Output: QueryBindable where Output.QueryValue == Result.QueryValue +// associatedtype Failure: Error +// associatedtype Result: QueryExpression +// var name: String { get } +// var isDeterministic: Bool { get } +// func callAsFunction(_ input: some QueryExpression) throws(Failure) -> Result +//} // --- // Library code: @available(macOS 14, *) From a8d55142a06d0c938b6a3aea4e8b0e67437c0051 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 29 Aug 2025 16:57:50 -0700 Subject: [PATCH 04/20] wip --- Package.swift | 61 +- Package@swift-6.0.swift | 61 +- .../StructuredQueriesCore/QueryBindable.swift | 24 +- Sources/StructuredQueriesSQLite/Exports.swift | 2 + Sources/StructuredQueriesSQLite/Macros.swift | 11 + .../DatabaseFunction.swift | 6 + .../StructuredQueriesSQLiteCore/Exports.swift | 1 + .../DatabaseFunctionMacro.swift | 241 ++++++ .../Plugin.swift | 9 + .../Database.swift | 7 - .../DatabaseFunction.swift | 91 ++ .../_StructuredQueriesSQLite/Exports.swift | 7 + .../SQLiteQueryDecoder.swift | 7 - .../StructuredQueriesSQLite3.h | 0 .../module.modulemap | 0 .../DatabaseFunctionMacroTests.swift | 809 ++++++++++++++++++ .../Support/SnapshotTests.swift | 9 +- .../StructuredQueriesTests/BindingTests.swift | 2 +- Tests/StructuredQueriesTests/CaseTests.swift | 2 +- .../CommonTableExpressionTests.swift | 2 +- .../CustomFunctionTests.swift | 223 ----- .../DatabaseFunctionTests.swift | 109 +++ .../DecodingTests.swift | 2 +- .../JSONFunctionsTests.swift | 2 +- .../KitchenSinkTests.swift | 2 +- Tests/StructuredQueriesTests/LiveTests.swift | 2 +- .../PrimaryKeyedTableTests.swift | 2 +- .../Support/AssertQuery.swift | 2 +- .../Support/Schema.swift | 2 +- Tests/StructuredQueriesTests/TableTests.swift | 2 +- .../TriggersTests.swift | 2 +- .../StructuredQueriesTests/UpdateTests.swift | 2 +- Tests/StructuredQueriesTests/WhereTests.swift | 2 +- 33 files changed, 1406 insertions(+), 300 deletions(-) create mode 100644 Sources/StructuredQueriesSQLite/Exports.swift create mode 100644 Sources/StructuredQueriesSQLite/Macros.swift create mode 100644 Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift create mode 100644 Sources/StructuredQueriesSQLiteCore/Exports.swift create mode 100644 Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift create mode 100644 Sources/StructuredQueriesSQLiteMacros/Plugin.swift rename Sources/{StructuredQueriesSQLite => _StructuredQueriesSQLite}/Database.swift (98%) create mode 100644 Sources/_StructuredQueriesSQLite/DatabaseFunction.swift create mode 100644 Sources/_StructuredQueriesSQLite/Exports.swift rename Sources/{StructuredQueriesSQLite => _StructuredQueriesSQLite}/SQLiteQueryDecoder.swift (95%) rename Sources/{StructuredQueriesSQLite3 => _StructuredQueriesSQLite3}/StructuredQueriesSQLite3.h (100%) rename Sources/{StructuredQueriesSQLite3 => _StructuredQueriesSQLite3}/module.modulemap (100%) create mode 100644 Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift delete mode 100644 Tests/StructuredQueriesTests/CustomFunctionTests.swift create mode 100644 Tests/StructuredQueriesTests/DatabaseFunctionTests.swift diff --git a/Package.swift b/Package.swift index 814aa2bf..d4666748 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,29 @@ let package = Package( ], exclude: ["Symbolic Links/README.md"] ), + .target( name: "StructuredQueriesSQLite", dependencies: [ - "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 +111,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 +123,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 +157,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..4295e2ee 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,29 @@ let package = Package( ], exclude: ["Symbolic Links/README.md"] ), + .target( name: "StructuredQueriesSQLite", dependencies: [ - "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 +99,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 +113,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 +148,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/QueryBindable.swift b/Sources/StructuredQueriesCore/QueryBindable.swift index 18fb2807..500fbe30 100644 --- a/Sources/StructuredQueriesCore/QueryBindable.swift +++ b/Sources/StructuredQueriesCore/QueryBindable.swift @@ -25,8 +25,8 @@ extension QueryBindable { extension [UInt8]: QueryBindable, QueryExpression { public var queryBinding: QueryBinding { .blob(self) } public init?(queryBinding: QueryBinding) { - guard case .blob = queryBinding else { return nil } - self.init(queryBinding: queryBinding) + guard case .blob(let value) = queryBinding else { return nil } + self = value } } @@ -37,16 +37,16 @@ extension Bool: QueryBindable { extension Double: QueryBindable { public var queryBinding: QueryBinding { .double(self) } public init?(queryBinding: QueryBinding) { - guard case .double = queryBinding else { return nil } - self.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 = queryBinding else { return nil } - self.init(queryBinding: queryBinding) + guard case .date(let value) = queryBinding else { return nil } + self = value } } @@ -73,16 +73,16 @@ extension Int32: QueryBindable { extension Int64: QueryBindable { public var queryBinding: QueryBinding { .int(self) } public init?(queryBinding: QueryBinding) { - guard case .int = queryBinding else { return nil } - self.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 .text = queryBinding else { return nil } - self.init(queryBinding: queryBinding) + guard case let .text(value) = queryBinding else { return nil } + self = value } } @@ -111,8 +111,8 @@ extension UInt64: QueryBindable { extension UUID: QueryBindable { public var queryBinding: QueryBinding { .uuid(self) } public init?(queryBinding: QueryBinding) { - guard case .uuid = queryBinding else { return nil } - self.init(queryBinding: queryBinding) + guard case .uuid(let value) = queryBinding else { return nil } + self = value } } 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..82494191 --- /dev/null +++ b/Sources/StructuredQueriesSQLite/Macros.swift @@ -0,0 +1,11 @@ +import StructuredQueriesSQLiteCore + +@attached(peer, names: overloaded, prefixed(`$`)) +public macro DatabaseFunction( + _ name: String = "", + isDeterministic: Bool = false +) = + #externalMacro( + module: "StructuredQueriesSQLiteMacros", + type: "DatabaseFunctionMacro" + ) diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift new file mode 100644 index 00000000..cd03481b --- /dev/null +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -0,0 +1,6 @@ +public protocol DatabaseFunction { + var name: String { get } + var argumentCount: Int? { get } + var isDeterministic: Bool { get } + func invoke(_ arguments: [QueryBinding]) -> QueryBinding +} 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..3ff16bb8 --- /dev/null +++ b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift @@ -0,0 +1,241 @@ +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) + var 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)])") + } + let bodyReturnClause: String + if let returnClause = signature.returnClause { + signature.returnClause?.type = returnClause.type.asQueryExpression() + bodyReturnClause = " \(returnClause.trimmedDescription)" + } else { + bodyReturnClause = " -> Void" + } + let bodyType = """ + (\(bodyArguments.joined(separator: ", ")))\ + \(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 + } + } + + return [ + """ + \(attributes)\(access)\(`static`)var $\(raw: declarationName): \(functionTypeName) { + \(functionTypeName)(\(declaration.name.trimmed)) + } + """, + """ + \(attributes)\(access)struct \(functionTypeName): \ + StructuredQueriesSQLiteCore.DatabaseFunction { + 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 98% rename from Sources/StructuredQueriesSQLite/Database.swift rename to Sources/_StructuredQueriesSQLite/Database.swift index d041abd1..5be1371a 100644 --- a/Sources/StructuredQueriesSQLite/Database.swift +++ b/Sources/_StructuredQueriesSQLite/Database.swift @@ -1,11 +1,4 @@ import Foundation -import StructuredQueries - -#if canImport(Darwin) - import SQLite3 -#else - import StructuredQueriesSQLite3 -#endif public struct Database { @usableFromInline diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift new file mode 100644 index 00000000..2474f010 --- /dev/null +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -0,0 +1,91 @@ +import Foundation + +extension DatabaseFunction { + public func install(_ db: OpaquePointer) { + let body = Unmanaged.passRetained(AnyBody(body: invoke)).toOpaque() + sqlite3_create_function_v2( + db, + name, + Int32(argumentCount ?? -1), + SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), + body, + { ctx, argc, argv in + do { + let body = Unmanaged + .fromOpaque(sqlite3_user_data(ctx)) + .takeUnretainedValue() + let arguments: [QueryBinding] = try (0...fromOpaque(ctx).release() + } + ) + } +} + +private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +private struct UnknownType: Error {} + +private final class AnyBody { + let body: ([QueryBinding]) -> QueryBinding + init(body: @escaping ([QueryBinding]) -> QueryBinding) { + self.body = body + } + func callAsFunction(_ arguments: [QueryBinding]) -> QueryBinding { + body(arguments) + } +} + +extension QueryBinding { + fileprivate func result(db: OpaquePointer?) throws { + switch self { + case .blob(let value): + sqlite3_result_blob(db, Array(value), Int32(value.count), SQLITE_TRANSIENT) + case .double(let value): + sqlite3_result_double(db, value) + case .date(let value): + sqlite3_result_text(db, value.iso8601String, -1, SQLITE_TRANSIENT) + case .int(let value): + sqlite3_result_int64(db, value) + case .null: + sqlite3_result_null(db) + case .text(let value): + sqlite3_result_text(db, value, -1, SQLITE_TRANSIENT) + case .uuid(let value): + sqlite3_result_text(db, value.uuidString.lowercased(), -1, SQLITE_TRANSIENT) + case .invalid(let error): + throw error.underlyingError + } + } +} diff --git a/Sources/_StructuredQueriesSQLite/Exports.swift b/Sources/_StructuredQueriesSQLite/Exports.swift new file mode 100644 index 00000000..bc6e8f05 --- /dev/null +++ b/Sources/_StructuredQueriesSQLite/Exports.swift @@ -0,0 +1,7 @@ +@_exported import StructuredQueriesSQLite + +#if canImport(Darwin) + @_exported import SQLite3 +#else + @_exported import _StructuredQueriesSQLite3 +#endif diff --git a/Sources/StructuredQueriesSQLite/SQLiteQueryDecoder.swift b/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift similarity index 95% rename from Sources/StructuredQueriesSQLite/SQLiteQueryDecoder.swift rename to Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift index c2a69ac2..027ce9f8 100644 --- a/Sources/StructuredQueriesSQLite/SQLiteQueryDecoder.swift +++ b/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift @@ -1,11 +1,4 @@ import Foundation -import StructuredQueries - -#if canImport(Darwin) - import SQLite3 -#else - import StructuredQueriesSQLite3 -#endif @usableFromInline struct SQLiteQueryDecoder: QueryDecoder { diff --git a/Sources/StructuredQueriesSQLite3/StructuredQueriesSQLite3.h b/Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h similarity index 100% rename from Sources/StructuredQueriesSQLite3/StructuredQueriesSQLite3.h rename to Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h diff --git a/Sources/StructuredQueriesSQLite3/module.modulemap b/Sources/_StructuredQueriesSQLite3/module.modulemap similarity index 100% rename from Sources/StructuredQueriesSQLite3/module.modulemap rename to Sources/_StructuredQueriesSQLite3/module.modulemap diff --git a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift new file mode 100644 index 00000000..73b507b9 --- /dev/null +++ b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift @@ -0,0 +1,809 @@ +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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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.DatabaseFunction { + 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/CustomFunctionTests.swift b/Tests/StructuredQueriesTests/CustomFunctionTests.swift deleted file mode 100644 index c36e37ea..00000000 --- a/Tests/StructuredQueriesTests/CustomFunctionTests.swift +++ /dev/null @@ -1,223 +0,0 @@ -import Dependencies -import Foundation -import InlineSnapshotTesting -import StructuredQueries -import StructuredQueriesSQLite -import StructuredQueriesTestSupport -import Testing - -extension SnapshotTests { - @Suite struct CustomFunctionTests { - @Test func customDate() { - @Dependency(\.defaultDatabase) var database - __$dateTime.install(database.handle) - assertQuery( - Values(_$dateTime()) - ) { - """ - SELECT "dateTime"(NULL) - """ - } results: { - """ - ┌────────────────────────────────┐ - │ Date(1970-01-01T00:00:00.000Z) │ - └────────────────────────────────┘ - """ - } - } - } -} - - - - - - - - - - -// --- -import Foundation -import SQLite3 - -// @CustomFunction // @DatabaseFunction ? @ScalarFunction (_vs._ @AggregateFunction?) -func dateTime(_ format: String? = nil) -> Date { - Date(timeIntervalSince1970: 0) -} - -// Macro expansion: -@available(macOS 14, *) -func _$dateTime( - _ format: some QueryExpression = String?.none -) -> some QueryExpression { - __$dateTime(format) -} - -@available(macOS 14, *) -var __$dateTime: CustomFunction { - CustomFunction("dateTime", isDeterministic: false, body: dateTime(_:)) -} - -//struct DateTime: DatabaseFunction { -// typealias Input = String? -//// -// typealias Output = Date -//// -// typealias Failure = Never -//// -// typealias Result = SQLQueryExpression -// -// let name = "dateTime" -// let isDeterministic = false -// func callAsFunction( -// _ input: some QueryExpression = String?.none -// ) -> Result { -// SQLQueryExpression("\(quote: name)(\(input))") -// } -//} -// --- -//protocol DatabaseFunction { -// associatedtype Input -// associatedtype Output: QueryBindable where Output.QueryValue == Result.QueryValue -// associatedtype Failure: Error -// associatedtype Result: QueryExpression -// var name: String { get } -// var isDeterministic: Bool { get } -// func callAsFunction(_ input: some QueryExpression) throws(Failure) -> Result -//} -// --- -// Library code: -@available(macOS 14, *) -struct CustomFunction { - let name: String - let isDeterministic: Bool - let body: (repeat each Input) throws(Failure) -> Output - - init( - _ name: String, - isDeterministic: Bool, - body: @escaping (repeat each Input) throws(Failure) -> Output - ) { - self.name = name - self.isDeterministic = isDeterministic - self.body = body - } - - func callAsFunction(_ input: repeat each T) -> SQLQueryExpression - where repeat each T: QueryExpression { - var arguments: [QueryFragment] = [] - for input in repeat each input { - arguments.append(input.queryFragment) - } - return SQLQueryExpression("\(quote: name)(\(arguments.joined(separator: ", ")))") - } - - fileprivate var anyBody: AnyBody { - AnyBody { argv in - var iterator = argv.makeIterator() - func next() throws -> Element { - guard let queryBinding = iterator.next(), let element = Element(queryBinding: queryBinding) - else { - throw QueryDecodingError.missingRequiredColumn // FIXME: New error case - } - return element - } - return try body(repeat { _ in try next() }((each Input).self)).queryBinding - } - } - - func install(_ db: OpaquePointer) { - // TODO: Should this be `-1`? - var count: Int32 = 0 - for _ in repeat (each Input).self { - count += 1 - } - let body = Unmanaged.passRetained(anyBody).toOpaque() - sqlite3_create_function_v2( - db, - name, - count, - SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), - body, - { ctx, argc, argv in - do { - let body = Unmanaged - .fromOpaque(sqlite3_user_data(ctx)) - .takeUnretainedValue() - let arguments: [QueryBinding] = try (0...fromOpaque(ctx).release() - } - ) - } -} - -private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) - -private struct UnknownType: Error {} - -private final class AnyBody { - let body: ([QueryBinding]) throws -> QueryBinding - init(body: @escaping ([QueryBinding]) throws -> QueryBinding) { - self.body = body - } - func callAsFunction(_ arguments: [QueryBinding]) throws -> QueryBinding { - try body(arguments) - } -} - -private extension QueryBinding { - func result(db: OpaquePointer?) throws { - switch self { - case .blob(let value): - sqlite3_result_blob(db, Array(value), Int32(value.count), SQLITE_TRANSIENT) - case .double(let value): - sqlite3_result_double(db, value) - case .date(let value): - sqlite3_result_text(db, value.iso8601String, -1, SQLITE_TRANSIENT) - case .int(let value): - sqlite3_result_int64(db, value) - case .null: - sqlite3_result_null(db) - case .text(let value): - sqlite3_result_text(db, value, -1, SQLITE_TRANSIENT) - case .uuid(let value): - sqlite3_result_text(db, value.uuidString.lowercased(), -1, SQLITE_TRANSIENT) - case .invalid(let error): - throw error - } - } -} diff --git a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift new file mode 100644 index 00000000..8268332d --- /dev/null +++ b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift @@ -0,0 +1,109 @@ +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" │ + └──────────┘ + """ + } + } + + @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 d56bfbc3..9994cb63 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 { From 4b9be8f269428abcb926fa584dc15a7d166c2d85 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 29 Aug 2025 17:15:06 -0700 Subject: [PATCH 05/20] wip --- Sources/StructuredQueriesSQLite/Macros.swift | 7 +++++++ .../DatabaseFunction.swift | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/Sources/StructuredQueriesSQLite/Macros.swift b/Sources/StructuredQueriesSQLite/Macros.swift index 82494191..4db431e6 100644 --- a/Sources/StructuredQueriesSQLite/Macros.swift +++ b/Sources/StructuredQueriesSQLite/Macros.swift @@ -1,5 +1,12 @@ 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 = "", diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift index cd03481b..2bd4218a 100644 --- a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -1,6 +1,22 @@ +/// A type representing a database function. +/// +/// Don't conform to this protocol directly. Instead, use the `@DatabaseFunction` macros to +/// generate a conformance. public protocol DatabaseFunction { + /// 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 } + + /// The function body. Transforms a collection 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 } From 075c8658398e12eee4533cea804d35622396a578 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 29 Aug 2025 17:16:58 -0700 Subject: [PATCH 06/20] wip --- Package@swift-6.0.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 4295e2ee..e9ff13c5 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -88,7 +88,7 @@ let package = Package( dependencies: [ .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - ], + ] ), .target( From 6c595e88953cb528292d5a304007bf1ba948651b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 29 Aug 2025 17:22:41 -0700 Subject: [PATCH 07/20] wip --- .../StructuredQueriesSQLiteCore.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md new file mode 100644 index 00000000..aeb4c739 --- /dev/null +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md @@ -0,0 +1,14 @@ +# ``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`` From ac8812690f80686655ef6cc814d8c1b4f0481429 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 29 Aug 2025 17:23:05 -0700 Subject: [PATCH 08/20] wip --- .spi.yml | 2 ++ 1 file changed, 2 insertions(+) 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' From 2f4346d6c76dff55f273b111580bf1c6a2a83a56 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 29 Aug 2025 17:24:01 -0700 Subject: [PATCH 09/20] wip --- Package.swift | 1 + Package@swift-6.0.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Package.swift b/Package.swift index d4666748..063c047a 100644 --- a/Package.swift +++ b/Package.swift @@ -81,6 +81,7 @@ let package = Package( .target( name: "StructuredQueriesSQLite", dependencies: [ + "StructuredQueries", "StructuredQueriesSQLiteCore", "StructuredQueriesSQLiteMacros", ] diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index e9ff13c5..d3f83fcc 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -72,6 +72,7 @@ let package = Package( .target( name: "StructuredQueriesSQLite", dependencies: [ + "StructuredQueries", "StructuredQueriesSQLiteCore", "StructuredQueriesSQLiteMacros", ] From b4128198a4e0e13624d56ac4c570abf82892ef0b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 29 Aug 2025 17:29:32 -0700 Subject: [PATCH 10/20] wip --- Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h | 4 ++-- Sources/_StructuredQueriesSQLite3/module.modulemap | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h b/Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h index 8e211139..21caa39e 100644 --- a/Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h +++ b/Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h @@ -1,4 +1,4 @@ -#ifndef StructuredQueriesSQLite3 -#define StructuredQueriesSQLite3 +#ifndef _StructuredQueriesSQLite3 +#define _StructuredQueriesSQLite3 #include #endif diff --git a/Sources/_StructuredQueriesSQLite3/module.modulemap b/Sources/_StructuredQueriesSQLite3/module.modulemap index bbb8d7df..f54a1234 100644 --- a/Sources/_StructuredQueriesSQLite3/module.modulemap +++ b/Sources/_StructuredQueriesSQLite3/module.modulemap @@ -1,5 +1,5 @@ -module StructuredQueriesSQLite3 [system] { +module _StructuredQueriesSQLite3 [system] { link "sqlite3" - header "StructuredQueriesSQLite3.h" + header "_StructuredQueriesSQLite3.h" export * } From ecdbf84af8cc2f2e968e83911a9f10c0ba0ac6c8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 29 Aug 2025 17:48:07 -0700 Subject: [PATCH 11/20] wip --- .../{StructuredQueriesSQLite3.h => _StructuredQueriesSQLite3.h} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/_StructuredQueriesSQLite3/{StructuredQueriesSQLite3.h => _StructuredQueriesSQLite3.h} (100%) diff --git a/Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h b/Sources/_StructuredQueriesSQLite3/_StructuredQueriesSQLite3.h similarity index 100% rename from Sources/_StructuredQueriesSQLite3/StructuredQueriesSQLite3.h rename to Sources/_StructuredQueriesSQLite3/_StructuredQueriesSQLite3.h From 40ceba77cb3362d94c93e0b1f9b2da57987343b7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 31 Aug 2025 11:39:04 -0700 Subject: [PATCH 12/20] wip --- .../DatabaseFunction.swift | 2 ++ .../DatabaseFunctionMacro.swift | 2 +- .../DatabaseFunction.swift | 30 ++++++++--------- .../DatabaseFunctionMacroTests.swift | 32 +++++++++---------- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift index 2bd4218a..9cbba17b 100644 --- a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -12,7 +12,9 @@ public protocol DatabaseFunction { /// 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 a collection of bindings handed to the function into a binding /// returned to the query. /// diff --git a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift index 3ff16bb8..a7cc0a2f 100644 --- a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift +++ b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift @@ -183,7 +183,7 @@ extension DatabaseFunctionMacro: PeerMacro { """, """ \(attributes)\(access)struct \(functionTypeName): \ - StructuredQueriesSQLiteCore.DatabaseFunction { + StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = \(databaseFunctionName) public let argumentCount: Int? = \(raw: argumentCount) public let isDeterministic = \(raw: isDeterministic) diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift index 2474f010..b676297e 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -1,21 +1,21 @@ import Foundation -extension DatabaseFunction { +extension ScalarDatabaseFunction { public func install(_ db: OpaquePointer) { - let body = Unmanaged.passRetained(AnyBody(body: invoke)).toOpaque() + let body = Unmanaged.passRetained(ScalarDatabaseFunctionContext(self)).toOpaque() sqlite3_create_function_v2( db, name, Int32(argumentCount ?? -1), SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), body, - { ctx, argc, argv in + { context, argumentCount, arguments in do { - let body = Unmanaged - .fromOpaque(sqlite3_user_data(ctx)) + let body = Unmanaged + .fromOpaque(sqlite3_user_data(context)) .takeUnretainedValue() - let arguments: [QueryBinding] = try (0...fromOpaque(ctx).release() + { context in + guard let context else { return } + Unmanaged.fromOpaque(context).release() } ) } @@ -57,10 +57,10 @@ private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.sel private struct UnknownType: Error {} -private final class AnyBody { +private final class ScalarDatabaseFunctionContext { let body: ([QueryBinding]) -> QueryBinding - init(body: @escaping ([QueryBinding]) -> QueryBinding) { - self.body = body + init(_ function: some ScalarDatabaseFunction) { + body = function.invoke } func callAsFunction(_ arguments: [QueryBinding]) -> QueryBinding { body(arguments) diff --git a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift index 73b507b9..f4b13a70 100644 --- a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift +++ b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift @@ -23,7 +23,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 0 public let isDeterministic = false @@ -69,7 +69,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "current_date" public let argumentCount: Int? = 0 public let isDeterministic = false @@ -115,7 +115,7 @@ extension SnapshotTests { __macro_local_8fortyTwofMu_(fortyTwo) } - struct __macro_local_8fortyTwofMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_8fortyTwofMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "fortyTwo" public let argumentCount: Int? = 0 public let isDeterministic = true @@ -161,7 +161,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 1 public let isDeterministic = false @@ -207,7 +207,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 1 public let isDeterministic = false @@ -253,7 +253,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 1 public let isDeterministic = false @@ -299,7 +299,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 1 public let isDeterministic = false @@ -345,7 +345,7 @@ extension SnapshotTests { __macro_local_6concatfMu_(concat) } - struct __macro_local_6concatfMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_6concatfMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "concat" public let argumentCount: Int? = 2 public let isDeterministic = false @@ -408,7 +408,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 1 public let isDeterministic = false @@ -454,7 +454,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 0 public let isDeterministic = false @@ -504,7 +504,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 0 public let isDeterministic = false @@ -554,7 +554,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - public struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + public struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 0 public let isDeterministic = false @@ -600,7 +600,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 0 public let isDeterministic = false @@ -669,7 +669,7 @@ extension SnapshotTests { __macro_local_11currentDatefMu_(currentDate) } - @available(*, unavailable) struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + @available(*, unavailable) struct __macro_local_11currentDatefMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "currentDate" public let argumentCount: Int? = 0 public let isDeterministic = false @@ -715,7 +715,7 @@ extension SnapshotTests { __macro_local_7defaultfMu_(`default`) } - public struct __macro_local_7defaultfMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + public struct __macro_local_7defaultfMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "default" public let argumentCount: Int? = 0 public let isDeterministic = false @@ -778,7 +778,7 @@ extension SnapshotTests { __macro_local_4voidfMu_(void) } - public struct __macro_local_4voidfMu_: StructuredQueriesSQLiteCore.DatabaseFunction { + public struct __macro_local_4voidfMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { public let name = "void" public let argumentCount: Int? = 0 public let isDeterministic = false From 73055602cf4ee49bc206336b888a37a364e38463 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 31 Aug 2025 11:42:19 -0700 Subject: [PATCH 13/20] wip --- .../DatabaseFunctionMacro.swift | 2 +- .../_StructuredQueriesSQLite/Database.swift | 2 +- .../DatabaseFunction.swift | 58 ++++++++++--------- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift index a7cc0a2f..457f0bc2 100644 --- a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift +++ b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift @@ -92,7 +92,7 @@ extension DatabaseFunctionMacro: PeerMacro { let functionTypeName = context.makeUniqueName(declarationName) let databaseFunctionName = StringLiteralExprSyntax(content: functionName) - var argumentCount = declaration.signature.parameterClause.parameters.count + let argumentCount = declaration.signature.parameterClause.parameters.count var bodyArguments: [String] = [] var signature = declaration.signature diff --git a/Sources/_StructuredQueriesSQLite/Database.swift b/Sources/_StructuredQueriesSQLite/Database.swift index 5be1371a..7a519b7c 100644 --- a/Sources/_StructuredQueriesSQLite/Database.swift +++ b/Sources/_StructuredQueriesSQLite/Database.swift @@ -192,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 index b676297e..c40bb377 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -14,29 +14,7 @@ extension ScalarDatabaseFunction { let body = Unmanaged .fromOpaque(sqlite3_user_data(context)) .takeUnretainedValue() - let arguments: [QueryBinding] = try (0.. QueryBinding init(_ function: some ScalarDatabaseFunction) { @@ -67,6 +41,36 @@ private final class ScalarDatabaseFunctionContext { } } +extension [QueryBinding] { + fileprivate init(argumentCount: Int32, arguments: UnsafeMutablePointer?) throws { + self = try (0.. Date: Sun, 31 Aug 2025 21:53:59 -0700 Subject: [PATCH 14/20] wip --- .../DatabaseFunction.swift | 6 +-- .../DatabaseFunction.swift | 54 +++++++++++++++---- .../DatabaseFunctionTests.swift | 2 +- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift index 9cbba17b..fd2e0fbc 100644 --- a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -1,7 +1,7 @@ /// A type representing a database function. /// -/// Don't conform to this protocol directly. Instead, use the `@DatabaseFunction` macros to -/// generate a conformance. +/// Don't conform to this protocol directly. Instead, use the `@DatabaseFunction` macro to generate +/// a conformance. public protocol DatabaseFunction { /// The name of the function. var name: String { get } @@ -15,7 +15,7 @@ public protocol DatabaseFunction { } public protocol ScalarDatabaseFunction: DatabaseFunction { - /// The function body. Transforms a collection of bindings handed to the function into a binding + /// 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. diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift index c40bb377..26607c1c 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -2,20 +2,20 @@ import Foundation extension ScalarDatabaseFunction { public func install(_ db: OpaquePointer) { - let body = Unmanaged.passRetained(ScalarDatabaseFunctionContext(self)).toOpaque() + let box = Unmanaged.passRetained(ScalarDatabaseFunctionBox(self)).toOpaque() sqlite3_create_function_v2( db, name, Int32(argumentCount ?? -1), SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), - body, + box, { context, argumentCount, arguments in do { - let body = Unmanaged + let box = Unmanaged .fromOpaque(sqlite3_user_data(context)) .takeUnretainedValue() let arguments = try [QueryBinding](argumentCount: argumentCount, arguments: arguments) - let output = body(arguments) + let output = box.function.invoke(arguments) try output.result(db: context) } catch { sqlite3_result_error(context, error.localizedDescription, -1) @@ -25,19 +25,16 @@ extension ScalarDatabaseFunction { nil, { context in guard let context else { return } - Unmanaged.fromOpaque(context).release() + Unmanaged.fromOpaque(context).release() } ) } } -private final class ScalarDatabaseFunctionContext { - let body: ([QueryBinding]) -> QueryBinding +private final class ScalarDatabaseFunctionBox { + let function: any ScalarDatabaseFunction init(_ function: some ScalarDatabaseFunction) { - body = function.invoke - } - func callAsFunction(_ arguments: [QueryBinding]) -> QueryBinding { - body(arguments) + self.function = function } } @@ -93,3 +90,38 @@ extension QueryBinding { } } } + +private final class Stream: Sequence { + private let condition = NSCondition() + private var buffer: [Element] = [] + private var isFinished = false + + func send(_ element: Element) { + condition.withLock { + buffer.append(element) + condition.signal() + } + } + + func finish() { + condition.withLock { + isFinished = true + condition.broadcast() + } + } + + func makeIterator() -> Iterator { Iterator(base: self) } + + struct Iterator: IteratorProtocol { + fileprivate let base: Stream + mutating func next() -> Element? { + base.condition.withLock { + while base.buffer.isEmpty && !base.isFinished { + base.condition.wait() + } + guard !base.buffer.isEmpty else { return nil } + return base.buffer.removeFirst() + } + } + } +} diff --git a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift index 8268332d..56b30f3b 100644 --- a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift +++ b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift @@ -75,7 +75,7 @@ extension SnapshotTests { """ } } - + @DatabaseFunction func throwing() throws -> String { struct Failure: LocalizedError { From b38a1dc6d002ac49a35c7267f1ebe9f8a2ba588f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 09:44:58 -0700 Subject: [PATCH 15/20] wip --- .../StructuredQueriesCore/QueryBindable.swift | 1 + .../DatabaseFunction.swift | 35 ------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/Sources/StructuredQueriesCore/QueryBindable.swift b/Sources/StructuredQueriesCore/QueryBindable.swift index 500fbe30..6fab9f1f 100644 --- a/Sources/StructuredQueriesCore/QueryBindable.swift +++ b/Sources/StructuredQueriesCore/QueryBindable.swift @@ -10,6 +10,7 @@ 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) } diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift index 26607c1c..7b1ff059 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -90,38 +90,3 @@ extension QueryBinding { } } } - -private final class Stream: Sequence { - private let condition = NSCondition() - private var buffer: [Element] = [] - private var isFinished = false - - func send(_ element: Element) { - condition.withLock { - buffer.append(element) - condition.signal() - } - } - - func finish() { - condition.withLock { - isFinished = true - condition.broadcast() - } - } - - func makeIterator() -> Iterator { Iterator(base: self) } - - struct Iterator: IteratorProtocol { - fileprivate let base: Stream - mutating func next() -> Element? { - base.condition.withLock { - while base.buffer.isEmpty && !base.isFinished { - base.condition.wait() - } - guard !base.buffer.isEmpty else { return nil } - return base.buffer.removeFirst() - } - } - } -} From 038b40be3b89d1794854b48bfc942fd09811885e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 10:11:12 -0700 Subject: [PATCH 16/20] wip --- .../DatabaseFunction.swift | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift index 7b1ff059..97bafa09 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -10,16 +10,12 @@ extension ScalarDatabaseFunction { SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), box, { context, argumentCount, arguments in - do { - let box = Unmanaged - .fromOpaque(sqlite3_user_data(context)) - .takeUnretainedValue() - let arguments = try [QueryBinding](argumentCount: argumentCount, arguments: arguments) - let output = box.function.invoke(arguments) - try output.result(db: context) - } catch { - sqlite3_result_error(context, error.localizedDescription, -1) - } + Unmanaged + .fromOpaque(sqlite3_user_data(context)) + .takeUnretainedValue() + .function + .invoke([QueryBinding](argumentCount: argumentCount, arguments: arguments)) + .result(db: context) }, nil, nil, @@ -39,9 +35,9 @@ private final class ScalarDatabaseFunctionBox { } extension [QueryBinding] { - fileprivate init(argumentCount: Int32, arguments: UnsafeMutablePointer?) throws { - self = try (0..?) { + self = (0.. Date: Mon, 1 Sep 2025 10:26:20 -0700 Subject: [PATCH 17/20] wip --- Sources/_StructuredQueriesSQLite/DatabaseFunction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift index 97bafa09..65a18caa 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -82,7 +82,7 @@ extension QueryBinding { case .uuid(let value): sqlite3_result_text(db, value.uuidString.lowercased(), -1, SQLITE_TRANSIENT) case .invalid(let error): - sqlite3_result_error(db, error.localizedDescription, -1) + sqlite3_result_error(db, error.underlyingError.localizedDescription, -1) } } } From af4db5beed44a994d7e936d1b97be2ee7916048f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 12:29:17 -0700 Subject: [PATCH 18/20] wip --- .../DatabaseFunction.swift | 18 +++++++++-- .../DatabaseFunctionMacro.swift | 9 +++++- .../DatabaseFunctionMacroTests.swift | 32 +++++++++++++++++++ .../DatabaseFunctionTests.swift | 18 +++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift index fd2e0fbc..c282e3c8 100644 --- a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -2,7 +2,10 @@ /// /// Don't conform to this protocol directly. Instead, use the `@DatabaseFunction` macro to generate /// a conformance. -public protocol DatabaseFunction { +public protocol DatabaseFunction { + associatedtype Input + associatedtype Output + /// The name of the function. var name: String { get } @@ -14,7 +17,7 @@ public protocol DatabaseFunction { var isDeterministic: Bool { get } } -public protocol ScalarDatabaseFunction: DatabaseFunction { +public protocol ScalarDatabaseFunction: DatabaseFunction { /// The function body. Transforms an array of bindings handed to the function into a binding /// returned to the query. /// @@ -22,3 +25,14 @@ public protocol ScalarDatabaseFunction: DatabaseFunction { /// - Returns: A value returned from the database function. func invoke(_ arguments: [QueryBinding]) -> QueryBinding } + +extension ScalarDatabaseFunction { + 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/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift index 457f0bc2..7f2f6068 100644 --- a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift +++ b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift @@ -125,15 +125,19 @@ extension DatabaseFunctionMacro: PeerMacro { 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 = """ - (\(bodyArguments.joined(separator: ", ")))\ + (\(inputType))\ \(declaration.signature.effectSpecifiers?.trimmedDescription ?? "")\ \(bodyReturnClause) """ @@ -174,6 +178,7 @@ extension DatabaseFunctionMacro: PeerMacro { continue } } + inputType = bodyArguments.count == 1 ? inputType : "(\(inputType))" return [ """ @@ -184,6 +189,8 @@ extension DatabaseFunctionMacro: PeerMacro { """ \(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) diff --git a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift index f4b13a70..f9632612 100644 --- a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift +++ b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift @@ -24,6 +24,8 @@ extension SnapshotTests { } 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 @@ -70,6 +72,8 @@ extension SnapshotTests { } 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 @@ -116,6 +120,8 @@ extension SnapshotTests { } 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 @@ -162,6 +168,8 @@ extension SnapshotTests { } 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 @@ -208,6 +216,8 @@ extension SnapshotTests { } 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 @@ -254,6 +264,8 @@ extension SnapshotTests { } 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 @@ -300,6 +312,8 @@ extension SnapshotTests { } 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 @@ -346,6 +360,8 @@ extension SnapshotTests { } 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 @@ -409,6 +425,8 @@ extension SnapshotTests { } 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 @@ -455,6 +473,8 @@ extension SnapshotTests { } 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 @@ -505,6 +525,8 @@ extension SnapshotTests { } 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 @@ -555,6 +577,8 @@ extension SnapshotTests { } 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 @@ -601,6 +625,8 @@ extension SnapshotTests { } 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 @@ -670,6 +696,8 @@ extension SnapshotTests { } @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 @@ -716,6 +744,8 @@ extension SnapshotTests { } 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 @@ -779,6 +809,8 @@ extension SnapshotTests { } 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 diff --git a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift index 56b30f3b..a5fc6372 100644 --- a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift +++ b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift @@ -76,6 +76,24 @@ extension SnapshotTests { } } + @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 { From 2037886b50d41b780421242fe30f6bbfdfeb366f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 12:32:44 -0700 Subject: [PATCH 19/20] wip --- Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift index c282e3c8..6200897f 100644 --- a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -3,7 +3,10 @@ /// 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. @@ -27,6 +30,10 @@ public protocol ScalarDatabaseFunction: DatabaseFunction { } 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 From fe3f2bcc9a523e87d60710e7f57e21006c5f9c0d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 1 Sep 2025 12:46:06 -0700 Subject: [PATCH 20/20] wip --- .../StructuredQueriesSQLite.md | 19 +++++++++++++++++++ .../StructuredQueriesSQLiteCore.md | 1 + 2 files changed, 20 insertions(+) create mode 100644 Sources/StructuredQueriesSQLite/Documentation.docc/StructuredQueriesSQLite.md 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/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md index aeb4c739..f6a79406 100644 --- a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/StructuredQueriesSQLiteCore.md @@ -12,3 +12,4 @@ custom database functions, and more. ### Custom functions - ``DatabaseFunction`` +- ``ScalarDatabaseFunction``