388 lines
12 KiB
Swift
388 lines
12 KiB
Swift
/// An SQL literal.
|
|
///
|
|
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
|
/// without any risk of syntax errors or SQL injection. For example:
|
|
///
|
|
/// ```swift
|
|
/// try dbQueue.write { db in
|
|
/// let name: String = "O'Brien"
|
|
/// let id: Int64 = 42
|
|
/// let query: SQL = "UPDATE player SET name = \(name) WHERE id = \(id)"
|
|
///
|
|
/// // UPDATE player SET name = 'O''Brien' WHERE id = 42
|
|
/// try db.execute(literal: query)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ## Topics
|
|
///
|
|
/// ### Creating an SQL Literal from a Literal Value
|
|
///
|
|
/// - ``init(stringLiteral:)``
|
|
/// - ``init(unicodeScalarLiteral:)-7p0al``
|
|
/// - ``init(extendedGraphemeClusterLiteral:)-1oham``
|
|
///
|
|
/// ### Creating an SQL Literal from an Interpolation
|
|
///
|
|
/// - ``init(stringInterpolation:)``
|
|
/// - ``SQLInterpolation``
|
|
///
|
|
/// ### Creating an SQL Literal from an SQL String
|
|
///
|
|
/// - ``init(sql:arguments:)``
|
|
///
|
|
/// ### Creating an SQL Literal from an SQL Expression
|
|
///
|
|
/// - ``init(_:)``
|
|
///
|
|
/// ### Concatenating SQL Literals
|
|
///
|
|
/// - ``append(literal:)``
|
|
/// - ``append(sql:arguments:)``
|
|
public struct SQL {
|
|
/// `SQL.Element` is a component of an `SQL` literal.
|
|
///
|
|
/// Elements can be qualified with table aliases, and this is how `SQL`
|
|
/// blends well in the query interface. See below how the `createdAt` column
|
|
/// is qualified with the `player` table in the generated SQL, in order to
|
|
/// avoid any conflict with the `team.createdAt` column:
|
|
///
|
|
/// func date(_ value: SQLSpecificExpressible) -> SQLExpression {
|
|
/// // An SQL literal made of three elements:
|
|
/// // - "DATE(" raw sql string
|
|
/// // - expression
|
|
/// // - ")" raw sql string
|
|
/// SQL("DATE(\(value))").sqlExpression
|
|
/// }
|
|
///
|
|
/// // SELECT player.*, team.*
|
|
/// // FROM player
|
|
/// // JOIN team ON team.id = player.teamId
|
|
/// // WHERE DATE(player.createdAt) = '2022-08-17'
|
|
/// let request = Player
|
|
/// .filter(date(Column("createdAt")) == "2022-08-17")
|
|
/// .including(required: Player.team)
|
|
enum Element {
|
|
/// A raw SQL literal with eventual arguments.
|
|
case sql(String, StatementArguments = StatementArguments())
|
|
|
|
/// A subquery.
|
|
case subquery(SQLSubquery)
|
|
|
|
/// An expression.
|
|
case expression(SQLExpression)
|
|
|
|
/// A selection.
|
|
case selection(SQLSelection)
|
|
|
|
/// An ordering.
|
|
case ordering(SQLOrdering)
|
|
|
|
var isEmpty: Bool {
|
|
switch self {
|
|
case let .sql(sql, _):
|
|
return sql.isEmpty
|
|
default:
|
|
// Subqueries, expressions, selections and orderings are
|
|
// assumed to be non-empty.
|
|
//
|
|
// Nothing prevents the user from creating an ill-formed empty
|
|
// expression, but we don't care about such misuse:
|
|
//
|
|
// // An ill-formed empty expression
|
|
// let expression = SQL("").sqlExpression
|
|
//
|
|
// let sql: SQL = "\(expression)"
|
|
// sql.isEmpty // false, deal with it ¯\_(ツ)_/¯
|
|
return false
|
|
}
|
|
}
|
|
|
|
fileprivate func sql(_ context: SQLGenerationContext) throws -> String {
|
|
switch self {
|
|
case let .sql(sql, arguments):
|
|
if context.append(arguments: arguments) == false {
|
|
// We don't know how to look for `?` in sql and
|
|
// replace them with literals.
|
|
fatalError("Not implemented: turning an SQL parameter into an SQL literal value")
|
|
}
|
|
return sql
|
|
case let .subquery(subquery):
|
|
return try subquery.sql(context)
|
|
case let .expression(expression):
|
|
return try expression.sql(context)
|
|
case let .selection(selection):
|
|
return try selection.sql(context)
|
|
case let .ordering(ordering):
|
|
return try ordering.sql(context)
|
|
}
|
|
}
|
|
|
|
fileprivate func qualified(with alias: TableAlias) -> Element {
|
|
switch self {
|
|
case .sql:
|
|
// A raw SQL string can't be qualified with a table alias,
|
|
// because we can't parse it.
|
|
return self
|
|
case .subquery:
|
|
// Subqueries don't need table alias
|
|
return self
|
|
case let .expression(expression):
|
|
return .expression(expression.qualified(with: alias))
|
|
case let .selection(selection):
|
|
return .selection(selection.qualified(with: alias))
|
|
case let .ordering(ordering):
|
|
return .ordering(ordering.qualified(with: alias))
|
|
}
|
|
}
|
|
}
|
|
|
|
private(set) var elements: [Element]
|
|
|
|
init(elements: [Element]) {
|
|
self.elements = elements
|
|
}
|
|
|
|
/// Creates an `SQL` literal from a plain SQL string, and
|
|
/// eventual arguments.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let query = SQL(
|
|
/// sql: "UPDATE player SET name = ? WHERE id = ?",
|
|
/// arguments: [name, id])
|
|
/// ```
|
|
public init(sql: String, arguments: StatementArguments = StatementArguments()) {
|
|
self.init(elements: [.sql(sql, arguments)])
|
|
}
|
|
|
|
/// Creates an `SQL` literal from an SQL expression.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let columnLiteral = SQL(Column("username"))
|
|
/// let suffixLiteral = SQL("@example.com".databaseValue)
|
|
/// let emailLiteral = [columnLiteral, suffixLiteral].joined(separator: " || ")
|
|
/// let request = User.select(emailLiteral.sqlExpression)
|
|
/// let emails = try String.fetchAll(db, request)
|
|
/// ```
|
|
public init(_ expression: some SQLSpecificExpressible) {
|
|
self.init(elements: [.expression(expression.sqlExpression)])
|
|
}
|
|
|
|
/// Returns true if this literal generates an empty SQL string
|
|
public var isEmpty: Bool {
|
|
elements.allSatisfy(\.isEmpty)
|
|
}
|
|
|
|
/// Turn a `SQL` literal into raw SQL and arguments.
|
|
///
|
|
/// - parameter db: A database connection.
|
|
/// - returns: A tuple made of a raw SQL string, and statement arguments.
|
|
public func build(_ db: Database) throws -> (sql: String, arguments: StatementArguments) {
|
|
let context = SQLGenerationContext(db)
|
|
let sql = try self.sql(context)
|
|
return (sql: sql, arguments: context.arguments)
|
|
}
|
|
|
|
/// Returns the literal SQL string given an SQL generation context.
|
|
func sql(_ context: SQLGenerationContext) throws -> String {
|
|
try elements.map { try $0.sql(context) }.joined()
|
|
}
|
|
|
|
func qualified(with alias: TableAlias) -> SQL {
|
|
SQL(elements: elements.map { $0.qualified(with: alias) })
|
|
}
|
|
}
|
|
|
|
extension SQL {
|
|
/// Returns the `SQL` literal produced by the concatenation of two literals.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let name = "O'Brien"
|
|
/// let selection: SQL = "SELECT * FROM player "
|
|
/// let condition: SQL = "WHERE name = \(name)"
|
|
/// let query = selection + condition
|
|
/// ```
|
|
public static func + (lhs: SQL, rhs: SQL) -> SQL {
|
|
var result = lhs
|
|
result += rhs
|
|
return result
|
|
}
|
|
|
|
/// Appends an `SQL` literal to the receiver.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let name = "O'Brien"
|
|
/// var query: SQL = "SELECT * FROM player "
|
|
/// query += "WHERE name = \(name)"
|
|
/// ```
|
|
public static func += (lhs: inout SQL, rhs: SQL) {
|
|
lhs.elements += rhs.elements
|
|
}
|
|
|
|
/// Appends an `SQL` literal to the receiver.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let name = "O'Brien"
|
|
/// var query: SQL = "SELECT * FROM player "
|
|
/// query.append(literal: "WHERE name = \(name)")
|
|
/// ```
|
|
public mutating func append(literal sqlLiteral: SQL) {
|
|
self += sqlLiteral
|
|
}
|
|
|
|
/// Appends a plain SQL string to the receiver, and eventual arguments.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let name = "O'Brien"
|
|
/// var query: SQL = "SELECT * FROM player "
|
|
/// query.append(sql: "WHERE name = ?", arguments: [name])
|
|
/// ```
|
|
public mutating func append(sql: String, arguments: StatementArguments = StatementArguments()) {
|
|
self += SQL(sql: sql, arguments: arguments)
|
|
}
|
|
}
|
|
|
|
extension SQL: SQLSpecificExpressible {
|
|
/// Creates a literal SQL expression.
|
|
///
|
|
/// Use this property when you need an explicit `SQLExpression`.
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// func date(_ value: some SQLExpressible) -> SQLExpression {
|
|
/// SQL("DATE(\(value))").sqlExpression
|
|
/// }
|
|
///
|
|
/// // SELECT * FROM "player" WHERE DATE("createdAt") = '2020-01-23'
|
|
/// let createdAt = Column("createdAt")
|
|
/// let request = Player.filter(date(createdAt) == "2020-01-23")
|
|
/// ```
|
|
public var sqlExpression: SQLExpression {
|
|
.literal(self)
|
|
}
|
|
}
|
|
|
|
extension SQL: SQLSelectable {
|
|
/// Creates a literal SQL result column.
|
|
///
|
|
/// Use this property when you need an explicit `SQLSelection`. For example:
|
|
///
|
|
/// ```swift
|
|
/// // SELECT firstName AS givenName, lastName AS familyName FROM player
|
|
/// let selection = SQL("firstName AS givenName, lastName AS familyName").sqlSelection
|
|
/// let request = Player.select(selection)
|
|
/// ```
|
|
public var sqlSelection: SQLSelection {
|
|
.literal(self)
|
|
}
|
|
}
|
|
|
|
extension SQL: SQLOrderingTerm {
|
|
/// Creates a literal SQL ordering term.
|
|
///
|
|
/// Use this property when you need an explicit `SQLOrdering`. For example:
|
|
///
|
|
/// ```swift
|
|
/// // SELECT * FROM player ORDER BY name DESC
|
|
/// let ordering = SQL("name DESC").sqlOrdering
|
|
/// let request = Player.order(ordering)
|
|
/// ```
|
|
///
|
|
/// An ordering term is only valid if it represent a single ordering terms.
|
|
///
|
|
/// ```swift
|
|
/// // Valid
|
|
/// SQL("score DESC").sqlOrdering
|
|
/// SQL("name").sqlOrdering
|
|
///
|
|
/// // Invalid
|
|
/// SQL("score DESC, name").sqlOrdering
|
|
/// ```
|
|
public var sqlOrdering: SQLOrdering {
|
|
.literal(self)
|
|
}
|
|
}
|
|
|
|
extension Sequence where Element == SQL {
|
|
/// Returns the concatenated `SQL` literal of this sequence of literals,
|
|
/// inserting the given raw SQL separator between each element.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```
|
|
/// let components: [SQL] = [
|
|
/// "UPDATE player",
|
|
/// "SET name = \(name)",
|
|
/// "WHERE id = \(id)"
|
|
/// ]
|
|
/// let query = components.joined(separator: " ")
|
|
/// ```
|
|
///
|
|
/// - Note: The separator is a raw SQL string, not an ``SQL`` literal.
|
|
public func joined(separator: String = "") -> SQL {
|
|
if separator.isEmpty {
|
|
return SQL(elements: flatMap(\.elements))
|
|
} else {
|
|
return SQL(elements: Array(map(\.elements).joined(separator: CollectionOfOne(.sql(separator)))))
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Collection where Element == SQL {
|
|
/// Returns the concatenated `SQL` literal of this collection of literals,
|
|
/// inserting the given raw SQL separator between each element.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let components: [SQL] = [
|
|
/// "UPDATE player",
|
|
/// "SET name = \(name)",
|
|
/// "WHERE id = \(id)"
|
|
/// ]
|
|
/// let query = components.joined(separator: " ")
|
|
/// ```
|
|
///
|
|
/// - Note: The separator is a raw SQL string, not an ``SQL`` literal.
|
|
public func joined(separator: String = "") -> SQL {
|
|
if separator.isEmpty {
|
|
return SQL(elements: flatMap(\.elements))
|
|
} else {
|
|
return SQL(elements: Array(map(\.elements).joined(separator: CollectionOfOne(.sql(separator)))))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ExpressibleByStringInterpolation
|
|
|
|
extension SQL: ExpressibleByStringInterpolation {
|
|
public init(unicodeScalarLiteral: String) {
|
|
self.init(sql: unicodeScalarLiteral, arguments: [])
|
|
}
|
|
|
|
public init(extendedGraphemeClusterLiteral: String) {
|
|
self.init(sql: extendedGraphemeClusterLiteral, arguments: [])
|
|
}
|
|
|
|
/// Creates an `SQL` literal from the given literal SQL string.
|
|
public init(stringLiteral: String) {
|
|
self.init(sql: stringLiteral, arguments: [])
|
|
}
|
|
|
|
public init(stringInterpolation sqlInterpolation: SQLInterpolation) {
|
|
self.init(elements: sqlInterpolation.elements)
|
|
}
|
|
}
|