summaryrefslogtreecommitdiffstats
path: root/SQLiteStudio3/coreSQLiteStudio/selectresolver.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'SQLiteStudio3/coreSQLiteStudio/selectresolver.cpp')
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/selectresolver.cpp673
1 files changed, 673 insertions, 0 deletions
diff --git a/SQLiteStudio3/coreSQLiteStudio/selectresolver.cpp b/SQLiteStudio3/coreSQLiteStudio/selectresolver.cpp
new file mode 100644
index 0000000..9e425a2
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/selectresolver.cpp
@@ -0,0 +1,673 @@
+#include "selectresolver.h"
+#include "parser/token.h"
+#include "parser/lexer.h"
+#include "parser/keywords.h"
+#include "schemaresolver.h"
+#include "parser/ast/sqlitecreateview.h"
+#include "common/global.h"
+#include <QDebug>
+#include <QHash>
+#include <QHashIterator>
+#include <QString>
+
+SelectResolver::SelectResolver(Db *db, const QString& originalQuery)
+{
+ this->db = db;
+ this->query = originalQuery;
+ schemaResolver = new SchemaResolver(db);
+}
+
+SelectResolver::SelectResolver(Db* db, const QString& originalQuery, const BiStrHash& dbNameToAttach) :
+ SelectResolver(db, originalQuery)
+{
+ this->dbNameToAttach = dbNameToAttach;
+}
+
+SelectResolver::~SelectResolver()
+{
+ safe_delete(schemaResolver);
+}
+
+QList<SelectResolver::Column> SelectResolver::resolve(SqliteSelect::Core *selectCore)
+{
+ errors.clear();
+ return resolveCore(selectCore);
+}
+
+QList<QList<SelectResolver::Column> > SelectResolver::resolve(SqliteSelect *select)
+{
+ errors.clear();
+ QList<QList<SelectResolver::Column> > results;
+ foreach (SqliteSelect::Core* core, select->coreSelects)
+ {
+ results << resolveCore(core);
+ currentCoreResults.clear();
+ }
+
+ return results;
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveAvailableColumns(SqliteSelect::Core *selectCore)
+{
+ errors.clear();
+ return resolveAvailableCoreColumns(selectCore);
+}
+
+QList<QList<SelectResolver::Column> > SelectResolver::resolveAvailableColumns(SqliteSelect *select)
+{
+ errors.clear();
+ QList<QList<SelectResolver::Column> > results;
+ foreach (SqliteSelect::Core* core, select->coreSelects)
+ results << resolveAvailableCoreColumns(core);
+
+ return results;
+}
+
+QSet<SelectResolver::Table> SelectResolver::resolveTables(SqliteSelect::Core *selectCore)
+{
+ QSet<Table> tables;
+ QList<Column> columns = resolveAvailableColumns(selectCore);
+ foreach (Column col, columns)
+ tables << col.getTable();
+
+ return tables;
+}
+
+QList<QSet<SelectResolver::Table> > SelectResolver::resolveTables(SqliteSelect *select)
+{
+ QList<QSet<Table> > results;
+ QList<QList<Column> > columnLists = resolveAvailableColumns(select);
+ foreach (QList<Column> columns, columnLists)
+ {
+ QSet<Table> tables;
+ foreach (Column col, columns)
+ tables << col.getTable();
+
+ results << tables;
+ }
+
+ return results;
+}
+
+QList<SelectResolver::Column> SelectResolver::translateToColumns(SqliteSelect* select, const TokenList& columnTokens)
+{
+ errors.clear();
+ QList<SelectResolver::Column> results;
+ foreach (TokenPtr token, columnTokens)
+ results << translateTokenToColumn(select, token);
+
+ return results;
+}
+
+SelectResolver::Column SelectResolver::translateToColumns(SqliteSelect* select, TokenPtr token)
+{
+ errors.clear();
+ return translateTokenToColumn(select, token);
+}
+
+bool SelectResolver::hasErrors() const
+{
+ return !errors.isEmpty();
+}
+
+const QStringList& SelectResolver::getErrors() const
+{
+ return errors;
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveCore(SqliteSelect::Core* selectCore)
+{
+ if (selectCore->from)
+ currentCoreSourceColumns = resolveJoinSource(selectCore->from);
+
+ foreach (SqliteSelect::Core::ResultColumn* resCol, selectCore->resultColumns)
+ resolve(resCol);
+
+ if (selectCore->distinctKw)
+ markDistinctColumns();
+
+ if (selectCore->groupBy.size() > 0)
+ markGroupedColumns();
+
+ fixColumnNames();
+
+ SqliteSelect* select = dynamic_cast<SqliteSelect*>(selectCore->parentStatement());
+ if (select && select->coreSelects.size() > 1)
+ markCompoundColumns();
+
+ if (select && select->with)
+ markCteColumns();
+
+ return currentCoreResults;
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveAvailableCoreColumns(SqliteSelect::Core* selectCore)
+{
+ QList<Column> columns;
+ if (selectCore->from)
+ columns = resolveJoinSource(selectCore->from);
+
+ SqliteSelect* select = dynamic_cast<SqliteSelect*>(selectCore->parentStatement());
+ if (select && select->with)
+ markCteColumns();
+
+ return columns;
+}
+
+SelectResolver::Column SelectResolver::translateTokenToColumn(SqliteSelect* select, TokenPtr token)
+{
+ // Default result
+ Column notTranslatedColumn;
+ notTranslatedColumn.type = Column::OTHER;
+ notTranslatedColumn.column = token->value;
+
+ // Find containing statement
+ SqliteStatement* parentStmt = select->findStatementWithToken(token);
+ if (!parentStmt)
+ {
+ qDebug() << "Could not find containing statement for given token while translating column token:" << token->toString()
+ << "Select tokens:" << select->tokens.toString();
+
+ return notTranslatedColumn;
+ }
+
+ // Go through all select cores, from the most deep, to the most shallow
+ SqliteSelect::Core* core = nullptr;
+ while (parentStmt)
+ {
+ // Find nearest SELECT core.
+ while (parentStmt && !(core = dynamic_cast<SqliteSelect::Core*>(parentStmt)))
+ parentStmt = parentStmt->parentStatement();
+
+ if (!core)
+ {
+ qDebug() << "Could not find SqliteSelect::Core object for given token while translating column token:" << token->toString()
+ << "Select:" << select->detokenize();
+
+ return notTranslatedColumn;
+ }
+
+ // Search through available columns
+ foreach (const Column& availableColumn, resolveAvailableColumns(core))
+ {
+ if (availableColumn.type == Column::COLUMN && availableColumn.column.compare(token->value, Qt::CaseInsensitive) == 0)
+ return availableColumn;
+ }
+
+ // Not in this core. See if there is any core upper (if this was a subselect).
+ parentStmt = parentStmt->parentStatement();
+ }
+
+ return notTranslatedColumn;
+}
+
+void SelectResolver::markDistinctColumns()
+{
+ markCurrentColumnsWithFlag(FROM_DISTINCT_SELECT);
+}
+
+void SelectResolver::markCompoundColumns()
+{
+ markCurrentColumnsWithFlag(FROM_COMPOUND_SELECT);
+}
+
+void SelectResolver::markCteColumns()
+{
+ markCurrentColumnsWithFlag(FROM_CTE_SELECT);
+}
+
+void SelectResolver::markGroupedColumns()
+{
+ markCurrentColumnsWithFlag(FROM_GROUPED_SELECT);
+}
+
+void SelectResolver::fixColumnNames()
+{
+ QSet<QString> existingDisplayNames;
+ QString originalName;
+ int i;
+
+ QMutableListIterator<Column> it(currentCoreResults);
+ while (it.hasNext())
+ {
+ originalName = it.next().displayName;
+ for (i = 1; existingDisplayNames.contains(it.value().displayName); i++)
+ it.value().displayName = originalName + ":" + QString::number(i);
+
+ existingDisplayNames << it.value().displayName;
+ }
+}
+
+void SelectResolver::markCurrentColumnsWithFlag(SelectResolver::Flag flag)
+{
+ QMutableListIterator<Column> it(currentCoreResults);
+ while (it.hasNext())
+ it.next().flags |= flag;
+}
+
+void SelectResolver::resolve(SqliteSelect::Core::ResultColumn *resCol)
+{
+ if (resCol->star)
+ resolveStar(resCol);
+ else
+ resolveExpr(resCol);
+}
+
+void SelectResolver::resolveStar(SqliteSelect::Core::ResultColumn *resCol)
+{
+ bool foundAtLeastOne = false;
+ foreach (SelectResolver::Column column, currentCoreSourceColumns)
+ {
+ if (!resCol->table.isNull())
+ {
+ /*
+ * Star was prefixed with table or table alias.
+ * The "FROM" clause allows to use alias name the same as
+ * some other table real name in the very same "FROM".
+ * Their columns concatenate, so here we allow any column that
+ * prefix matches either alias or table from data source list.
+ * For example it's correct to query:
+ * SELECT test.* FROM test, otherTable AS test;
+ * This case is simpler then in resolveDbAndTable(),
+ * because here's no database allowed.
+ *
+ * Also, if the table has an alias specified,
+ * then the alias has a precedence before table's name,
+ * therefore we match table name only if the table alias
+ * is null.
+ */
+ if (
+ (
+ !column.tableAlias.isNull() &&
+ resCol->table.compare(column.tableAlias, Qt::CaseInsensitive) != 0
+ ) ||
+ (
+ column.tableAlias.isNull() &&
+ resCol->table.compare(column.table, Qt::CaseInsensitive) != 0
+ )
+
+ )
+ {
+ continue;
+ }
+ }
+
+ // If source column name is aliased, use it
+ if (!column.alias.isNull())
+ column.displayName = column.alias;
+ else
+ column.displayName = column.column;
+
+ column.originalColumn = resCol;
+ currentCoreResults << column;
+ foundAtLeastOne = true;
+ }
+
+ if (!foundAtLeastOne)
+ errors << QObject::tr("Could not resolve data source for column: %1").arg(resCol->detokenize());
+}
+
+void SelectResolver::resolveExpr(SqliteSelect::Core::ResultColumn *resCol)
+{
+ SelectResolver::Column column;
+ column.alias = resCol->alias;
+ column.originalColumn = resCol;
+ column.column = getResColTokensWithoutAlias(resCol).detokenize().trimmed();
+ column.displayName = !resCol->alias.isNull() ? column.alias : column.column;
+
+ SqliteExpr* expr = resCol->expr;
+ if (expr->mode != SqliteExpr::Mode::ID)
+ {
+ // Not a simple column, but some expression
+ column.type = Column::OTHER;
+ currentCoreResults << column;
+
+ // In this case we end it here.
+ return;
+ }
+
+ // Now we know we're dealing with db.table.column (with db and table optional)
+ resolveDbAndTable(resCol);
+}
+
+void SelectResolver::resolveDbAndTable(SqliteSelect::Core::ResultColumn *resCol)
+{
+ SqliteExpr* expr = resCol->expr;
+
+ // Basic info
+ Column col;
+ col.alias = resCol->alias;
+ col.column = expr->column;
+ col.originalColumn = resCol;
+ col.type = Column::COLUMN;
+
+ // Display name
+ if (col.alias.isNull())
+ col.displayName = expr->column;
+ else
+ col.displayName = col.alias;
+
+ // Looking for table relation
+ Column matched;
+ if (isRowIdKeyword(expr->column))
+ matched = resolveRowIdColumn(expr);
+ else if (!expr->database.isNull())
+ matched = resolveExplicitColumn(expr->database, expr->table, expr->column);
+ else if (!expr->table.isNull())
+ matched = resolveExplicitColumn(expr->table, expr->column);
+ else
+ matched = resolveExplicitColumn(expr->column);
+
+
+ if (!matched.table.isNull())
+ {
+ col.database = matched.database;
+ col.originalDatabase = resolveDatabase(matched.database);
+ col.table = matched.table;
+ col.tableAlias = matched.tableAlias;
+ col.flags = matched.flags;
+ }
+ else if (!ignoreInvalidNames)
+ {
+ qDebug() << "Source table for column '" << expr->detokenize()
+ << "' not matched while resolving select: " << query;
+ }
+
+ currentCoreResults << col;
+}
+
+SelectResolver::Column SelectResolver::resolveRowIdColumn(SqliteExpr *expr)
+{
+ // Looking for first source that can provide ROWID.
+ foreach (Column column, currentCoreSourceColumns)
+ {
+ if (column.table.isNull())
+ continue; // ROWID cannot be related to source with no table
+
+ if (!expr->table.isNull() && matchTable(column, expr->table))
+ return column;
+ }
+ return Column();
+}
+
+SelectResolver::Column SelectResolver::resolveExplicitColumn(const QString &columnName)
+{
+ foreach (const Column& column, currentCoreSourceColumns)
+ {
+ if (columnName.compare(column.column, Qt::CaseInsensitive) != 0 && columnName.compare(column.alias, Qt::CaseInsensitive) != 0)
+ continue;
+
+ return column;
+ }
+ return Column();
+}
+
+SelectResolver::Column SelectResolver::resolveExplicitColumn(const QString &table, const QString &columnName)
+{
+ foreach (const Column& column, currentCoreSourceColumns)
+ {
+ if (columnName.compare(column.column, Qt::CaseInsensitive) != 0 && columnName.compare(column.alias, Qt::CaseInsensitive) != 0)
+ continue;
+
+ if (!matchTable(column, table))
+ continue;
+
+ return column;
+ }
+ return Column();
+}
+
+SelectResolver::Column SelectResolver::resolveExplicitColumn(const QString &database, const QString &table, const QString &columnName)
+{
+ foreach (const Column& column, currentCoreSourceColumns)
+ {
+ if (columnName.compare(column.column, Qt::CaseInsensitive) != 0 && columnName.compare(column.alias, Qt::CaseInsensitive) != 0)
+ continue;
+
+ if (!matchTable(column, table))
+ continue;
+
+ if (database.compare(column.database, Qt::CaseInsensitive) != 0)
+ continue;
+
+ return column;
+ }
+ return Column();
+}
+
+bool SelectResolver::matchTable(const SelectResolver::Column &sourceColumn, const QString &table)
+{
+ // First check by tableAlias if it's present
+ if (!sourceColumn.tableAlias.isNull())
+ return (sourceColumn.tableAlias.compare(table, Qt::CaseInsensitive) == 0);
+
+ return (sourceColumn.table.compare(table, Qt::CaseInsensitive) == 0);
+}
+
+TokenList SelectResolver::getResColTokensWithoutAlias(SqliteSelect::Core::ResultColumn *resCol)
+{
+ TokenList allTokens = resCol->tokens;
+ if (!resCol->alias.isNull())
+ {
+ int depth = 0;
+ int idx = -1;
+ int idxCandidate = -1;
+ for (const TokenPtr& token : allTokens)
+ {
+ idxCandidate++;
+ if (token->type == Token::PAR_LEFT)
+ {
+ depth++;
+ }
+ else if (token->type == Token::PAR_RIGHT)
+ {
+ depth--;
+ }
+ else if (token->type == Token::KEYWORD && token->value.compare("AS", Qt::CaseInsensitive) && depth <= 0)
+ {
+ idx = idxCandidate;
+ break;
+ }
+ }
+
+ if (idx > -1)
+ allTokens = allTokens.mid(0, idx - 1);
+ }
+
+ return allTokens;
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveJoinSource(SqliteSelect::Core::JoinSource *joinSrc)
+{
+ QList<SelectResolver::Column> columnSources;
+ columnSources += resolveSingleSource(joinSrc->singleSource);
+ foreach (SqliteSelect::Core::JoinSourceOther* otherSrc, joinSrc->otherSources)
+ columnSources += resolveOtherSource(otherSrc);
+
+ return columnSources;
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveSingleSource(SqliteSelect::Core::SingleSource *joinSrc)
+{
+ if (!joinSrc)
+ return QList<Column>();
+
+ if (joinSrc->select)
+ return resolveSingleSourceSubSelect(joinSrc);
+
+ if (joinSrc->joinSource)
+ return resolveJoinSource(joinSrc->joinSource);
+
+ if (isView(joinSrc->database, joinSrc->table))
+ return resolveView(joinSrc->database, joinSrc->table, joinSrc->alias);
+
+ QList<Column> columnSources;
+ QStringList columns = getTableColumns(joinSrc->database, joinSrc->table, joinSrc->alias);
+ Column column;
+ foreach (QString columnName, columns)
+ {
+ column.type = Column::COLUMN;
+ column.column = columnName;
+ column.table = joinSrc->table;;
+ column.database = joinSrc->database;
+ column.originalDatabase = resolveDatabase(joinSrc->database);
+ if (!joinSrc->alias.isNull())
+ column.tableAlias = joinSrc->alias;
+
+ columnSources << column;
+ }
+
+ return columnSources;
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveSingleSourceSubSelect(SqliteSelect::Core::SingleSource *joinSrc)
+{
+ QList<Column> columnSources = resolveSubSelect(joinSrc->select);
+ applySubSelectAlias(columnSources, joinSrc->alias);
+ return columnSources;
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveOtherSource(SqliteSelect::Core::JoinSourceOther *otherSrc)
+{
+ return resolveSingleSource(otherSrc->singleSource);
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveSubSelect(SqliteSelect *select)
+{
+ QList<Column> columnSources;
+ Q_ASSERT(select->coreSelects.size() > 0);
+
+ bool compound = (select->coreSelects.size() > 1);
+
+ if (compound && !resolveMultiCore)
+ return columnSources;
+
+ SelectResolver internalResolver(db, query);
+ columnSources += internalResolver.resolve(select->coreSelects[0]);
+
+ if (compound)
+ {
+ QMutableListIterator<Column> it(columnSources);
+ while (it.hasNext())
+ it.next().flags |= FROM_COMPOUND_SELECT;
+ }
+
+ return columnSources;
+}
+
+QList<SelectResolver::Column> SelectResolver::resolveView(const QString& database, const QString& name, const QString& alias)
+{
+ QList<Column> results;
+ SqliteQueryPtr query = schemaResolver->getParsedObject(database, name, SchemaResolver::VIEW);
+ if (!query)
+ {
+ qDebug() << "Could not get parsed CREATE VIEW in SelectResolver::resolveView().";
+ return results;
+ }
+
+ SqliteCreateViewPtr createView = query.dynamicCast<SqliteCreateView>();
+ if (!createView)
+ {
+ qDebug() << "Parsed object not a CREATE VIEW as expected, but instead it's:" << sqliteQueryTypeToString(query->queryType);
+ return results;
+ }
+
+ results = resolveSubSelect(createView->select);
+ applySubSelectAlias(results, (!alias.isNull() ? alias : name));
+
+ return results;
+}
+
+bool SelectResolver::isView(const QString& database, const QString& name)
+{
+ return schemaResolver->getViews(database).contains(name, Qt::CaseInsensitive);
+}
+
+QStringList SelectResolver::getTableColumns(const QString &database, const QString &table, const QString& alias)
+{
+ Table dbTable;
+ dbTable.database = database;
+ dbTable.table = table;
+ dbTable.alias = alias;
+
+ if (tableColumnsCache.contains(dbTable))
+ return tableColumnsCache.value(dbTable);
+ else
+ {
+ QStringList columns = schemaResolver->getTableColumns(database, table);
+ tableColumnsCache[dbTable] = columns;
+ return columns;
+ }
+}
+
+void SelectResolver::applySubSelectAlias(QList<SelectResolver::Column>& columns, const QString& alias)
+{
+ // If this subselect is aliased, then all source columns should be considered as from aliased table
+ QMutableListIterator<Column> it(columns);
+ if (!alias.isNull())
+ {
+ while (it.hasNext())
+ {
+ it.next().tableAlias = alias;
+ it.value().flags &= ~FROM_ANONYMOUS_SELECT; // remove anonymous flag
+ }
+ }
+ else
+ {
+ // Otherwise, mark column as being from anonymous subselect.
+ // This is used by QueryExecutorColumns step to avoid prefixing result column with table
+ // when it comes from anonymous subselect (which SQLite needs this to be not prefixed column).
+ while (it.hasNext())
+ it.next().flags |= FROM_ANONYMOUS_SELECT;
+ }
+}
+
+QString SelectResolver::resolveDatabase(const QString& database)
+{
+ if (dbNameToAttach.containsRight(database, Qt::CaseInsensitive))
+ return dbNameToAttach.valueByRight(database, Qt::CaseInsensitive);
+
+ return database;
+}
+
+int SelectResolver::Table::operator ==(const SelectResolver::Table &other)
+{
+ return table == other.table && database == other.database && alias == other.alias;
+}
+
+int operator==(const SelectResolver::Table& t1, const SelectResolver::Table& t2)
+{
+ return t1.table == t2.table && t1.database == t2.database && t1.alias == t2.alias;
+}
+
+uint qHash(const SelectResolver::Table& table)
+{
+ return qHash(table.database + "." + table.table + "." + table.alias);
+}
+
+int SelectResolver::Column::operator ==(const SelectResolver::Column &other)
+{
+ return table == other.table && database == other.database && column == other.column && tableAlias == other.tableAlias;
+}
+
+SelectResolver::Table SelectResolver::Column::getTable()
+{
+ Table resTable;
+ resTable.table = table;
+ resTable.database = database;
+ resTable.originalDatabase = originalDatabase;
+ resTable.alias = tableAlias;
+ resTable.flags = flags;
+ return resTable;
+}
+
+int operator ==(const SelectResolver::Column &c1, const SelectResolver::Column &c2)
+{
+ return c1.table == c2.table && c1.database == c2.database && c1.column == c2.column && c1.tableAlias == c2.tableAlias;
+}
+
+
+uint qHash(const SelectResolver::Column &column)
+{
+ return qHash(column.database + "." + column.table + "." + column.column + "/" + column.tableAlias);
+}