-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Add support for the WITH clause #1139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
12e2166
03b5688
76d34dd
bdacf4d
ced1851
ed43285
0dfc7b0
cfa6010
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// | ||
// SQLite.swift | ||
// https://github.com/stephencelis/SQLite.swift | ||
// Copyright © 2014-2015 Stephen Celis. | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in | ||
// all copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
// THE SOFTWARE. | ||
// | ||
import Foundation | ||
|
||
extension QueryType { | ||
|
||
/// Sets a `WITH` clause on the query. | ||
/// | ||
/// let users = Table("users") | ||
/// let id = Expression<String>("email") | ||
/// let name = Expression<String?>("name") | ||
/// | ||
/// let userNames = Table("user_names") | ||
/// userCategories.with(userNames, as: users.select(name)) | ||
/// // WITH "user_names" as (SELECT "name" FROM "users") SELECT * FROM "user_names" | ||
/// | ||
/// - Parameters: | ||
/// | ||
/// - alias: A name to assign to the table expression. | ||
/// | ||
/// - recursive: Whether to evaluate the expression recursively. | ||
/// | ||
/// - hint: Provides a hint to the query planner for how the expression should be implemented. | ||
/// | ||
/// - subquery: A query that generates the rows for the table expression. | ||
/// | ||
/// - Returns: A query with the given `ORDER BY` clause applied. | ||
public func with(_ alias: Table, columns: [Expressible]? = nil, recursive: Bool = false, | ||
hint: MaterializationHint? = nil, as subquery: QueryType) -> Self { | ||
var query = self | ||
let clause = WithClauses.Clause(alias: alias, columns: columns, hint: hint, query: subquery) | ||
query.clauses.with.recursive = query.clauses.with.recursive || recursive | ||
query.clauses.with.clauses.append(clause) | ||
return query | ||
} | ||
|
||
/// self.clauses.with transformed to an Expressible | ||
var withClause: Expressible? { | ||
guard !clauses.with.clauses.isEmpty else { | ||
return nil | ||
} | ||
|
||
let innerClauses = ", ".join(clauses.with.clauses.map { (clause) in | ||
let hintExpr: Expression<Void>? | ||
if let hint = clause.hint { | ||
hintExpr = Expression<Void>(literal: hint.rawValue) | ||
} else { | ||
hintExpr = nil | ||
} | ||
|
||
let columnExpr: Expression<Void>? | ||
if let columns = clause.columns { | ||
columnExpr = "".wrap(", ".join(columns)) | ||
} else { | ||
columnExpr = nil | ||
} | ||
|
||
let expressions: [Expressible?] = [ | ||
clause.alias.tableName(), | ||
columnExpr, | ||
Expression<Void>(literal: "AS"), | ||
hintExpr, | ||
"".wrap(clause.query) as Expression<Void> | ||
] | ||
|
||
return " ".join(expressions.compactMap { $0 }) | ||
}) | ||
|
||
return " ".join([ | ||
Expression<Void>(literal: clauses.with.recursive ? "WITH RECURSIVE" : "WITH"), | ||
innerClauses | ||
]) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -193,12 +193,14 @@ extension QueryType { | |
/// | ||
/// - Parameters: | ||
/// | ||
/// - all: If false, duplicate rows are removed from the result. | ||
/// | ||
/// - table: A query representing the other table. | ||
/// | ||
/// - Returns: A query with the given `UNION` clause applied. | ||
public func union(_ table: QueryType) -> Self { | ||
public func union(all: Bool = false, _ table: QueryType) -> Self { | ||
var query = self | ||
query.clauses.union.append(table) | ||
query.clauses.union.append((all, table)) | ||
return query | ||
} | ||
|
||
|
@@ -596,9 +598,9 @@ extension QueryType { | |
return nil | ||
} | ||
|
||
return " ".join(clauses.union.map { query in | ||
return " ".join(clauses.union.map { (all, query) in | ||
" ".join([ | ||
Expression<Void>(literal: "UNION"), | ||
Expression<Void>(literal: all ? "UNION ALL" : "UNION"), | ||
query | ||
]) | ||
}) | ||
|
@@ -856,6 +858,7 @@ extension QueryType { | |
|
||
public var expression: Expression<Void> { | ||
let clauses: [Expressible?] = [ | ||
withClause, | ||
selectClause, | ||
joinClause, | ||
whereClause, | ||
|
@@ -1233,8 +1236,30 @@ public enum OnConflict: String { | |
|
||
} | ||
|
||
/// Materialization hints for `WITH` clause | ||
public enum MaterializationHint: String { | ||
|
||
case materialized = "MATERIALIZED" | ||
|
||
case notMaterialized = "NOT MATERIALIZED" | ||
} | ||
|
||
// MARK: - Private | ||
|
||
struct WithClauses { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these could be moved to the new file as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, will move. Wasn't sure whether to move them since they kind of feel like part of QueryClauses. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤦 of course, my bad! |
||
struct Clause { | ||
var alias: Table | ||
var columns: [Expressible]? | ||
var hint: MaterializationHint? | ||
var query: QueryType | ||
} | ||
/// The `RECURSIVE` flag is applied to the entire `WITH` clause | ||
var recursive: Bool = false | ||
|
||
/// Each `WITH` clause may have multiple subclauses | ||
var clauses: [Clause] = [] | ||
} | ||
|
||
public struct QueryClauses { | ||
|
||
var select = (distinct: false, columns: [Expression<Void>(literal: "*") as Expressible]) | ||
|
@@ -1251,7 +1276,9 @@ public struct QueryClauses { | |
|
||
var limit: (length: Int, offset: Int?)? | ||
|
||
var union = [QueryType]() | ||
var union = [(all: Bool, table: QueryType)]() | ||
|
||
var with = WithClauses() | ||
|
||
fileprivate init(_ name: String, alias: String?, database: String?) { | ||
from = (name, alias, database) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that putting
all
in front of the parameter list is clearer here (and mimics the SQL), but as you say this will break API compatibility, so we'll have to release it as0.14
.