diff --git a/Sources/SQLKit/Builders/Implementations/SQLDeleteBuilder.swift b/Sources/SQLKit/Builders/Implementations/SQLDeleteBuilder.swift index 93e2f413..5912bb5b 100644 --- a/Sources/SQLKit/Builders/Implementations/SQLDeleteBuilder.swift +++ b/Sources/SQLKit/Builders/Implementations/SQLDeleteBuilder.swift @@ -1,5 +1,5 @@ /// Builds ``SQLDelete`` queries. -public final class SQLDeleteBuilder: SQLQueryBuilder, SQLPredicateBuilder, SQLReturningBuilder { +public final class SQLDeleteBuilder: SQLQueryBuilder, SQLPredicateBuilder, SQLReturningBuilder, SQLCommonTableExpressionBuilder { /// ``SQLDelete`` query being built. public var delete: SQLDelete @@ -26,6 +26,13 @@ public final class SQLDeleteBuilder: SQLQueryBuilder, SQLPredicateBuilder, SQLRe set { self.delete.returning = newValue } } + // See `SQLCommonTableExpressionBuilder.tableExpressionGroup`. + @inlinable + public var tableExpressionGroup: SQLCommonTableExpressionGroup? { + get { self.delete.tableExpressionGroup } + set { self.delete.tableExpressionGroup = newValue } + } + /// Create a new ``SQLDeleteBuilder``. @inlinable public init(_ delete: SQLDelete, on database: any SQLDatabase) { diff --git a/Sources/SQLKit/Builders/Implementations/SQLInsertBuilder.swift b/Sources/SQLKit/Builders/Implementations/SQLInsertBuilder.swift index 5ca04987..cb16b50e 100644 --- a/Sources/SQLKit/Builders/Implementations/SQLInsertBuilder.swift +++ b/Sources/SQLKit/Builders/Implementations/SQLInsertBuilder.swift @@ -5,7 +5,7 @@ /// > ``SQLInsertBuilder``'s otherwise-identical public APIs overwrite the effects of any previous invocation. It /// > would ideally be preferable to change ``SQLInsertBuilder``'s semantics in this regard, but this would be a /// > significant breaking change in the API's behavior, and must therefore wait for a major version bump. -public final class SQLInsertBuilder: SQLQueryBuilder, SQLReturningBuilder/*, SQLUnqualifiedColumnListBuilder*/ { +public final class SQLInsertBuilder: SQLQueryBuilder, SQLReturningBuilder/*, SQLUnqualifiedColumnListBuilder*/, SQLCommonTableExpressionBuilder { /// The ``SQLInsert`` query this builder builds. public var insert: SQLInsert @@ -25,6 +25,13 @@ public final class SQLInsertBuilder: SQLQueryBuilder, SQLReturningBuilder/*, SQL set { self.insert.returning = newValue } } + // See `SQLCommonTableExpressionBuilder.tableExpressionGroup`. + @inlinable + public var tableExpressionGroup: SQLCommonTableExpressionGroup? { + get { self.insert.tableExpressionGroup } + set { self.insert.tableExpressionGroup = newValue } + } + /// Creates a new `SQLInsertBuilder`. @inlinable public init(_ insert: SQLInsert, on database: any SQLDatabase) { diff --git a/Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift b/Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift index 16fa00d0..2e72cd82 100644 --- a/Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift +++ b/Sources/SQLKit/Builders/Implementations/SQLUnionBuilder.swift @@ -1,5 +1,5 @@ /// Builds top-level ``SQLUnion`` queries which may be executed on their own. -public final class SQLUnionBuilder: SQLQueryBuilder, SQLQueryFetcher, SQLCommonUnionBuilder { +public final class SQLUnionBuilder: SQLQueryBuilder, SQLQueryFetcher, SQLCommonUnionBuilder, SQLCommonTableExpressionBuilder { // See `SQLCommonUnionBuilder.union`. public var union: SQLUnion @@ -12,6 +12,13 @@ public final class SQLUnionBuilder: SQLQueryBuilder, SQLQueryFetcher, SQLCommonU self.union } + // See `SQLCommonTableExpressionBuilder.tableExpressionGroup`. + @inlinable + public var tableExpressionGroup: SQLCommonTableExpressionGroup? { + get { self.union.tableExpressionGroup } + set { self.union.tableExpressionGroup = newValue } + } + /// Create a new ``SQLUnionBuilder``. @inlinable public init(on database: any SQLDatabase, initialQuery: SQLSelect) { diff --git a/Sources/SQLKit/Builders/Implementations/SQLUpdateBuilder.swift b/Sources/SQLKit/Builders/Implementations/SQLUpdateBuilder.swift index 502cfda7..1acc493e 100644 --- a/Sources/SQLKit/Builders/Implementations/SQLUpdateBuilder.swift +++ b/Sources/SQLKit/Builders/Implementations/SQLUpdateBuilder.swift @@ -1,5 +1,5 @@ /// Builds ``SQLUpdate`` queries. -public final class SQLUpdateBuilder: SQLQueryBuilder, SQLPredicateBuilder, SQLReturningBuilder, SQLColumnUpdateBuilder { +public final class SQLUpdateBuilder: SQLQueryBuilder, SQLPredicateBuilder, SQLReturningBuilder, SQLColumnUpdateBuilder, SQLCommonTableExpressionBuilder { /// An ``SQLUpdate`` containing the complete current state of the builder. public var update: SQLUpdate @@ -33,6 +33,13 @@ public final class SQLUpdateBuilder: SQLQueryBuilder, SQLPredicateBuilder, SQLRe set { self.update.returning = newValue } } + // See `SQLCommonTableExpressionBuilder.tableExpressionGroup`. + @inlinable + public var tableExpressionGroup: SQLCommonTableExpressionGroup? { + get { self.update.tableExpressionGroup } + set { self.update.tableExpressionGroup = newValue } + } + /// Create a new ``SQLUpdateBuilder``. /// /// Use this API directly only if you need to have control over the builder's initial update query. Prefer using diff --git a/Sources/SQLKit/Builders/Prototypes/SQLCommonTableExpressionBuilder.swift b/Sources/SQLKit/Builders/Prototypes/SQLCommonTableExpressionBuilder.swift new file mode 100644 index 00000000..49c52464 --- /dev/null +++ b/Sources/SQLKit/Builders/Prototypes/SQLCommonTableExpressionBuilder.swift @@ -0,0 +1,323 @@ +/// Common definitions for query builders which support Common Table Expressions. +public protocol SQLCommonTableExpressionBuilder: AnyObject { + /// An optional group of common table expressions to include in the query under construction. + var tableExpressionGroup: SQLCommonTableExpressionGroup? { get set } +} + +extension SQLCommonTableExpressionBuilder { + // MARK: - String name, string columns + + /// Specify a subquery to include as a common table expression, for use elsewhere in the overall query. + /// + /// Example usage: + /// ```swift + /// try await sqlDatabase.update("table1") + /// .with("c", columns: ["a"], as: SQLSubquery.select {$0 + /// .column("x") + /// .from("table3") + /// }) + /// .set("foo", to: "bar") + /// .where("foo", .equal, SQLColumn("a", table: "c")) + /// .run() + /// ``` + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate + /// > that a non-recursive CTE's query is not self-referential. It is the responsibility of the user to invoke + /// > the appropriate variant of this method. Failure to do so will result in generating invalid SQL. + /// + /// - Parameters: + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE, usually a `SELECT` query. + @inlinable + @discardableResult + public func with(_ name: some StringProtocol, columns: [String], as query: some SQLExpression) -> Self { + self.with(name, columns: columns.map(SQLIdentifier.init(_:)), as: query) + } + + /// Specify a subquery to include as a _recursive_ common table expression, for use elsewhere in + /// the overall query. + /// + /// Example usage: + /// ```swift + /// try await sqlDatabase.update("table1") + /// .with(recursive: "c", columns: ["n"], as: SQLSubquery + /// .union { $0.column(SQLBind("1"), as: "n") } + /// .union(all: { $0 + /// .column(SQLBinaryExpression("n", .add, 1)) + /// .from("c").where("n", .lessThan, 3) + /// }).finish()) + /// .set("foo", to: "bar") + /// .where("foo", .equal, SQLColumn("n", table: "c")) + /// .run() + /// ``` + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate + /// > that a recursive CTE's query takes the proper form. It is the responsibility of the user to invoke the + /// > appropriate variant of this method. Failure to do so will result in generating invalid SQL. + /// + /// - Parameters: + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE. For a recursive CTE, this must be an + /// expression representing at least one `SELECT` statement which does _not_ refer to the CTE and at least + /// one `UNION ALL` or `UNION DISTINCT` clause terminating with a `SELECT` statement which explicitly refers + /// to the CTE itself. + @inlinable + @discardableResult + public func with(recursive name: some StringProtocol, columns: [String], as query: some SQLExpression) -> Self { + self.with(recursive: name, columns: columns.map(SQLIdentifier.init(_:)), as: query) + } + + // MARK: - String name, expression columns + + /// Specify a subquery to include as a common table expression, for use elsewhere in the overall query. + /// + /// Example usage: + /// ```swift + /// try await sqlDatabase.update("table1") + /// .with("c", columns: ["a"], as: SQLSubquery.select {$0 + /// .column("x") + /// .from("table3") + /// }) + /// .set("foo", to: "bar") + /// .where("foo", .equal, SQLColumn("a", table: "c")) + /// .run() + /// ``` + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate + /// > that a non-recursive CTE's query is not self-referential. It is the responsibility of the user to invoke + /// > the appropriate variant of this method. Failure to do so will result in generating invalid SQL. + /// + /// - Parameters: + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE, usually a `SELECT` query. + @inlinable + @discardableResult + public func with(_ name: some StringProtocol, columns: [any SQLExpression] = [], as query: some SQLExpression) -> Self { + self.with(SQLIdentifier(String(name)), columns: columns, as: query) + } + + /// Specify a subquery to include as a _recursive_ common table expression, for use elsewhere in + /// the overall query. + /// + /// Example usage: + /// ```swift + /// try await sqlDatabase.update("table1") + /// .with(recursive: "c", columns: ["n"], as: SQLSubquery + /// .union { $0.column(SQLBind("1"), as: "n") } + /// .union(all: { $0 + /// .column(SQLBinaryExpression("n", .add, 1)) + /// .from("c").where("n", .lessThan, 3) + /// }).finish()) + /// .set("foo", to: "bar") + /// .where("foo", .equal, SQLColumn("n", table: "c")) + /// .run() + /// ``` + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate + /// > that a recursive CTE's query takes the proper form. It is the responsibility of the user to invoke the + /// > appropriate variant of this method. Failure to do so will result in generating invalid SQL. + /// + /// - Parameters: + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE. For a recursive CTE, this must be an + /// expression representing at least one `SELECT` statement which does _not_ refer to the CTE and at least + /// one `UNION ALL` or `UNION DISTINCT` clause terminating with a `SELECT` statement which explicitly refers + /// to the CTE itself. + @inlinable + @discardableResult + public func with(recursive name: some StringProtocol, columns: [any SQLExpression] = [], as query: some SQLExpression) -> Self { + self.with(recursive: SQLIdentifier(String(name)), columns: columns, as: query) + } + + // MARK: - Expression name, string columns + + /// Specify a subquery to include as a common table expression, for use elsewhere in the overall query. + /// + /// Example usage: + /// ```swift + /// try await sqlDatabase.update("table1") + /// .with("c", columns: ["a"], as: SQLSubquery.select {$0 + /// .column("x") + /// .from("table3") + /// }) + /// .set("foo", to: "bar") + /// .where("foo", .equal, SQLColumn("a", table: "c")) + /// .run() + /// ``` + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate + /// > that a non-recursive CTE's query is not self-referential. It is the responsibility of the user to invoke + /// > the appropriate variant of this method. Failure to do so will result in generating invalid SQL. + /// + /// - Parameters: + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE, usually a `SELECT` query. + @inlinable + @discardableResult + public func with(_ name: some SQLExpression, columns: [String], as query: some SQLExpression) -> Self { + self.with(name, columns: columns.map(SQLIdentifier.init(_:)), as: query) + } + + /// Specify a subquery to include as a _recursive_ common table expression, for use elsewhere in + /// the overall query. + /// + /// Example usage: + /// ```swift + /// try await sqlDatabase.update("table1") + /// .with(recursive: "c", columns: ["n"], as: SQLSubquery + /// .union { $0.column(SQLBind("1"), as: "n") } + /// .union(all: { $0 + /// .column(SQLBinaryExpression("n", .add, 1)) + /// .from("c").where("n", .lessThan, 3) + /// }).finish()) + /// .set("foo", to: "bar") + /// .where("foo", .equal, SQLColumn("n", table: "c")) + /// .run() + /// ``` + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate + /// > that a recursive CTE's query takes the proper form. It is the responsibility of the user to invoke the + /// > appropriate variant of this method. Failure to do so will result in generating invalid SQL. + /// + /// - Parameters: + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE. For a recursive CTE, this must be an + /// expression representing at least one `SELECT` statement which does _not_ refer to the CTE and at least + /// one `UNION ALL` or `UNION DISTINCT` clause terminating with a `SELECT` statement which explicitly refers + /// to the CTE itself. + @inlinable + @discardableResult + public func with(recursive name: some SQLExpression, columns: [String], as query: some SQLExpression) -> Self { + self.with(recursive: name, columns: columns.map(SQLIdentifier.init(_:)), as: query) + } + + // MARK: - Expression name, expression columns + + /// Specify a subquery to include as a common table expression, for use elsewhere in the overall query. + /// + /// Example usage: + /// ```swift + /// try await sqlDatabase.update("table1") + /// .with("c", columns: ["a"], as: SQLSubquery.select {$0 + /// .column("x") + /// .from("table3") + /// }) + /// .set("foo", to: "bar") + /// .where("foo", .equal, SQLColumn("a", table: "c")) + /// .run() + /// ``` + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate + /// > that a non-recursive CTE's query is not self-referential. It is the responsibility of the user to invoke + /// > the appropriate variant of this method. Failure to do so will result in generating invalid SQL. + /// + /// - Parameters: + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE, usually a `SELECT` query. + @inlinable + @discardableResult + public func with(_ name: some SQLExpression, columns: [any SQLExpression] = [], as query: some SQLExpression) -> Self { + self.with(isRecursive: false, name: name, columns: columns, as: query) + } + + /// Specify a subquery to include as a _recursive_ common table expression, for use elsewhere in + /// the overall query. + /// + /// Example usage: + /// ```swift + /// try await sqlDatabase.update("table1") + /// .with(recursive: "c", columns: ["n"], as: SQLSubquery + /// .union { $0.column(SQLBind("1"), as: "n") } + /// .union(all: { $0 + /// .column(SQLBinaryExpression("n", .add, 1)) + /// .from("c").where("n", .lessThan, 3) + /// }).finish()) + /// .set("foo", to: "bar") + /// .where("foo", .equal, SQLColumn("n", table: "c")) + /// .run() + /// ``` + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ + /// > validate that a recursive CTE's query takes the proper form. It is the responsibility of the user to + /// > invoke the appropriate variant of this method. Failure to do so will result in generating invalid SQL. + /// + /// - Parameters: + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE. For a recursive CTE, this must be an + /// expression representing at least one `SELECT` statement which does _not_ refer to the CTE and at least + /// one `UNION ALL` or `UNION DISTINCT` clause terminating with a `SELECT` statement which explicitly refers + /// to the CTE itself. + @inlinable + @discardableResult + public func with(recursive name: some SQLExpression, columns: [any SQLExpression] = [], as query: some SQLExpression) -> Self { + self.with(isRecursive: true, name: name, columns: columns, as: query) + } + + // MARK: - Funnel + + /// Specify a potentially-recursive common table expression for use elsewhere in a query. + /// + /// This is the common "funnel" method invoked by all other methods provided by + /// ``SQLCommonTableExpressionBuilder``. Most users will not need to call this method directly. + /// + /// See ``with(_:columns:as)`` and ``with(recursive:_:columns:as:)`` for usage examples. + /// + /// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate + /// > that a recursive CTE's query takes the proper form, nor that a non-recursive CTE's query is not + /// > self-referential. It is the responsibility of the user to specify the flag accurately. Failure to do so + /// > will result in generating invalid SQL. + /// + /// - Parameters: + /// - isRecursive: Specifies whether or not the CTE is recursive. + /// - name: The name to assign to the query's results. + /// - columns: An optional list of unqualified column names to use for referencing the query's results. + /// If no column names are provided, the names are inferred from the query. If column names are provided, + /// the number of names provided must match the number of columns returned by the query. + /// - query: An expression which provides the contents of the CTE. If the CTE is recursive, this must be an + /// expression representing at least one `SELECT` statement which does _not_ refer to the CTE and at least + /// one `UNION ALL` or `UNION DISTINCT` clause terminating with a `SELECT` statement which explicitly refers + /// to the CTE itself. + @inlinable + @discardableResult + public func with( + isRecursive: Bool, + name: some SQLExpression, + columns: [any SQLExpression] = [], + as query: some SQLExpression + ) -> Self { + var expression = SQLCommonTableExpression(alias: name, query: query) + expression.isRecursive = isRecursive + expression.columns = columns + + if self.tableExpressionGroup != nil { + self.tableExpressionGroup?.tableExpressions.append(expression) + } else { + self.tableExpressionGroup = .init(tableExpressions: [expression]) + } + return self + } +} diff --git a/Sources/SQLKit/Builders/Prototypes/SQLSubqueryClauseBuilder.swift b/Sources/SQLKit/Builders/Prototypes/SQLSubqueryClauseBuilder.swift index 844185a3..3f183e9f 100644 --- a/Sources/SQLKit/Builders/Prototypes/SQLSubqueryClauseBuilder.swift +++ b/Sources/SQLKit/Builders/Prototypes/SQLSubqueryClauseBuilder.swift @@ -9,7 +9,7 @@ /// expressions in other queries. /// /// > Important: Despite the use of the term "subquery", this builder does not provide -/// > methods for specifying subquery operators (e.g. `ANY`, `SOME`) or CTE clauses (`WITH`), +/// > methods for specifying subquery operators (e.g. `ANY`, `SOME`), /// > nor does it enclose its result in grouping parenthesis, as all of these formations are /// > context-specific and are the purview of builders that conform to this protocol. /// @@ -21,7 +21,8 @@ public protocol SQLSubqueryClauseBuilder: SQLPredicateBuilder, SQLSecondaryPredicateBuilder, SQLPartialResultBuilder, - SQLAliasedColumnListBuilder + SQLAliasedColumnListBuilder, + SQLCommonTableExpressionBuilder { /// The ``SQLSelect`` query under construction. var select: SQLSelect { get set } @@ -69,6 +70,13 @@ extension SQLSubqueryClauseBuilder { get { self.select.columns } set { self.select.columns = newValue } } + + // See `SQLCommonTableExpressionBuilder.tableExpressionGroup`. + @inlinable + public var tableExpressionGroup: SQLCommonTableExpressionGroup? { + get { self.select.tableExpressionGroup } + set { self.select.tableExpressionGroup = newValue } + } } // MARK: - Distinct diff --git a/Sources/SQLKit/Expressions/Clauses/SQLCommonTableExpression.swift b/Sources/SQLKit/Expressions/Clauses/SQLCommonTableExpression.swift new file mode 100644 index 00000000..0629c0d7 --- /dev/null +++ b/Sources/SQLKit/Expressions/Clauses/SQLCommonTableExpression.swift @@ -0,0 +1,85 @@ +/// A clause describing a single Common Table Expressions, which in itws simplest form provides +/// additional data to a primary query in the same way as joining to a subquery. +/// +/// > Note: There is no ``SQLDialect`` flag for CTE support, as CTEs are supported by all of the +/// > databases for which first-party drivers exist at the time of this writing (although they are +/// > not available in MySQL 5.7, which is long since EOL and should not be in use by anyone anymore). +public struct SQLCommonTableExpression: SQLExpression { + /// Indicates whether the CTE is recursive, e.g. whether its query is a `UNION` whose second subquery + /// refers to the CTE's own aliased name. + /// + /// > Warning: Neither ``SQLCommonTableExpression`` nor the methods of ``SQLCommonTableExpressionBuilder`` + /// > validate that a recursive CTE's query takes the proper form, nor that a non-recursive CTE's query + /// > is not self-referential. It is the responsibility of the user to specify the flag accurately. Failure + /// > to do so will result in generating invalid SQL. + public var isRecursive: Bool = false + + /// The name used to refer to the CTE's data. + public var alias: any SQLExpression + + /// A list of column names yielded by the CTE. May be empty. + public var columns: [any SQLExpression] = [] + + /// The subquery which yields the CTE's data. + public var query: any SQLExpression + + /// Create a new Common Table Expression. + /// + /// - Parameters: + /// - alias: Specifies the name to be used to refer to the CTE. + /// - query: The subquery which yields the CTE's data. + public init(alias: some SQLExpression, query: some SQLExpression) { + self.alias = alias + self.query = query + } + + // See `SQLExpression.serialize(to:)`. + public func serialize(to serializer: inout SQLSerializer) { + serializer.statement { + /// The ``SQLCommonTableExpression/isRecursive`` flag is not used in this logic. This is not an + /// oversight. CTE syntax requires that `RECURSIVE` be specified as part of the overall `WITH` + /// clause, rather on a per-CTE basis. As such, the recursive flag is handled by the serialization + /// logic of ``SQLCommonTableExpressionGroup``. + $0.append(self.alias) + if !self.columns.isEmpty { + $0.append(SQLGroupExpression(self.columns)) + } + if let subqueryExpr = self.query as? SQLSubquery { + $0.append("AS", subqueryExpr) + } else if let subqueryExpr = self.query as? SQLUnionSubquery { + $0.append("AS", subqueryExpr) + } else if let groupExpr = self.query as? SQLGroupExpression { + $0.append("AS", groupExpr) + } else { + $0.append("AS", SQLGroupExpression(self.query)) + } + } + } +} + +/// A clause representing a group of one or more ``SQLCommonTableExpression``s. +/// +/// This expression makes up a complete `WITH` clause in the generated SQL, serving to centralize the +/// serialization logic for such a clause in a single location rather than requiring it to be repeated +/// by every query type that supports CTEs. +public struct SQLCommonTableExpressionGroup: SQLExpression { + /// The list of common table expressions which make up the group. + /// + /// Must contain at least one expression. If the list is empty, invalid SQL will be generated. + public var tableExpressions: [any SQLExpression] + + public init(tableExpressions: [any SQLExpression]) { + self.tableExpressions = tableExpressions + } + + // See `SQLExpression.serialize(to:)`. + public func serialize(to serializer: inout SQLSerializer) { + serializer.statement { + $0.append("WITH") + if self.tableExpressions.contains(where: { ($0 as? SQLCommonTableExpression)?.isRecursive ?? false }) { + $0.append("RECURSIVE") + } + $0.append(SQLList(self.tableExpressions)) + } + } +} diff --git a/Sources/SQLKit/Expressions/Queries/SQLDelete.swift b/Sources/SQLKit/Expressions/Queries/SQLDelete.swift index c7b234da..535bea9d 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLDelete.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLDelete.swift @@ -8,6 +8,9 @@ /// /// See ``SQLDeleteBuilder``. public struct SQLDelete: SQLExpression { + /// An optional common table expression group. + public var tableExpressionGroup: SQLCommonTableExpressionGroup? + /// The table containing rows to delete. public var table: any SQLExpression @@ -34,6 +37,8 @@ public struct SQLDelete: SQLExpression { // See `SQLExpression.serialize(to:)`. public func serialize(to serializer: inout SQLSerializer) { serializer.statement { + $0.append(self.tableExpressionGroup) + $0.append("DELETE FROM", self.table) if let predicate = self.predicate { $0.append("WHERE", predicate) diff --git a/Sources/SQLKit/Expressions/Queries/SQLInsert.swift b/Sources/SQLKit/Expressions/Queries/SQLInsert.swift index 3389f6f0..220f6ab9 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLInsert.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLInsert.swift @@ -22,6 +22,9 @@ /// /// See ``SQLInsertBuilder``. public struct SQLInsert: SQLExpression { + /// An optional common table expression group. + public var tableExpressionGroup: SQLCommonTableExpressionGroup? + /// The table to which rows are to be added. public var table: any SQLExpression @@ -70,6 +73,7 @@ public struct SQLInsert: SQLExpression { // See `SQLExpression.serialize(to:)`. public func serialize(to serializer: inout SQLSerializer) { serializer.statement { + $0.append(self.tableExpressionGroup) $0.append("INSERT") $0.append(self.conflictStrategy?.queryModifier(for: $0)) $0.append("INTO", self.table) diff --git a/Sources/SQLKit/Expressions/Queries/SQLSelect.swift b/Sources/SQLKit/Expressions/Queries/SQLSelect.swift index 10adf8bc..6d51d44c 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLSelect.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLSelect.swift @@ -29,6 +29,9 @@ /// /// See ``SQLSelectBuilder``. public struct SQLSelect: SQLExpression { + /// An optional common table expression group. + public var tableExpressionGroup: SQLCommonTableExpressionGroup? + /// One or more expessions describing the data to retrieve from the database. public var columns: [any SQLExpression] = [] @@ -97,6 +100,7 @@ public struct SQLSelect: SQLExpression { // See `SQLExpression.serialize(to:)`. public func serialize(to serializer: inout SQLSerializer) { serializer.statement { + $0.append(self.tableExpressionGroup) $0.append("SELECT") if self.isDistinct { $0.append("DISTINCT") diff --git a/Sources/SQLKit/Expressions/Queries/SQLUnion.swift b/Sources/SQLKit/Expressions/Queries/SQLUnion.swift index c15c3168..f1f37111 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLUnion.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLUnion.swift @@ -17,6 +17,9 @@ /// /// See ``SQLUnionBuilder``. public struct SQLUnion: SQLExpression { + /// An optional common table expression group. + public var tableExpressionGroup: SQLCommonTableExpressionGroup? + /// The required first query of the union. public var initialQuery: SQLSelect @@ -80,6 +83,8 @@ public struct SQLUnion: SQLExpression { // See `SQLExpression.serialize(to:)`. public func serialize(to serializer: inout SQLSerializer) { serializer.statement { stmt in + stmt.append(self.tableExpressionGroup) + guard !self.unions.isEmpty else { /// If no unions are specified, serialize as a plain query even if the dialect would otherwise /// specify the use of parenthesized subqueries. Ignores orderBys, limit, and offset. diff --git a/Sources/SQLKit/Expressions/Queries/SQLUpdate.swift b/Sources/SQLKit/Expressions/Queries/SQLUpdate.swift index 7709e674..f6a79951 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLUpdate.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLUpdate.swift @@ -16,6 +16,9 @@ /// /// See ``SQLUpdateBuilder``. public struct SQLUpdate: SQLExpression { + /// An optional common table expression group. + public var tableExpressionGroup: SQLCommonTableExpressionGroup? + /// The table containing the row(s) to be updated. public var table: any SQLExpression @@ -43,6 +46,7 @@ public struct SQLUpdate: SQLExpression { // See `SQLExpression.serialize(to:)`. public func serialize(to serializer: inout SQLSerializer) { serializer.statement { + $0.append(self.tableExpressionGroup) $0.append("UPDATE", self.table) $0.append("SET", SQLList(self.values)) if let predicate = self.predicate { diff --git a/Tests/SQLKitTests/SQLCodingTests.swift b/Tests/SQLKitTests/SQLCodingTests.swift index 32de909a..1058d149 100644 --- a/Tests/SQLKitTests/SQLCodingTests.swift +++ b/Tests/SQLKitTests/SQLCodingTests.swift @@ -265,7 +265,7 @@ func superCase(_ path: [any CodingKey]) -> any CodingKey { SomeCodingKey(stringValue: path.last!.stringValue.encapitalized) } -extension SQLLiteral: Equatable { +extension SQLKit.SQLLiteral: Swift.Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.all, .all), (.`default`, .`default`), (.null, .null): return true @@ -277,7 +277,7 @@ extension SQLLiteral: Equatable { } } -extension SQLBind: Equatable { +extension SQLKit.SQLBind: Swift.Equatable { // Don't do this. This is horrible. public static func == (lhs: Self, rhs: Self) -> Bool { (try? JSONEncoder().encode(lhs.encodable) == JSONEncoder().encode(rhs.encodable)) ?? false } } diff --git a/Tests/SQLKitTests/SQLCommonTableExpressionTests.swift b/Tests/SQLKitTests/SQLCommonTableExpressionTests.swift new file mode 100644 index 00000000..24adcd4a --- /dev/null +++ b/Tests/SQLKitTests/SQLCommonTableExpressionTests.swift @@ -0,0 +1,303 @@ +import SQLKit +import XCTest + +final class SQLCommonTableExpressionTests: XCTestCase { + var db = TestDatabase() + + override class func setUp() { + XCTAssert(isLoggingConfigured) + } + + func testSelectQueryWithCTE() { + XCTAssertSerialization( + of: self.db.select().with("a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).column(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) SELECT ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.select().with(SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).column(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) SELECT ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.select().with("a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).column(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) SELECT ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.select().with(SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).column(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) SELECT ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.select().with(recursive: "a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).column(SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) SELECT ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.select().with(recursive: SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).column(SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) SELECT ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.select().with(recursive: "a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).column(SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) SELECT ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.select().with(recursive: SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).column(SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) SELECT ``a``.``b``" + ) + } + + func testUpdateQueryWithCTE() { + XCTAssertSerialization( + of: self.db.update("t").with("a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).set("c", to: SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) UPDATE ``t`` SET ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.update("t").with(SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).set("c", to: SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) UPDATE ``t`` SET ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.update("t").with("a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).set("c", to: SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) UPDATE ``t`` SET ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.update("t").with(SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).set("c", to: SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) UPDATE ``t`` SET ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.update("t").with(recursive: "a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).set("c", to: SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) UPDATE ``t`` SET ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.update("t").with(recursive: SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).set("c", to: SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) UPDATE ``t`` SET ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.update("t").with(recursive: "a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).set("c", to: SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) UPDATE ``t`` SET ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.update("t").with(recursive: SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).set("c", to: SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) UPDATE ``t`` SET ``c`` = ``a``.``b``" + ) + } + + func testInsertQueryWithCTE() { + XCTAssertSerialization( + of: self.db.insert(into: "t").with("a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).columns("c").values(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) INSERT INTO ``t`` (``c``) VALUES (``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.insert(into: "t").with(SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).columns("c").values(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) INSERT INTO ``t`` (``c``) VALUES (``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.insert(into: "t").with("a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).columns("c").values(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) INSERT INTO ``t`` (``c``) VALUES (``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.insert(into: "t").with(SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).columns("c").values(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) INSERT INTO ``t`` (``c``) VALUES (``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.insert(into: "t").with(recursive: "a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).columns("c").values(SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) INSERT INTO ``t`` (``c``) VALUES (``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.insert(into: "t").with(recursive: SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).columns("c").values(SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) INSERT INTO ``t`` (``c``) VALUES (``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.insert(into: "t").with(recursive: "a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).columns("c").values(SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) INSERT INTO ``t`` (``c``) VALUES (``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.insert(into: "t").with(recursive: SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).columns("c").values(SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) INSERT INTO ``t`` (``c``) VALUES (``a``.``b``)" + ) + } + + func testDeleteQueryWithCTE() { + XCTAssertSerialization( + of: self.db.delete(from: "t").with("a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).where("c", .equal, SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) DELETE FROM ``t`` WHERE ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.delete(from: "t").with(SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).where("c", .equal, SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) DELETE FROM ``t`` WHERE ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.delete(from: "t").with("a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).where("c", .equal, SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) DELETE FROM ``t`` WHERE ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.delete(from: "t").with(SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).where("c", .equal, SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1) DELETE FROM ``t`` WHERE ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.delete(from: "t").with(recursive: "a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).where("c", .equal, SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) DELETE FROM ``t`` WHERE ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.delete(from: "t").with(recursive: SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).where("c", .equal, SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) DELETE FROM ``t`` WHERE ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.delete(from: "t").with(recursive: "a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).where("c", .equal, SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) DELETE FROM ``t`` WHERE ``c`` = ``a``.``b``" + ) + + XCTAssertSerialization( + of: self.db.delete(from: "t").with(recursive: SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).where("c", .equal, SQLColumn("b", table: "a")), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) DELETE FROM ``t`` WHERE ``c`` = ``a``.``b``" + ) + } + + func testUnionQueryWithCTE() { + self.db._dialect.unionFeatures = [.union, .unionAll, .intersect, .intersectAll, .except, .exceptAll, .parenthesizedSubqueries] + + XCTAssertSerialization( + of: self.db.union { $0 }.with("a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).union(all: { $0.column(SQLColumn("b", table: "a")) }), + is: "WITH ``a`` (``b``) AS (SELECT 1) (SELECT) UNION ALL (SELECT ``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.union { $0 }.with(SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).union(all: { $0.column(SQLColumn("b", table: "a")) }), + is: "WITH ``a`` (``b``) AS (SELECT 1) (SELECT) UNION ALL (SELECT ``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.union { $0 }.with("a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).union(all: { $0.column(SQLColumn("b", table: "a")) }), + is: "WITH ``a`` (``b``) AS (SELECT 1) (SELECT) UNION ALL (SELECT ``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.union { $0 }.with(SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).union(all: { $0.column(SQLColumn("b", table: "a")) }), + is: "WITH ``a`` (``b``) AS (SELECT 1) (SELECT) UNION ALL (SELECT ``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.union { $0 }.with(recursive: "a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).union(all: { $0.column(SQLColumn("b", table: "a")) }), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) (SELECT) UNION ALL (SELECT ``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.union { $0 }.with(recursive: SQLIdentifier("a"), columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).union(all: { $0.column(SQLColumn("b", table: "a")) }), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) (SELECT) UNION ALL (SELECT ``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.union { $0 }.with(recursive: "a", columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).union(all: { $0.column(SQLColumn("b", table: "a")) }), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) (SELECT) UNION ALL (SELECT ``a``.``b``)" + ) + + XCTAssertSerialization( + of: self.db.union { $0 }.with(recursive: SQLIdentifier("a"), columns: [SQLIdentifier("b")], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }).union(all: { $0.column(SQLColumn("b", table: "a")) }), + is: "WITH RECURSIVE ``a`` (``b``) AS (SELECT 1) (SELECT) UNION ALL (SELECT ``a``.``b``)" + ) + } + + func testMultipleCTEs() { + XCTAssertSerialization( + of: self.db.select() + .with("a", columns: ["b"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }) + .with("d", columns: ["e"], as: SQLSubquery.select { $0.column(SQLLiteral.numeric("1")) }) + .column(SQLColumn("b", table: "a")), + is: "WITH ``a`` (``b``) AS (SELECT 1), ``d`` (``e``) AS (SELECT 1) SELECT ``a``.``b``" + ) + } + + func testCodeCoverage() { + var query = self.db.select().column(SQLColumn("b", table: "a")).select + query.tableExpressionGroup = .init(tableExpressions: [ + SQLCommonTableExpression(alias: SQLIdentifier("x"), query: SQLRaw("VALUES(``1``)")), + SQLCommonTableExpression(alias: SQLIdentifier("x"), query: SQLGroupExpression(SQLRaw("VALUES(``1``)"))), + SQLGroupExpression(SQLRaw("FOO")) + ]) + + XCTAssertSerialization(of: self.db.raw("\(query)"), is: "WITH ``x`` AS (VALUES(``1``)), ``x`` AS (VALUES(``1``)), (FOO) SELECT ``a``.``b``") + } + + func testMoreRealisticCTEs() { + // Simple sub-SELECT avoidance + // Taken from https://www.postgresql.org/docs/16/queries-with.html#QUERIES-WITH-SELECT + self.db._dialect.identifierQuote = SQLRaw("\"") + XCTAssertSerialization( + of: self.db.select() + .with("regional_sales", as: SQLSubquery.select { $0 + .column("region").column(SQLFunction("SUM", args: SQLColumn("amount")), as: "total_sales").from("orders").groupBy("region") + }) + .with("top_regions", as: SQLSubquery.select { $0 + .column("region").from("regional_sales") + .where("total_sales", .greaterThan, SQLSubquery.select { $0 + .column(SQLBinaryExpression(SQLFunction("SUM", args: SQLIdentifier("total_sales")), .divide, SQLLiteral.numeric("10"))) + .from("regional_sales") + }) + }) + .columns("region", "product") + .column(SQLFunction("SUM", args: SQLColumn("quantity")), as: "product_units") + .column(SQLFunction("SUM", args: SQLColumn("amount")), as: "product_sales") + .from("orders").where("region", .in, SQLSubquery.select { $0.column("region").from("top_regions") }).groupBy("region").groupBy("product"), + is: """ + WITH + "regional_sales" AS (SELECT "region", SUM("amount") AS "total_sales" FROM "orders" GROUP BY "region"), + "top_regions" AS (SELECT "region" FROM "regional_sales" WHERE "total_sales" > (SELECT SUM("total_sales") / 10 FROM "regional_sales")) + SELECT "region", "product", SUM("quantity") AS "product_units", SUM("amount") AS "product_sales" + FROM "orders" + WHERE "region" IN (SELECT "region" FROM "top_regions") + GROUP BY "region", "product" + """.replacing("\n", with: " ") + ) + + // Fibonacci series generator + // Taken from https://dev.mysql.com/doc/refman/8.4/en/with.html#common-table-expressions-recursive-fibonacci-series + self.db._dialect.identifierQuote = SQLRaw("`") + self.db._dialect.unionFeatures = [.union, .unionAll] + XCTAssertSerialization( + of: self.db.select() + .with(recursive: "fibonacci", columns: ["n", "fib_n", "next_fib_n"], as: SQLSubquery.union { $0 + .columns(SQLLiteral.numeric("1"), SQLLiteral.numeric("0"), SQLLiteral.numeric("1")) + }.union(all: { $0 + .column(SQLBinaryExpression(SQLColumn("n"), .add, SQLLiteral.numeric("1"))) + .column("next_fib_n") + .column(SQLBinaryExpression(SQLColumn("fib_n"), .add, SQLColumn("next_fib_n"))) + .from("fibonacci") + .where("n", .lessThan, SQLLiteral.numeric("10")) + }) + .finish()) + .column(SQLLiteral.all) + .from("fibonacci"), + is: """ + WITH RECURSIVE `fibonacci` (`n`, `fib_n`, `next_fib_n`) AS ( + SELECT 1, 0, 1 + UNION ALL + SELECT `n` + 1, `next_fib_n`, `fib_n` + `next_fib_n` FROM `fibonacci` WHERE `n` < 10 + ) + SELECT * FROM `fibonacci` + """.replacing(" ", with: "").replacing("(\n", with: "(").replacing("\n)", with: ")").replacing("\n", with: " ") + ) + } +} diff --git a/Tests/SQLKitTests/TestMocks.swift b/Tests/SQLKitTests/TestMocks.swift index c1276045..ae9405f1 100644 --- a/Tests/SQLKitTests/TestMocks.swift +++ b/Tests/SQLKitTests/TestMocks.swift @@ -138,7 +138,7 @@ struct GenericDialect: SQLDialect { } } -extension SQLDataType: Equatable { +extension SQLKit.SQLDataType: Swift.Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.bigint, .bigint), (.blob, .blob), (.int, .int), (.real, .real),