GRDB.swift/GRDB/QueryInterface/SQLSelectQueryGenerator.swift
2019-03-15 08:41:57 +01:00

450 lines
17 KiB
Swift

/// SQLSelectQueryGenerator is able to generate an SQL SELECT query.
struct SQLSelectQueryGenerator {
fileprivate let relation: SQLQualifiedRelation
private let isDistinct: Bool
private let groupPromise: DatabasePromise<[SQLExpression]>?
private let havingExpression: SQLExpression?
private let limit: SQLLimit?
init(_ query: SQLSelectQuery) {
// To generate SQL, we need a "qualified" relation, where all tables,
// expressions, etc, are identified with table aliases.
//
// All those aliases let us disambiguate tables at the SQL level, and
// prefix columns names. For example, the following request...
//
// Book.filter(Column("kind") == Book.Kind.novel)
// .including(optional: Book.author)
// .including(optional: Book.translator)
// .annotated(with: Book.awards.count)
//
// ... generates the following SQL, where all identifiers are correctly
// disambiguated and qualified:
//
// SELECT book.*, person1.*, person2.*, COUNT(DISTINCT award.id)
// FROM book
// LEFT JOIN person person1 ON person1.id = book.authorId
// LEFT JOIN person person2 ON person2.id = book.translatorId
// LEFT JOIN award ON award.bookId = book.id
// GROUP BY book.id
// WHERE book.kind = 'novel'
relation = SQLQualifiedRelation(query.relation)
// Qualify group expressions and having clause with the relation alias.
//
// This turns `GROUP BY id` INTO `GROUP BY book.id`, and
// `HAVING MAX(year) < 2000` INTO `HAVING MAX(book.year) < 2000`.
let alias = relation.alias
groupPromise = query.groupPromise?.map { [alias] in $0.map { $0.qualifiedExpression(with: alias) } }
havingExpression = query.havingExpression?.qualifiedExpression(with: alias)
// Preserve other flags
isDistinct = query.isDistinct
limit = query.limit
}
func sql(_ db: Database, _ context: inout SQLGenerationContext) throws -> String {
var sql = "SELECT"
if isDistinct {
sql += " DISTINCT"
}
let selection = relation.selection
GRDBPrecondition(!selection.isEmpty, "Can't generate SQL with empty selection")
sql += " " + selection.map { $0.resultColumnSQL(&context) }.joined(separator: ", ")
sql += try " FROM " + relation.source.sql(db, &context)
for (_, join) in relation.joins {
sql += try " " + join.sql(db, &context, leftAlias: relation.alias, isRequiredAllowed: true)
}
if let filter = try relation.filterPromise.resolve(db) {
sql += " WHERE " + filter.expressionSQL(&context)
}
if let groupExpressions = try groupPromise?.resolve(db), !groupExpressions.isEmpty {
sql += " GROUP BY " + groupExpressions.map { $0.expressionSQL(&context) }.joined(separator: ", ")
}
if let havingExpression = havingExpression {
sql += " HAVING " + havingExpression.expressionSQL(&context)
}
let orderings = try relation.ordering.resolve(db)
if !orderings.isEmpty {
sql += " ORDER BY " + orderings.map { $0.orderingTermSQL(&context) }.joined(separator: ", ")
}
if let limit = limit {
sql += " LIMIT " + limit.sql
}
return sql
}
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
return try (makeSelectStatement(db), rowAdapter(db))
}
func databaseRegion(_ db: Database) throws -> DatabaseRegion {
let statement = try makeSelectStatement(db)
let databaseRegion = statement.databaseRegion
// Can we intersect the region with rowIds?
//
// Give up unless request feeds from a single database table
guard case .table(tableName: let tableName, alias: _) = relation.source else {
// TODO: try harder
return databaseRegion
}
// Give up unless primary key is rowId
let primaryKeyInfo = try db.primaryKey(tableName)
guard primaryKeyInfo.isRowID else {
return databaseRegion
}
// Give up unless there is a where clause
guard let filter = try relation.filterPromise.resolve(db) else {
return databaseRegion
}
// The filter knows better
guard let rowIds = filter.matchedRowIds(rowIdName: primaryKeyInfo.rowIDColumn) else {
return databaseRegion
}
// Database regions are case-insensitive: use the canonical table name
let canonicalTableName = try db.canonicalTableName(tableName)
return databaseRegion.tableIntersection(canonicalTableName, rowIds: rowIds)
}
func makeDeleteStatement(_ db: Database) throws -> UpdateStatement {
if let groupExpressions = try groupPromise?.resolve(db), !groupExpressions.isEmpty {
// Programmer error
fatalError("Can't delete query with GROUP BY clause")
}
guard havingExpression == nil else {
// Programmer error
fatalError("Can't delete query with HAVING clause")
}
guard relation.joins.isEmpty else {
// Programmer error
fatalError("Can't delete query with JOIN clause")
}
guard case .table = relation.source else {
// Programmer error
fatalError("Can't delete without any database table")
}
var context = SQLGenerationContext.queryGenerationContext(aliases: relation.allAliases)
var sql = try "DELETE FROM " + relation.source.sql(db, &context)
if let filter = try relation.filterPromise.resolve(db) {
sql += " WHERE " + filter.expressionSQL(&context)
}
if let limit = limit {
let orderings = try relation.ordering.resolve(db)
if !orderings.isEmpty {
sql += " ORDER BY " + orderings.map { $0.orderingTermSQL(&context) }.joined(separator: ", ")
}
if Database.sqliteCompileOptions.contains("ENABLE_UPDATE_DELETE_LIMIT") {
sql += " LIMIT " + limit.sql
} else {
fatalError("Can't delete query with limit")
}
}
let statement = try db.makeUpdateStatement(sql: sql)
statement.arguments = context.arguments!
return statement
}
/// Returns a select statement
private func makeSelectStatement(_ db: Database) throws -> SelectStatement {
// Build an SQK generation context with all aliases found in the query,
// so that we can disambiguate tables that are used several times with
// SQL aliases.
var context = SQLGenerationContext.queryGenerationContext(aliases: relation.allAliases)
// Generate SQL
let sql = try self.sql(db, &context)
// Compile & set arguments
let statement = try db.makeSelectStatement(sql: sql)
statement.arguments = context.arguments! // not nil for this kind of context
return statement
}
/// Returns the row adapter which presents the fetched rows according to the
/// tree of joined relations.
///
/// The adapter is nil for queries without any included relation,
/// because the fetched rows don't need any processing:
///
/// // SELECT * FROM book
/// let request = Book.all()
/// for row in try Row.fetchAll(db, request) {
/// row // [id:1, title:"Moby-Dick"]
/// let book = Book(row: row)
/// }
///
/// But as soon as the selection includes columns of a included relation,
/// we need an adapter:
///
/// // SELECT book.*, author.* FROM book JOIN author ON author.id = book.authorId
/// let request = Book.including(required: Book.author)
/// for row in try Row.fetchAll(db, request) {
/// row // [id:1, title:"Moby-Dick"]
/// let book = Book(row: row)
///
/// row.scopes["author"] // [id:12, name:"Herman Melville"]
/// let author: Author = row["author"]
/// }
private func rowAdapter(_ db: Database) throws -> RowAdapter? {
return try relation.rowAdapter(db, fromIndex: 0)?.adapter
}
}
/// A "qualified" relation, where all tables are identified with a table alias.
private struct SQLQualifiedRelation {
/// The alias for the relation
///
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
/// ^ alias
let alias: TableAlias
/// All aliases, including aliases of joined relations
var allAliases: [TableAlias] {
var aliases = [alias]
for join in joins.values {
aliases.append(contentsOf: join.relation.allAliases)
}
aliases.append(contentsOf: source.allAliases)
return aliases
}
/// The source
///
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
/// ^ source
let source: SQLQualifiedSource
/// The selection, not including selection of joined relations
private let ownSelection: [SQLSelectable]
/// The full selection, including selection of joined relations
///
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
/// ^ fullSelection
var selection: [SQLSelectable] {
return joins.reduce(into: ownSelection) {
$0.append(contentsOf: $1.value.relation.selection)
}
}
/// The filtering clause
///
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
/// ^ filterPromise
let filterPromise: DatabasePromise<SQLExpression?>
/// The ordering, not including ordering of joined relations
private let ownOrdering: SQLRelation.Ordering
/// The full ordering, including orderings of joined relations
///
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
/// ^ ordering
var ordering: SQLRelation.Ordering {
return joins.reduce(ownOrdering) {
$0.appending($1.value.relation.ordering)
}
}
/// The joins
///
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
/// ^ joins
let joins: OrderedDictionary<String, SQLQualifiedJoin>
init(_ relation: SQLRelation) {
// Qualify the source, so that it be disambiguated with an SQL alias
// if needed (when a select query uses the same table several times).
// This disambiguation job will be actually performed by
// SQLGenerationContext, when the SQLSelectQueryGenerator which owns
// this SQLQualifiedRelation generates SQL.
source = SQLQualifiedSource(relation.source)
let alias = source.alias
self.alias = alias
// Qualify all joins, selection, filter, and ordering, so that all
// identifiers can be correctly disambiguated and qualified.
joins = relation.joins.mapValues(SQLQualifiedJoin.init)
ownSelection = relation.selection.map { $0.qualifiedSelectable(with: alias) }
filterPromise = relation.filterPromise.map { [alias] in $0?.qualifiedExpression(with: alias) }
ownOrdering = relation.ordering.qualified(with: alias)
}
/// See SQLSelectQueryGenerator.rowAdapter(_:)
///
/// - parameter db: A database connection.
/// - parameter startIndex: The index of the leftmost selected column of
/// this relation in a full SQL query. `startIndex` is 0 for the relation
/// at the root of a SQLSelectQueryGenerator (as opposed to the
/// joined relations).
/// - returns: An optional tuple made of a RowAdapter and the index past the
/// rightmost selected column of this relation. Nil is returned if this
/// relations does not need any row adapter.
func rowAdapter(_ db: Database, fromIndex startIndex: Int) throws -> (adapter: RowAdapter, endIndex: Int)? {
// Root relation && no join => no need for any adapter
if startIndex == 0 && joins.isEmpty {
return nil
}
// The number of columns in own selection. Columns selected by joined
// relations are appended after.
let selectionWidth = try ownSelection
.map { try $0.columnCount(db) }
.reduce(0, +)
// Recursively build adapters for each joined relation with a selection.
// Name them according to the join keys.
var endIndex = startIndex + selectionWidth
var scopes: [String: RowAdapter] = [:]
for (key, join) in joins {
if let (joinAdapter, joinEndIndex) = try join.relation.rowAdapter(db, fromIndex: endIndex) {
scopes[key] = joinAdapter
endIndex = joinEndIndex
}
}
// (Root relation || empty selection) && no included relation => no need for any adapter
if (startIndex == 0 || selectionWidth == 0) && scopes.isEmpty {
return nil
}
// Build a RangeRowAdapter extended with the adapters of joined relations.
//
// // SELECT book.*, author.* FROM book JOIN author ON author.id = book.authorId
// let request = Book.including(required: Book.author)
// for row in try Row.fetchAll(db, request) {
//
// The RangeRowAdapter hides the columns appended by joined relations:
//
// row // [id:1, title:"Moby-Dick"]
// let book = Book(row: row)
//
// Scopes give access to those joined relations:
//
// row.scopes["author"] // [id:12, name:"Herman Melville"]
// let author: Author = row["author"]
// }
let rangeAdapter = RangeRowAdapter(startIndex ..< (startIndex + selectionWidth))
let adapter = rangeAdapter.addingScopes(scopes)
return (adapter: adapter, endIndex: endIndex)
}
}
/// A "qualified" source, where all tables are identified with a table alias.
private enum SQLQualifiedSource {
case table(tableName: String, alias: TableAlias)
indirect case query(SQLSelectQueryGenerator)
var alias: TableAlias {
switch self {
case .table(_, let alias):
return alias
case .query(let query):
return query.relation.alias
}
}
var allAliases: [TableAlias] {
switch self {
case .table(_, let alias):
return [alias]
case .query(let query):
return query.relation.allAliases
}
}
init(_ source: SQLSource) {
switch source {
case .table(let tableName, let alias):
let alias = alias ?? TableAlias(tableName: tableName)
self = .table(tableName: tableName, alias: alias)
case .query(let query):
self = .query(SQLSelectQueryGenerator(query))
}
}
func sql(_ db: Database, _ context: inout SQLGenerationContext) throws -> String {
switch self {
case .table(let tableName, let alias):
if let aliasName = context.aliasName(for: alias) {
return "\(tableName.quotedDatabaseIdentifier) \(aliasName.quotedDatabaseIdentifier)"
} else {
return "\(tableName.quotedDatabaseIdentifier)"
}
case .query(let query):
return try "(\(query.sql(db, &context)))"
}
}
}
/// A "qualified" join, where all tables are identified with a table alias.
private struct SQLQualifiedJoin {
private let kind: SQLJoin.Kind
private let condition: SQLJoin.Condition
let relation: SQLQualifiedRelation
init(_ join: SQLJoin) {
self.kind = join.kind
self.condition = join.condition
self.relation = SQLQualifiedRelation(join.relation)
}
func sql(_ db: Database,_ context: inout SQLGenerationContext, leftAlias: TableAlias, isRequiredAllowed: Bool) throws -> String {
var isRequiredAllowed = isRequiredAllowed
var sql = ""
switch kind {
case .optional:
isRequiredAllowed = false
sql += "LEFT JOIN"
case .required:
guard isRequiredAllowed else {
// TODO: chainOptionalRequired
fatalError("Not implemented: chaining a required association behind an optional association")
}
sql += "JOIN"
}
sql += try " " + relation.source.sql(db, &context)
let rightAlias = relation.alias
let filters = try [
condition.sqlExpression(db, leftAlias: leftAlias, rightAlias: rightAlias),
relation.filterPromise.resolve(db)
].compactMap { $0 }
if !filters.isEmpty {
sql += " ON " + filters.joined(operator: .and).expressionSQL(&context)
}
for (_, join) in relation.joins {
sql += try " " + join.sql(db, &context, leftAlias: rightAlias, isRequiredAllowed: isRequiredAllowed)
}
return sql
}
}