diff --git a/Package.resolved b/Package.resolved index 2d3964e..35206b7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "3e791080143c7c1715f4ff6a4ff3902d3ac67b0f705edede755fdf0d0720f749", + "originHash" : "a58fb97a0766d3d4769b071c86939776c571582ca30a531d8305d0f82af2a1bb", "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 2667d51..c1b28cc 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/vapor/mysql-nio", from: "1.8.0"), - .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.3"), + .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 34751db..c775cb5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ MySQL/MariaDB driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package. -[![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138)](https://github.com/feather-framework/feather-mysql-database/releases/tag/1.0.0-beta.2) +[ + ![Release: 1.0.0-beta.3](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E3-F05138) +]( + https://github.com/feather-framework/feather-mysql-database/releases/tag/1.0.0-beta.3 +) ## Features @@ -32,7 +36,7 @@ MySQL/MariaDB driver implementation for the abstract [Feather Database](https:// Add the dependency to your `Package.swift`: ```swift -.package(url: "https://github.com/feather-framework/feather-mysql-database", exact: "1.0.0-beta.2"), +.package(url: "https://github.com/feather-framework/feather-mysql-database", exact: "1.0.0-beta.3"), ``` Then add `FeatherMySQLDatabase` to your target dependencies: diff --git a/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift index 160324f..2c72b89 100644 --- a/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift @@ -9,9 +9,42 @@ import FeatherDatabase import MySQLNIO import NIOCore +extension DatabaseQuery { + + fileprivate struct MySQLQuery { + var sql: String + var bindings: [MySQLData] + } + + fileprivate func toMySQLQuery() -> MySQLQuery { + var mysqlSQL = sql + var mysqlBindings: [MySQLData] = [] + + for binding in bindings { + let idx = binding.index + 1 + mysqlSQL = + mysqlSQL + .replacing("{{\(idx)}}", with: "?") + + switch binding.binding { + case .int(let value): + mysqlBindings.append(.init(int: value)) + case .double(let value): + mysqlBindings.append(.init(double: value)) + case .string(let value): + mysqlBindings.append(.init(string: value)) + } + } + + return .init( + sql: mysqlSQL, + bindings: mysqlBindings + ) + } +} + public struct MySQLDatabaseConnection: DatabaseConnection, Sendable { - public typealias Query = MySQLQuery public typealias RowSequence = MySQLRowSequence let connection: MySQLNIO.MySQLConnection @@ -27,14 +60,15 @@ public struct MySQLDatabaseConnection: DatabaseConnection, Sendable { /// - 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 mysqlQuery = query.toMySQLQuery() let rows = try await connection.query( - query.sql, - query.bindings + mysqlQuery.sql, + mysqlQuery.bindings ) .get() diff --git a/Sources/FeatherMySQLDatabase/MySQLDatabaseQuery.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseQuery.swift deleted file mode 100644 index cfa3551..0000000 --- a/Sources/FeatherMySQLDatabase/MySQLDatabaseQuery.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// MySQLDatabaseQuery.swift -// feather-mysql-database -// -// Created by Tibor Bödecs on 2026. 01. 10. -// - -import FeatherDatabase -import MySQLNIO - -/// A MySQL query with SQL text and bound parameters. -/// -/// Use this type to construct MySQL queries safely. -public struct MySQLQuery: 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: [MySQLData] - - /// 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: [MySQLData] = [] - ) { - self.sql = sql - self.bindings = bindings - } -} - -extension MySQLQuery: ExpressibleByStringInterpolation { - - /// A string interpolation builder for MySQL 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: [MySQLData] - - /// 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. - public mutating func appendInterpolation( - _ value: String? - ) { - switch value { - case .some(let value): - self.binds.append(.init(string: value)) - self.sql.append(contentsOf: "?") - case .none: - self.sql.append(contentsOf: "NULL") - } - } - - @inlinable - /// Append an interpolated integer value. - /// - /// The value is bound as a MySQL integer. - /// - Parameter value: The integer value to interpolate. - public mutating func appendInterpolation( - _ value: Int - ) { - self.binds.append(.init(int: value)) - self.sql.append(contentsOf: "?") - } - - @inlinable - /// Append an interpolated floating-point value. - /// - /// The value is bound as a MySQL double. - /// - Parameter value: The double value to interpolate. - public mutating func appendInterpolation( - _ value: Double - ) { - self.binds.append(.init(double: value)) - self.sql.append(contentsOf: "?") - } - - @inlinable - /// Append an interpolated string value. - /// - /// The value is bound as a MySQL string. - /// - Parameter value: The string value to interpolate. - public mutating func appendInterpolation( - _ value: String - ) { - self.binds.append(.init(string: value)) - self.sql.append(contentsOf: "?") - } - - @inlinable - /// Append an interpolated MySQL data value. - /// - /// The value is bound directly as MySQL data. - /// - Parameter value: The MySQL data value to interpolate. - public mutating func appendInterpolation( - _ value: MySQLData - ) { - 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. - 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/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift b/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift index 8152bdf..a32be61 100644 --- a/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift +++ b/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift @@ -418,18 +418,15 @@ struct MySQLDatabaseTestSuite { """# ) - let insert = MySQLQuery( - unsafeSQL: #""" - INSERT INTO `\#(table)` + try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: table)` (`id`, `name`) VALUES - (?, ?); - """#, - bindings: [.init(int: 1), .init(string: "gizmo")] + (\#(1), \#("gizmo")); + """# ) - try await connection.run(query: insert) - let result = try await connection.run( query: #""" @@ -471,14 +468,15 @@ struct MySQLDatabaseTestSuite { ) let body: String? = nil - let insert: MySQLQuery = #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `body`) - VALUES - (1, \#(body)); - """# - try await connection.run(query: insert) + try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: table)` + (`id`, `body`) + VALUES + (1, \#(body)); + """# + ) let result = try await connection.run( @@ -520,15 +518,14 @@ struct MySQLDatabaseTestSuite { """# ) - let label: MySQLData = .init(string: "alpha") - let insert: MySQLQuery = #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `label`) - VALUES - (1, \#(label)); - """# - - try await connection.run(query: insert) + try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: table)` + (`id`, `label`) + VALUES + (1, \#("alpha")); + """# + ) let result = try await connection.run(