Add CI/CD configuration and API documentation
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user