From d15559594ccc450d32fb39dde932cc4b6ca85759 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Sat, 9 Aug 2025 11:16:50 +0200 Subject: [PATCH 1/7] Fix `SQLCastExpression` for MySQL --- .../Expressions/SQLCast+FluentKeypaths.swift | 11 +---- .../Expressions/SQLCastExpression.swift | 42 +++++++------------ .../FluentSQLKitExtrasTests.swift | 8 +++- .../SQLKitExtrasTests/SQLKitExtrasTests.swift | 8 +++- 4 files changed, 29 insertions(+), 40 deletions(-) diff --git a/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift b/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift index 549c9a3..585919e 100644 --- a/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift +++ b/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift @@ -7,16 +7,7 @@ extension SQLExpression { /// for the desired type. public static func cast( _ column: KeyPath, - to type: some StringProtocol - ) -> Self where Self == SQLCastExpression { - .cast(.column(column), to: type) - } - - /// Convenience method for creating a ``SQLCastExpression`` expression using a Fluent keypath for the value and an expression - /// for the desired type. - public static func cast( - _ column: KeyPath, - to type: some SQLExpression + to type: String ) -> Self where Self == SQLCastExpression { .cast(.column(column), to: type) } diff --git a/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift b/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift index d2e596e..4ded761 100644 --- a/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift +++ b/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift @@ -6,31 +6,27 @@ public struct SQLCastExpression: SQLExpression { public let original: any SQLExpression /// The desired type to cast the original expression to. - public let desiredType: any SQLExpression - - /// Create a new ``SQLCastExpression``. - /// - /// - Parameters: - /// - original: The original expression to be cast. - /// - desiredType: The desired type to cast the original expression to. - public init(expr: some SQLExpression, castType: some SQLExpression) { - self.original = expr - self.desiredType = castType - } + public let desiredType: String /// Convenience initializer for creating a ``SQLCastExpression`` with a string for the original expression and a string for the desired type. /// /// - Parameters: /// - original: The original expression to be cast. /// - desiredType: The desired type to cast the original expression to, represented as a string. - public init(_ original: some SQLExpression, to type: some StringProtocol) { - self.init(expr: original, castType: .identifier(type)) + public init(_ original: some SQLExpression, to type: String) { + self.original = original + self.desiredType = type } /// See `SQLExpression.serialize(to:)`. public func serialize(to serializer: inout SQLSerializer) { - SQLFunction("CAST", args: SQLAlias(self.original, as: self.desiredType)) - .serialize(to: &serializer) + if serializer.dialect.name == "mysql" { + SQLFunction("CAST", args: SQLAlias(self.original, as: SQLRaw(self.desiredType))) + .serialize(to: &serializer) + } else { + SQLFunction("CAST", args: SQLAlias(self.original, as: SQLIdentifier(self.desiredType))) + .serialize(to: &serializer) + } } } @@ -38,24 +34,16 @@ extension SQLExpression { /// Convenience method for creating a ``SQLCastExpression`` using a column name and a string for the desired type. public static func cast( _ column: some StringProtocol, - to type: some StringProtocol + to type: String ) -> Self where Self == SQLCastExpression { - .cast(.column(column), to: .identifier(type)) + .cast(.column(column), to: type) } /// Convenience method for creating a ``SQLCastExpression`` using a column name and a string for the desired type. public static func cast( _ column: some SQLExpression, - to type: some StringProtocol - ) -> Self where Self == SQLCastExpression { - .cast(column, to: .identifier(type)) - } - - /// Convenience method for creating a ``SQLCastExpression`` using a column name and an expression for the desired type. - public static func cast( - _ column: some SQLExpression, - to type: some SQLExpression + to type: String ) -> Self where Self == SQLCastExpression { - .init(expr: column, castType: type) + .init(column, to: type) } } diff --git a/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift b/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift index 41388ca..a7d9b66 100644 --- a/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift +++ b/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift @@ -301,7 +301,13 @@ struct FluentSQLKitExtrasTests { @Test func castExpression() { #expect(serialize(.cast(\FooModel.$field, to: "text")) == #"CAST("foos"."field" AS "text")"#) - #expect(serialize(.cast(\FooModel.$field, to: .unsafeRaw("text"))) == #"CAST("foos"."field" AS text)"#) + + let mysqlDB = MockSQLDatabase(dialect: "mysql") + let castExpr: any SQLExpression = .cast(\FooModel.$field, to: "text") + #expect(mysqlDB.serialize(castExpr).sql == #"CAST("foos"."field" AS text)"#) + + let postgresDB = MockSQLDatabase(dialect: "postgresql") + #expect(postgresDB.serialize(castExpr).sql == #"CAST("foos"."field" AS "text")"#) } } } diff --git a/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift b/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift index f4eac2b..82fd17c 100644 --- a/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift +++ b/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift @@ -254,11 +254,15 @@ struct SQLKitExtrasTests { @Test func castExpression() { #expect(serialize(SQLCastExpression(.column("foo"), to: "text")) == #"CAST("foo" AS "text")"#) - #expect(serialize(SQLCastExpression(expr: .column("foo"), castType: .unsafeRaw("text"))) == #"CAST("foo" AS text)"#) #expect(serialize(.cast("foo", to: "text")) == #"CAST("foo" AS "text")"#) #expect(serialize(.cast(.column("foo"), to: "text")) == #"CAST("foo" AS "text")"#) - #expect(serialize(.cast(.column("foo"), to: .unsafeRaw("text"))) == #"CAST("foo" AS text)"#) + + let mysqlDB = MockSQLDatabase(dialect: "mysql") + #expect(mysqlDB.serialize(SQLCastExpression(.column("foo"), to: "text")).sql == #"CAST("foo" AS text)"#) + + let postgresDB = MockSQLDatabase(dialect: "postgresql") + #expect(postgresDB.serialize(SQLCastExpression(.column("foo"), to: "text")).sql == #"CAST("foo" AS "text")"#) } @Test From 9211d9b794f9533b555baa8b6e6268d8c3e75318 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Sat, 9 Aug 2025 11:56:26 +0200 Subject: [PATCH 2/7] Use Gwynne's approach --- .../Expressions/SQLCast+FluentKeypaths.swift | 9 ++++ .../Expressions/SQLCastExpression.swift | 44 ++++++++++++++----- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift b/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift index 585919e..f958e57 100644 --- a/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift +++ b/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift @@ -11,5 +11,14 @@ extension SQLExpression { ) -> Self where Self == SQLCastExpression { .cast(.column(column), to: type) } + + /// Convenience method for creating a ``SQLCastExpression`` expression using a Fluent keypath for the value and an expression + /// for the desired type. + public static func cast( + _ column: KeyPath, + to type: some SQLExpression + ) -> Self where Self == SQLCastExpression { + .cast(.column(column), to: type) + } } #endif diff --git a/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift b/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift index 4ded761..d389d03 100644 --- a/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift +++ b/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift @@ -6,27 +6,41 @@ public struct SQLCastExpression: SQLExpression { public let original: any SQLExpression /// The desired type to cast the original expression to. - public let desiredType: String + public let desiredType: any SQLExpression + + /// Create a new ``SQLCastExpression``. + /// + /// - Parameters: + /// - original: The original expression to be cast. + /// - desiredType: The desired type to cast the original expression to. + public init(expr: some SQLExpression, castType: some SQLExpression) { + self.original = expr + self.desiredType = castType + } /// Convenience initializer for creating a ``SQLCastExpression`` with a string for the original expression and a string for the desired type. /// /// - Parameters: /// - original: The original expression to be cast. /// - desiredType: The desired type to cast the original expression to, represented as a string. - public init(_ original: some SQLExpression, to type: String) { - self.original = original - self.desiredType = type + public init(_ original: some SQLExpression, to type: some StringProtocol) { + self.init(expr: original, castType: .identifier(type)) } /// See `SQLExpression.serialize(to:)`. public func serialize(to serializer: inout SQLSerializer) { - if serializer.dialect.name == "mysql" { - SQLFunction("CAST", args: SQLAlias(self.original, as: SQLRaw(self.desiredType))) - .serialize(to: &serializer) + let desiredType: any SQLExpression = if + serializer.dialect.name == "mysql", + let ident = self.desiredType as? SQLIdentifier, + ident.string.allSatisfy({ $0.isASCII && ($0.isLowercase || $0.isUppercase || $0.isWholeNumber || $0 == "_") }) + { + SQLRaw(ident.string) } else { - SQLFunction("CAST", args: SQLAlias(self.original, as: SQLIdentifier(self.desiredType))) - .serialize(to: &serializer) + self.desiredType } + + SQLFunction("CAST", args: SQLAlias(self.original, as: desiredType)) + .serialize(to: &serializer) } } @@ -34,7 +48,7 @@ extension SQLExpression { /// Convenience method for creating a ``SQLCastExpression`` using a column name and a string for the desired type. public static func cast( _ column: some StringProtocol, - to type: String + to type: some StringProtocol ) -> Self where Self == SQLCastExpression { .cast(.column(column), to: type) } @@ -42,8 +56,16 @@ extension SQLExpression { /// Convenience method for creating a ``SQLCastExpression`` using a column name and a string for the desired type. public static func cast( _ column: some SQLExpression, - to type: String + to type: some StringProtocol ) -> Self where Self == SQLCastExpression { .init(column, to: type) } + + /// Convenience method for creating a ``SQLCastExpression`` using a column name and an expression for the desired type. + public static func cast( + _ column: some SQLExpression, + to type: some SQLExpression + ) -> Self where Self == SQLCastExpression { + .init(expr: column, castType: type) + } } From 2d757a38f454378841f6b435ecb51956a9ed7011 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Sat, 9 Aug 2025 11:57:53 +0200 Subject: [PATCH 3/7] Nits --- .../Expressions/SQLCast+FluentKeypaths.swift | 2 +- .../SQLKitExtras/Expressions/SQLCastExpression.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift b/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift index f958e57..549c9a3 100644 --- a/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift +++ b/Sources/SQLKitExtras/FluentSQLKitExtras/Expressions/SQLCast+FluentKeypaths.swift @@ -7,7 +7,7 @@ extension SQLExpression { /// for the desired type. public static func cast( _ column: KeyPath, - to type: String + to type: some StringProtocol ) -> Self where Self == SQLCastExpression { .cast(.column(column), to: type) } diff --git a/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift b/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift index d389d03..e207476 100644 --- a/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift +++ b/Sources/SQLKitExtras/SQLKitExtras/Expressions/SQLCastExpression.swift @@ -33,7 +33,7 @@ public struct SQLCastExpression: SQLExpression { serializer.dialect.name == "mysql", let ident = self.desiredType as? SQLIdentifier, ident.string.allSatisfy({ $0.isASCII && ($0.isLowercase || $0.isUppercase || $0.isWholeNumber || $0 == "_") }) - { + { SQLRaw(ident.string) } else { self.desiredType @@ -50,7 +50,7 @@ extension SQLExpression { _ column: some StringProtocol, to type: some StringProtocol ) -> Self where Self == SQLCastExpression { - .cast(.column(column), to: type) + .cast(.column(column), to: .identifier(type)) } /// Convenience method for creating a ``SQLCastExpression`` using a column name and a string for the desired type. @@ -58,7 +58,7 @@ extension SQLExpression { _ column: some SQLExpression, to type: some StringProtocol ) -> Self where Self == SQLCastExpression { - .init(column, to: type) + .cast(column, to: .identifier(type)) } /// Convenience method for creating a ``SQLCastExpression`` using a column name and an expression for the desired type. From 9dcf067db754694a1cb21cd0f294f817d785abfc Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 25 Aug 2025 10:59:03 -0500 Subject: [PATCH 4/7] Fixup test --- Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift b/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift index a7d9b66..d833753 100644 --- a/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift +++ b/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift @@ -302,12 +302,8 @@ struct FluentSQLKitExtrasTests { func castExpression() { #expect(serialize(.cast(\FooModel.$field, to: "text")) == #"CAST("foos"."field" AS "text")"#) - let mysqlDB = MockSQLDatabase(dialect: "mysql") - let castExpr: any SQLExpression = .cast(\FooModel.$field, to: "text") - #expect(mysqlDB.serialize(castExpr).sql == #"CAST("foos"."field" AS text)"#) - - let postgresDB = MockSQLDatabase(dialect: "postgresql") - #expect(postgresDB.serialize(castExpr).sql == #"CAST("foos"."field" AS "text")"#) + #expect(MockSQLDatabase(dialect: "mysql").serialize(.cast(\FooModel.$field, to: "text")).sql == #"CAST("foos"."field" AS text)"#) + #expect(MockSQLDatabase(dialect: "postgresql").serialize(.cast(\FooModel.$field, to: "text")).sql == #"CAST("foos"."field" AS "text")"#) } } } From 758f92d3535e9cd0fe9b877c80c3b91de0e640e5 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 25 Aug 2025 10:59:49 -0500 Subject: [PATCH 5/7] Fixup test --- Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift b/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift index 82fd17c..8ac8cbb 100644 --- a/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift +++ b/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift @@ -258,11 +258,8 @@ struct SQLKitExtrasTests { #expect(serialize(.cast("foo", to: "text")) == #"CAST("foo" AS "text")"#) #expect(serialize(.cast(.column("foo"), to: "text")) == #"CAST("foo" AS "text")"#) - let mysqlDB = MockSQLDatabase(dialect: "mysql") - #expect(mysqlDB.serialize(SQLCastExpression(.column("foo"), to: "text")).sql == #"CAST("foo" AS text)"#) - - let postgresDB = MockSQLDatabase(dialect: "postgresql") - #expect(postgresDB.serialize(SQLCastExpression(.column("foo"), to: "text")).sql == #"CAST("foo" AS "text")"#) + #expect(MockSQLDatabase(dialect: "mysql").serialize(SQLCastExpression(.column("foo"), to: "text")).sql == #"CAST("foo" AS text)"#) + #expect(MockSQLDatabase(dialect: "postgresql").serialize(SQLCastExpression(.column("foo"), to: "text")).sql == #"CAST("foo" AS "text")"#) } @Test From ae0366b2156dc17dd9f21ca5ebfa9af8a4bbcb13 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 25 Aug 2025 11:04:35 -0500 Subject: [PATCH 6/7] Put back missing test --- Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift b/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift index d833753..56c1acc 100644 --- a/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift +++ b/Tests/SQLKitExtrasTests/FluentSQLKitExtrasTests.swift @@ -301,6 +301,7 @@ struct FluentSQLKitExtrasTests { @Test func castExpression() { #expect(serialize(.cast(\FooModel.$field, to: "text")) == #"CAST("foos"."field" AS "text")"#) + #expect(serialize(.cast(\FooModel.$field, to: .unsafeRaw("text"))) == #"CAST("foos"."field" AS text)"#) #expect(MockSQLDatabase(dialect: "mysql").serialize(.cast(\FooModel.$field, to: "text")).sql == #"CAST("foos"."field" AS text)"#) #expect(MockSQLDatabase(dialect: "postgresql").serialize(.cast(\FooModel.$field, to: "text")).sql == #"CAST("foos"."field" AS "text")"#) From 023d2037da308b794e170d10714978d63b3c6dd8 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 25 Aug 2025 11:05:35 -0500 Subject: [PATCH 7/7] Put back missing tests --- Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift b/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift index 8ac8cbb..0a39cb5 100644 --- a/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift +++ b/Tests/SQLKitExtrasTests/SQLKitExtrasTests.swift @@ -254,9 +254,11 @@ struct SQLKitExtrasTests { @Test func castExpression() { #expect(serialize(SQLCastExpression(.column("foo"), to: "text")) == #"CAST("foo" AS "text")"#) + #expect(serialize(SQLCastExpression(expr: .column("foo"), castType: .unsafeRaw("text"))) == #"CAST("foo" AS text)"#) #expect(serialize(.cast("foo", to: "text")) == #"CAST("foo" AS "text")"#) #expect(serialize(.cast(.column("foo"), to: "text")) == #"CAST("foo" AS "text")"#) + #expect(serialize(.cast(.column("foo"), to: .unsafeRaw("text"))) == #"CAST("foo" AS text)"#) #expect(MockSQLDatabase(dialect: "mysql").serialize(SQLCastExpression(.column("foo"), to: "text")).sql == #"CAST("foo" AS text)"#) #expect(MockSQLDatabase(dialect: "postgresql").serialize(SQLCastExpression(.column("foo"), to: "text")).sql == #"CAST("foo" AS "text")"#)