diff --git a/Package.resolved b/Package.resolved index 33959b4..afe3c94 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "89777d929ec26bbed6f64b140dce168ca0fbc69d9273ed1edcfaf674a6bb8eb4", + "originHash" : "9477039766900a876852733dc60c616e209d09ef46f39d32ca540a12c6ec7b4b", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "revision" : "4ef69e67018c4bdf843858e8976c13b97c3afe4c", - "version" : "1.0.0-beta.3" + "revision" : "8bd475b24dcf18b9b03534c99c5ccf626a8d38b9", + "version" : "1.0.0-beta.4" } }, { diff --git a/Package.swift b/Package.swift index 016c569..9bdbd56 100644 --- a/Package.swift +++ b/Package.swift @@ -45,8 +45,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/vapor/sqlite-nio", from: "1.12.0"), - .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.3"), .package(url: "https://github.com/swift-server/swift-service-lifecycle", from: "2.8.0"), + .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.4"), // [docc-plugin-placeholder] ], targets: [ diff --git a/README.md b/README.md index 7ac1506..805357e 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ SQLite driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package. [ - ![Release: 1.0.0-beta.4](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E4-F05138) + ![Release: 1.0.0-beta.5](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E5-F05138) ]( - https://github.com/feather-framework/feather-sqlite-database/releases/tag/1.0.0-beta.4 + https://github.com/feather-framework/feather-sqlite-database/releases/tag/1.0.0-beta.5 ) ## Features @@ -36,7 +36,7 @@ SQLite driver implementation for the abstract [Feather Database](https://github. Add the dependency to your `Package.swift`: ```swift -.package(url: "https://github.com/feather-framework/feather-sqlite-database", exact: "1.0.0-beta.4"), +.package(url: "https://github.com/feather-framework/feather-sqlite-database", exact: "1.0.0-beta.5"), ``` Then add `FeatherSQLiteDatabase` to your target dependencies: diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift index 1db5ffa..46e9490 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseConnection.swift @@ -9,9 +9,42 @@ import FeatherDatabase import Logging import SQLiteNIO +extension DatabaseQuery { + + fileprivate struct SQLiteQuery { + var sql: String + var bindings: [SQLiteData] + } + + fileprivate func toSQLiteQuery() -> SQLiteQuery { + var sqliteSQL = sql + var sqliteBindings: [SQLiteData] = [] + + for binding in bindings { + let idx = binding.index + 1 + sqliteSQL = + sqliteSQL + .replacing("{{\(idx)}}", with: "?") + + switch binding.binding { + case .int(let value): + sqliteBindings.append(.integer(value)) + case .double(let value): + sqliteBindings.append(.float(value)) + case .string(let value): + sqliteBindings.append(.text(value)) + } + } + + return .init( + sql: sqliteSQL, + bindings: sqliteBindings + ) + } +} + public struct SQLiteDatabaseConnection: DatabaseConnection { - public typealias Query = SQLiteDatabaseQuery public typealias RowSequence = SQLiteDatabaseRowSequence var connection: SQLiteConnection @@ -27,13 +60,14 @@ public struct SQLiteDatabaseConnection: DatabaseConnection { /// - Returns: A query result containing the returned rows. @discardableResult public func run( - query: Query, + query: DatabaseQuery, _ handler: (RowSequence) async throws -> T = { $0 } ) async throws(DatabaseError) -> T { do { + let sqliteQuery = query.toSQLiteQuery() let result = try await connection.query( - query.sql, - query.bindings + sqliteQuery.sql, + sqliteQuery.bindings ) return try await handler( SQLiteDatabaseRowSequence( diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift deleted file mode 100644 index e845f76..0000000 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseQuery.swift +++ /dev/null @@ -1,199 +0,0 @@ -// -// SQLiteDatabaseQuery.swift -// feather-sqlite-database -// -// Created by Tibor Bödecs on 2026. 01. 10. -// - -import FeatherDatabase -import SQLiteNIO - -/// A SQLite query with SQL text and bound parameters. -/// -/// Use this type to construct SQLite queries safely. -public struct SQLiteDatabaseQuery: DatabaseQuery { - /// The SQL text to execute. - /// - /// This is the raw SQL string for the query. - public var sql: String - /// The bound parameters for the SQL text. - /// - /// These values are passed alongside `sql`. - public var bindings: [SQLiteData] - - /// Create a query from raw SQL and bindings. - /// - /// Prefer string interpolation initializers when possible to bind values. - /// - Parameters: - /// - sql: The raw SQL string to execute. - /// - bindings: The bound parameters for the SQL. - public init( - unsafeSQL sql: String, - bindings: [SQLiteData] = [] - ) { - self.sql = sql - self.bindings = bindings - } -} - -extension SQLiteDatabaseQuery: ExpressibleByStringInterpolation { - - /// A string interpolation builder for SQLite queries. - /// - /// Use interpolation to bind values safely into SQL text. - public struct StringInterpolation: StringInterpolationProtocol, Sendable { - - /// The string literal type used by the interpolation. - /// - /// This matches the standard `String` literal type. - public typealias StringLiteralType = String - - @usableFromInline - var sql: String - - @usableFromInline - var binds: [SQLiteData] - - /// Create a new interpolation buffer. - /// - /// Use the provided capacities to preallocate storage. - /// - Parameters: - /// - literalCapacity: The expected literal character count. - /// - interpolationCount: The expected number of interpolations. - public init( - literalCapacity: Int, - interpolationCount: Int - ) { - self.sql = "" - self.sql.reserveCapacity(literalCapacity) - self.binds = [] - self.binds.reserveCapacity(interpolationCount) - } - - /// Append a literal string segment. - /// - /// This adds raw SQL text to the builder. - /// - Parameter literal: The literal string segment. - public mutating func appendLiteral( - _ literal: String - ) { - self.sql.append(contentsOf: literal) - } - - @inlinable - /// Append an interpolated optional string value. - /// - /// Non-nil values are bound, and nil values emit `NULL`. - /// - Parameter value: The optional string value to interpolate. - /// - Returns: Nothing. - public mutating func appendInterpolation( - _ value: String? - ) { - switch value { - case .some(let value): - self.binds.append(.text(value)) - self.sql.append(contentsOf: "?") - case .none: - self.sql.append(contentsOf: "NULL") - } - } - - @inlinable - /// Append an interpolated integer value. - /// - /// The value is bound as a SQLite integer. - /// - Parameter value: The integer value to interpolate. - /// - Returns: Nothing. - public mutating func appendInterpolation( - _ value: Int - ) { - self.binds.append(.integer(value)) - self.sql.append(contentsOf: "?") - } - - @inlinable - /// Append an interpolated floating-point value. - /// - /// The value is bound as a SQLite float. - /// - Parameter value: The double value to interpolate. - /// - Returns: Nothing. - public mutating func appendInterpolation( - _ value: Double - ) { - self.binds.append(.float(value)) - self.sql.append(contentsOf: "?") - } - - @inlinable - /// Append an interpolated string value. - /// - /// The value is bound as a SQLite text value. - /// - Parameter value: The string value to interpolate. - /// - Returns: Nothing. - public mutating func appendInterpolation( - _ value: String - ) { - self.binds.append(.text(value)) - self.sql.append(contentsOf: "?") - } - - @inlinable - /// Append an interpolated SQLite data value. - /// - /// The value is bound directly as SQLite data. - /// - Parameter value: The SQLite data value to interpolate. - /// - Returns: Nothing. - public mutating func appendInterpolation( - _ value: SQLiteData - ) { - self.binds.append(value) - self.sql.append(contentsOf: "?") - } - - // @inlinable - // public mutating func appendInterpolation(_ value: SQLiteData?) throws { - // switch value { - // case .none: - // self.binds.append(.null) - // case .some(let value): - // self.binds.append(value) - // } - // - // self.sql.append(contentsOf: "?") - // } - - @inlinable - /// Append an unescaped SQL fragment. - /// - /// Use this only for trusted identifiers or SQL keywords. - /// - Parameter interpolated: The raw SQL fragment to insert. - /// - Returns: Nothing. - public mutating func appendInterpolation( - unescaped interpolated: String - ) { - self.sql.append(contentsOf: interpolated) - } - } - - /// Create a query from a string interpolation builder. - /// - /// This initializer is used by Swift string interpolation. - /// - Parameter stringInterpolation: The interpolation builder. - public init( - stringInterpolation: StringInterpolation - ) { - self.sql = stringInterpolation.sql - self.bindings = stringInterpolation.binds - } - - /// Create a query from a string literal. - /// - /// This initializer does not add any bindings. - /// - Parameter value: The literal SQL string. - public init( - stringLiteral value: String - ) { - self.sql = value - self.bindings = [] - } -} diff --git a/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift index 0b5b853..0106e5c 100644 --- a/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift @@ -273,18 +273,15 @@ struct FeatherSQLiteDatabaseTestSuite { """# ) - let insert = SQLiteDatabaseQuery( - unsafeSQL: #""" + try await connection.run( + query: #""" INSERT INTO "widgets" ("id", "name") VALUES - (?, ?); - """#, - bindings: [.integer(1), .text("gizmo")] + (\#(1), \#("gizmo")); + """# ) - try await connection.run(query: insert) - let result = try await connection.run( query: #""" @@ -317,14 +314,15 @@ struct FeatherSQLiteDatabaseTestSuite { ) let body: String? = nil - let insert: SQLiteDatabaseQuery = #""" - INSERT INTO "notes" - ("id", "body") - VALUES - (1, \#(body)); - """# - try await connection.run(query: insert) + try await connection.run( + query: #""" + INSERT INTO "notes" + ("id", "body") + VALUES + (1, \#(body)); + """# + ) let result = try await connection.run( @@ -357,15 +355,14 @@ struct FeatherSQLiteDatabaseTestSuite { """# ) - let label: SQLiteData = .text("alpha") - let insert: SQLiteDatabaseQuery = #""" - INSERT INTO "tags" - ("id", "label") - VALUES - (1, \#(label)); - """# - - try await connection.run(query: insert) + try await connection.run( + query: #""" + INSERT INTO "tags" + ("id", "label") + VALUES + (1, \#("alpha")); + """# + ) let result = try await connection.run(