From c7275ab011a9692480997326966e07d97fd20c93 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Sat, 7 Feb 2026 16:07:52 +0100 Subject: [PATCH 1/3] basic query currency type implementation --- Package.resolved | 6 +-- Package.swift | 3 +- .../MySQLDatabaseConnection.swift | 48 ++++++++++++++++--- .../FeatherMySQLDatabaseTestSuite.swift | 29 +++++------ 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/Package.resolved b/Package.resolved index 2d3964e..09f3fcc 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "3e791080143c7c1715f4ff6a4ff3902d3ac67b0f705edede755fdf0d0720f749", + "originHash" : "c0353d0aea4db0806491f3c20ae23e322ce73ff4dbdbb4bbd2640e6f61870ac3", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "revision" : "4ef69e67018c4bdf843858e8976c13b97c3afe4c", - "version" : "1.0.0-beta.3" + "branch" : "feature/query-type", + "revision" : "a47dc8198f2427349156cc3619e017b75a9c584a" } }, { diff --git a/Package.swift b/Package.swift index 2667d51..c162e16 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,8 @@ 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.3"), + .package(url: "https://github.com/feather-framework/feather-database", branch: "feature/query-type"), // [docc-plugin-placeholder] ], targets: [ diff --git a/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift index 160324f..9832b4d 100644 --- a/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift @@ -9,9 +9,43 @@ import FeatherDatabase import MySQLNIO import NIOCore +extension Query { + + fileprivate struct MySQLQuery { + var sql: String + var bindings: [MySQLData] + } + + fileprivate func toMySQLQuery() -> MySQLQuery { + var mysqlSQL = sql + var mysqlBindings: [MySQLData] = [] + + for binding in bindings { + /// postgres binding index starts with 1 + 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 @@ -31,12 +65,12 @@ public struct MySQLDatabaseConnection: DatabaseConnection, Sendable { _ handler: (RowSequence) async throws -> T = { $0 } ) async throws(DatabaseError) -> T { do { - let rows = - try await connection.query( - query.sql, - query.bindings - ) - .get() + let mysqlQuery = query.toMySQLQuery() + let rows = try await connection.query( + mysqlQuery.sql, + mysqlQuery.bindings + ) + .get() return try await handler( MySQLRowSequence( diff --git a/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift b/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift index 8152bdf..7821a62 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 = #""" + + try await connection.run( + query: #""" INSERT INTO `\#(unescaped: table)` (`id`, `body`) VALUES (1, \#(body)); """# - - try await connection.run(query: insert) + ) let result = try await connection.run( @@ -520,15 +518,14 @@ struct MySQLDatabaseTestSuite { """# ) - let label: MySQLData = .init(string: "alpha") - let insert: MySQLQuery = #""" + try await connection.run( + query: #""" INSERT INTO `\#(unescaped: table)` (`id`, `label`) VALUES - (1, \#(label)); + (1, \#("alpha")); """# - - try await connection.run(query: insert) + ) let result = try await connection.run( From 2e3e21b86fd4033e5eaa65964f784e8b2ed272b5 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Sat, 7 Feb 2026 16:30:43 +0100 Subject: [PATCH 2/3] remove unused code --- README.md | 8 +- .../MySQLDatabaseConnection.swift | 16 +- .../MySQLDatabaseQuery.swift | 181 ------------------ .../FeatherMySQLDatabaseTestSuite.swift | 22 +-- 4 files changed, 25 insertions(+), 202 deletions(-) delete mode 100644 Sources/FeatherMySQLDatabase/MySQLDatabaseQuery.swift 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 9832b4d..b30cad5 100644 --- a/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift @@ -21,9 +21,9 @@ extension Query { var mysqlBindings: [MySQLData] = [] for binding in bindings { - /// postgres binding index starts with 1 let idx = binding.index + 1 - mysqlSQL = mysqlSQL + mysqlSQL = + mysqlSQL .replacing("{{\(idx)}}", with: "?") switch binding.binding { @@ -43,7 +43,6 @@ extension Query { } } - public struct MySQLDatabaseConnection: DatabaseConnection, Sendable { public typealias RowSequence = MySQLRowSequence @@ -66,11 +65,12 @@ public struct MySQLDatabaseConnection: DatabaseConnection, Sendable { ) async throws(DatabaseError) -> T { do { let mysqlQuery = query.toMySQLQuery() - let rows = try await connection.query( - mysqlQuery.sql, - mysqlQuery.bindings - ) - .get() + let rows = + try await connection.query( + mysqlQuery.sql, + mysqlQuery.bindings + ) + .get() return try await handler( MySQLRowSequence( 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 7821a62..a32be61 100644 --- a/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift +++ b/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift @@ -468,14 +468,14 @@ struct MySQLDatabaseTestSuite { ) let body: String? = nil - + try await connection.run( query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `body`) - VALUES - (1, \#(body)); - """# + INSERT INTO `\#(unescaped: table)` + (`id`, `body`) + VALUES + (1, \#(body)); + """# ) let result = @@ -520,11 +520,11 @@ struct MySQLDatabaseTestSuite { try await connection.run( query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `label`) - VALUES - (1, \#("alpha")); - """# + INSERT INTO `\#(unescaped: table)` + (`id`, `label`) + VALUES + (1, \#("alpha")); + """# ) let result = From 28ab124f4f34810323b8a493ab2df0a2b99fab16 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Sat, 7 Feb 2026 17:37:52 +0100 Subject: [PATCH 3/3] prep for release --- Package.resolved | 6 +++--- Package.swift | 3 +-- Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Package.resolved b/Package.resolved index 09f3fcc..35206b7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "c0353d0aea4db0806491f3c20ae23e322ce73ff4dbdbb4bbd2640e6f61870ac3", + "originHash" : "a58fb97a0766d3d4769b071c86939776c571582ca30a531d8305d0f82af2a1bb", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "branch" : "feature/query-type", - "revision" : "a47dc8198f2427349156cc3619e017b75a9c584a" + "revision" : "8bd475b24dcf18b9b03534c99c5ccf626a8d38b9", + "version" : "1.0.0-beta.4" } }, { diff --git a/Package.swift b/Package.swift index c162e16..c1b28cc 100644 --- a/Package.swift +++ b/Package.swift @@ -37,8 +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", branch: "feature/query-type"), + .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.4"), // [docc-plugin-placeholder] ], targets: [ diff --git a/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift index b30cad5..2c72b89 100644 --- a/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift @@ -9,7 +9,7 @@ import FeatherDatabase import MySQLNIO import NIOCore -extension Query { +extension DatabaseQuery { fileprivate struct MySQLQuery { var sql: String @@ -60,7 +60,7 @@ 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 {