diff --git a/Package.resolved b/Package.resolved index 80425a7..e59601f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "4fcf6655c10aa8b2d812570ea3b119de335eea4c9b6d66e846abf32a9765b181", + "originHash" : "f951e492bbf3c68bb512d30e55b02b36748ad1765728fae1b9bec14e2a58fb38", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "revision" : "55b08d7bd028c7eddbd231efaaaa0d38e78c0b9b", - "version" : "1.0.0-beta.5" + "revision" : "0b862ac3ee31859e2c8a54b06390ee92d1a7c0b4", + "version" : "1.0.0-rc.1" } }, { diff --git a/Package.swift b/Package.swift index d8c7500..71241d1 100644 --- a/Package.swift +++ b/Package.swift @@ -46,7 +46,7 @@ let package = Package( .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/swift-server/swift-service-lifecycle", from: "2.8.0"), - .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.5"), + .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-rc.1"), // [docc-plugin-placeholder] ], targets: [ diff --git a/README.md b/README.md index c90d491..a4e5e11 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.9](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E9-F05138) + ![Release: 1.0.0-rc.1](https://img.shields.io/badge/Release-1%2E0%2E0--rc%2E1-F05138) ]( - https://github.com/feather-framework/feather-database-sqlite/releases/tag/1.0.0-beta.9 + https://github.com/feather-framework/feather-database-sqlite/releases/tag/1.0.0-rc.1 ) ## 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-database-sqlite", exact: "1.0.0-beta.9"), +.package(url: "https://github.com/feather-framework/feather-database-sqlite", exact: "1.0.0-rc.1"), ``` Then add `FeatherDatabaseSQLite` to your target dependencies: @@ -53,7 +53,7 @@ To enable an additional trait on the package, update the package dependency: ```diff .package( url: "https://github.com/feather-framework/feather-database-sqlite", - exact: "1.0.0-beta.9", + exact: "1.0.0-rc.1", + traits: [ + .defaults, + "ServiceLifecycle", @@ -121,9 +121,6 @@ for try await item in result { await client.shutdown() ``` -> [!WARNING] -> This repository is a work in progress, things can break until it reaches v1.0.0. - ## Other database drivers The following database driver implementations are available for use: diff --git a/Tests/FeatherDatabaseSQLiteTests/FeatherDatabaseSQLiteTestSuite.swift b/Tests/FeatherDatabaseSQLiteTests/FeatherDatabaseSQLiteTestSuite.swift index 961ba2a..4797d12 100644 --- a/Tests/FeatherDatabaseSQLiteTests/FeatherDatabaseSQLiteTestSuite.swift +++ b/Tests/FeatherDatabaseSQLiteTests/FeatherDatabaseSQLiteTestSuite.swift @@ -112,14 +112,16 @@ struct FeatherDatabaseSQLiteTestSuite { let name1 = "Andromeda" let name2 = "Milky Way" + let id1: Int? = nil + let id2: Int? = nil try await connection.run( query: #""" INSERT INTO "galaxies" ("id", "name") VALUES - (\#(nil), \#(name1)), - (\#(nil), \#(name2)); + (\#(id1), \#(name1)), + (\#(id2), \#(name2)); """# ) @@ -270,6 +272,341 @@ struct FeatherDatabaseSQLiteTestSuite { } } + @Test + func boundOptionalInterpolationRoundTrip() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + let boundString: String? = "alpha" + let missingString: String? = nil + let boundInt: Int? = 21 + let missingInt: Int? = nil + let boundFloat: Float? = 1.25 + let missingFloat: Float? = nil + let boundDouble: Double? = 3.75 + let missingDouble: Double? = nil + let boundBool: Bool? = true + let missingBool: Bool? = nil + + struct OptionalRow: Sendable { + let boundString: String? + let missingString: String? + let boundInt: Int? + let missingInt: Int? + let boundFloat: Double? + let missingFloat: Double? + let boundDouble: Double? + let missingDouble: Double? + let boundBool: Bool? + let missingBool: Bool? + + init(_ row: DatabaseRow) throws { + self.boundString = try row.decode( + column: "bound_string", + as: String?.self + ) + self.missingString = try row.decode( + column: "missing_string", + as: String?.self + ) + self.boundInt = try row.decode( + column: "bound_int", + as: Int?.self + ) + self.missingInt = try row.decode( + column: "missing_int", + as: Int?.self + ) + self.boundFloat = try row.decode( + column: "bound_float", + as: Double?.self + ) + self.missingFloat = try row.decode( + column: "missing_float", + as: Double?.self + ) + self.boundDouble = try row.decode( + column: "bound_double", + as: Double?.self + ) + self.missingDouble = try row.decode( + column: "missing_double", + as: Double?.self + ) + self.boundBool = try row.decode( + column: "bound_bool", + as: Bool?.self + ) + self.missingBool = try row.decode( + column: "missing_bool", + as: Bool?.self + ) + } + } + + let result = try await connection.run( + query: #""" + SELECT + \#(boundString) AS "bound_string", + \#(missingString) AS "missing_string", + \#(boundInt) AS "bound_int", + \#(missingInt) AS "missing_int", + \#(boundFloat) AS "bound_float", + \#(missingFloat) AS "missing_float", + \#(boundDouble) AS "bound_double", + \#(missingDouble) AS "missing_double", + \#(boundBool) AS "bound_bool", + \#(missingBool) AS "missing_bool"; + """# + ) { try await $0.collect().map { try OptionalRow($0) } } + + #expect(result.count == 1) + #expect(result[0].boundString == "alpha") + #expect(result[0].missingString == nil) + #expect(result[0].boundInt == 21) + #expect(result[0].missingInt == nil) + #expect(result[0].boundFloat == 1.25) + #expect(result[0].missingFloat == nil) + #expect(result[0].boundDouble == 3.75) + #expect(result[0].missingDouble == nil) + #expect(result[0].boundBool == true) + #expect(result[0].missingBool == nil) + } + } + } + + @Test + func unescapedOptionalInterpolationRoundTrip() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "fragments" ( + "alpha" TEXT NOT NULL + ); + """# + ) + try await connection.run( + query: #""" + INSERT INTO "fragments" + ("alpha") + VALUES + ('omega'); + """# + ) + + let rawColumn: String? = "alpha" + let missingColumn: String? = nil + let rawInt: Int? = 7 + let missingInt: Int? = nil + let rawFloat: Float? = 2.5 + let missingFloat: Float? = nil + let rawDouble: Double? = 4.5 + let missingDouble: Double? = nil + let rawBool: Bool? = true + let missingBool: Bool? = nil + + struct RawOptionalRow: Sendable { + let rawColumn: String? + let missingColumn: String? + let rawInt: Int? + let missingInt: Int? + let rawFloat: Double? + let missingFloat: Double? + let rawDouble: Double? + let missingDouble: Double? + let rawBool: Bool? + let missingBool: Bool? + + init(_ row: DatabaseRow) throws { + self.rawColumn = try row.decode( + column: "raw_column", + as: String?.self + ) + self.missingColumn = try row.decode( + column: "missing_column", + as: String?.self + ) + self.rawInt = try row.decode( + column: "raw_int", + as: Int?.self + ) + self.missingInt = try row.decode( + column: "missing_int", + as: Int?.self + ) + self.rawFloat = try row.decode( + column: "raw_float", + as: Double?.self + ) + self.missingFloat = try row.decode( + column: "missing_float", + as: Double?.self + ) + self.rawDouble = try row.decode( + column: "raw_double", + as: Double?.self + ) + self.missingDouble = try row.decode( + column: "missing_double", + as: Double?.self + ) + self.rawBool = try row.decode( + column: "raw_bool", + as: Bool?.self + ) + self.missingBool = try row.decode( + column: "missing_bool", + as: Bool?.self + ) + } + } + + let result = try await connection.run( + query: #""" + SELECT + \#(unescaped: rawColumn) AS "raw_column", + \#(unescaped: missingColumn) AS "missing_column", + \#(unescaped: rawInt) AS "raw_int", + \#(unescaped: missingInt) AS "missing_int", + \#(unescaped: rawFloat) AS "raw_float", + \#(unescaped: missingFloat) AS "missing_float", + \#(unescaped: rawDouble) AS "raw_double", + \#(unescaped: missingDouble) AS "missing_double", + \#(unescaped: rawBool) AS "raw_bool", + \#(unescaped: missingBool) AS "missing_bool" + FROM "fragments"; + """# + ) { try await $0.collect().map { try RawOptionalRow($0) } } + + #expect(result.count == 1) + #expect(result[0].rawColumn == "omega") + #expect(result[0].missingColumn == nil) + #expect(result[0].rawInt == 7) + #expect(result[0].missingInt == nil) + #expect(result[0].rawFloat == 2.5) + #expect(result[0].missingFloat == nil) + #expect(result[0].rawDouble == 4.5) + #expect(result[0].missingDouble == nil) + #expect(result[0].rawBool == true) + #expect(result[0].missingBool == nil) + } + } + } + + @Test + func arrayInterpolationRoundTrip() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "array_samples" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "ratio" REAL NOT NULL, + "score" REAL NOT NULL, + "active" INTEGER NOT NULL + ); + """# + ) + try await connection.run( + query: #""" + INSERT INTO "array_samples" + ("id", "name", "ratio", "score", "active") + VALUES + (1, 'alpha', 1.5, 3.5, 1), + (2, 'beta', 2.25, 4.75, 0); + """# + ) + + let names = ["alpha", "omega"] + let ids = [1, 99] + let ratios: [Float] = [1.5, 9.5] + let scores = [3.5, 9.75] + let flags = [true, false] + + let result = try await connection.run( + query: #""" + SELECT + "id", + "name" + FROM "array_samples" + WHERE + "name" IN (\#(names)) + AND "id" IN (\#(ids)) + AND "ratio" IN (\#(ratios)) + AND "score" IN (\#(scores)) + AND "active" IN (\#(flags)) + ORDER BY "id"; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect(try result[0].decode(column: "id", as: Int.self) == 1) + #expect( + try result[0].decode(column: "name", as: String.self) + == "alpha" + ) + } + } + } + + @Test + func optionalArrayInterpolationRoundTrip() async throws { + try await runUsingTestDatabaseClient { database in + try await database.withConnection { connection in + try await connection.run( + query: #""" + CREATE TABLE "optional_array_samples" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "ratio" REAL NOT NULL, + "score" REAL NOT NULL, + "active" INTEGER NOT NULL + ); + """# + ) + try await connection.run( + query: #""" + INSERT INTO "optional_array_samples" + ("id", "name", "ratio", "score", "active") + VALUES + (1, 'alpha', 1.5, 3.5, 1), + (2, 'beta', 2.25, 4.75, 0); + """# + ) + + let names: [String?] = ["alpha", nil, "omega"] + let ids: [Int?] = [1, nil, 99] + let ratios: [Float?] = [1.5, nil, 9.5] + let scores: [Double?] = [3.5, nil, 9.75] + let flags: [Bool?] = [true, nil, false] + + let result = try await connection.run( + query: #""" + SELECT + "id", + "name" + FROM "optional_array_samples" + WHERE + "name" IN (\#(names)) + AND "id" IN (\#(ids)) + AND "ratio" IN (\#(ratios)) + AND "score" IN (\#(scores)) + AND "active" IN (\#(flags)) + ORDER BY "id"; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect(try result[0].decode(column: "id", as: Int.self) == 1) + #expect( + try result[0].decode(column: "name", as: String.self) + == "alpha" + ) + } + } + } + @Test func unsafeSQLBindings() async throws { try await runUsingTestDatabaseClient { database in