aboutsummaryrefslogtreecommitdiffstats
path: root/SQLiteStudio3/coreSQLiteStudio/completionhelper.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'SQLiteStudio3/coreSQLiteStudio/completionhelper.cpp')
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/completionhelper.cpp1425
1 files changed, 1425 insertions, 0 deletions
diff --git a/SQLiteStudio3/coreSQLiteStudio/completionhelper.cpp b/SQLiteStudio3/coreSQLiteStudio/completionhelper.cpp
new file mode 100644
index 0000000..99eb334
--- /dev/null
+++ b/SQLiteStudio3/coreSQLiteStudio/completionhelper.cpp
@@ -0,0 +1,1425 @@
+#include "completionhelper.h"
+#include "completioncomparer.h"
+#include "db/db.h"
+#include "parser/keywords.h"
+#include "parser/parser.h"
+#include "parser/lexer.h"
+#include "parser/ast/sqlitecreatetable.h"
+#include "parser/ast/sqlitecreatetrigger.h"
+#include "dbattacher.h"
+#include "common/utils.h"
+#include "common/utils_sql.h"
+#include "services/dbmanager.h"
+#include <QStringList>
+#include <QDebug>
+
+QStringList sqlite3Pragmas;
+QStringList sqlite2Pragmas;
+QStringList sqlite3Functions;
+QStringList sqlite2Functions;
+
+bool CompletionHelper::enableLemonDebug = false;
+
+CompletionHelper::CompletionHelper(const QString &sql, Db* db)
+ : CompletionHelper(sql, sql.length(), db)
+{
+}
+
+CompletionHelper::CompletionHelper(const QString &sql, quint32 cursorPos, Db* db)
+ : db(db), cursorPosition(cursorPos), fullSql(sql)
+{
+ schemaResolver = new SchemaResolver(db);
+ selectResolver = new SelectResolver(db, fullSql);
+ selectResolver->ignoreInvalidNames = true;
+ dbAttacher = SQLITESTUDIO->createDbAttacher(db);
+}
+
+void CompletionHelper::init()
+{
+ sqlite3Pragmas << "auto_vacuum" << "automatic_index" << "busy_timeout" << "cache_size"
+ << "case_sensitive_like" << "checkpoint_fullfsync" << "collation_list"
+ << "compile_options" << "count_changes" << "data_store_directory"
+ << "database_list" << "default_cache_size" << "empty_result_callbacks"
+ << "encoding" << "foreign_key_check" << "foreign_key_list" << "foreign_keys"
+ << "freelist_count" << "full_column_names" << "fullfsync"
+ << "ignore_check_constraints" << "incremental_vacuum" << "index_info"
+ << "index_list" << "integrity_check" << "journal_mode" << "journal_size_limit"
+ << "legacy_file_format" << "locking_mode" << "max_page_count" << "page_count"
+ << "page_size" << "quick_check" << "read_uncommitted" << "recursive_triggers"
+ << "reverse_unordered_selects" << "schema_version" << "secure_delete"
+ << "short_column_names" << "shrink_memory" << "synchronous" << "table_info"
+ << "temp_store" << "temp_store_directory" << "user_version"
+ << "wal_autocheckpoint" << "wal_checkpoint" << "writable_schema";
+
+ sqlite2Pragmas << "cache_size" << "count_changes" << "database_list" << "default_cache_size"
+ << "default_synchronous" << "default_temp_store" << "empty_result_callbacks"
+ << "foreign_key_list" << "full_column_names" << "index_info" << "index_list"
+ << "integrity_check" << "parser_trace" << "show_datatypes" << "synchronous"
+ << "table_info" << "temp_store";
+
+ sqlite3Functions << "avg(X)" << "count(X)" << "count(*)" << "group_concat(X)"
+ << "group_concat(X,Y)" << "max(X)" << "min(X)" << "sum(X)" << "total(X)"
+ << "abs(X)" << "changes()" << "char(X1,X2,...,XN)" << "coalesce(X,Y,...)"
+ << "glob(X,Y)" << "ifnull(X,Y)" << "instr(X,Y)" << "hex(X)"
+ << "last_insert_rowid()" << "length(X)" << "like(X,Y)" << "like(X,Y,Z)"
+ << "load_extension(X,Y)" << "lower(X)" << "ltrim(X)" << "ltrim(X,Y)"
+ << "max(X,Y,...)" << "min(X,Y,...)" << "nullif(X,Y)" << "quote(X)"
+ << "random()" << "randomblob(N)" << "hex(randomblob(16))"
+ << "lower(hex(randomblob(16)))" << "replace(X,Y,Z)" << "round(X)"
+ << "round(X,Y)" << "rtrim(X)" << "rtrim(X,Y)" << "soundex(X)"
+ << "sqlite_compileoption_get(N)" << "sqlite_compileoption_used(X)"
+ << "sqlite_source_id()" << "sqlite_version()" << "substr(X,Y,Z)"
+ << "substr(X,Y)" << "total_changes()" << "trim(X)" << "trim(X,Y)"
+ << "typeof(X)" << "unicode(X)" << "upper(X)" << "zeroblob(N)"
+ << "date(timestr,mod,mod,...)" << "time(timestr,mod,mod,...)"
+ << "datetime(timestr,mod,mod,...)" << "julianday(timestr,mod,mod,...)"
+ << "strftime(format,timestr,mod,mod,...)" << "likelihood(X,Y)"
+ << "likely(X)" << "unlikely(X)";
+
+ sqlite2Functions << "abs(X)" << "coalesce(X,Y,...)" << "glob(X,Y)" << "ifnull(X,Y)"
+ << "last_insert_rowid()" << "length(X)" << "like(X,Y)" << "lower(X)"
+ << "max(X,Y,...)" << "min(X,Y,...)" << "nullif(X,Y)" << "random(*)"
+ << "round(X,)" << "round(X,Y)" << "soundex(X)" << "sqlite_version(*)"
+ << "substr(X,Y,Z)" << "typeof(X)" << "upper(X)" << "avg(X)" << "count(X)"
+ << "count(*)" << "max(X)" << "min(X)" << "sum(X)";
+
+ sqlite2Pragmas.sort();
+ sqlite3Pragmas.sort();
+ sqlite2Functions.sort();
+ sqlite3Functions.sort();
+}
+
+CompletionHelper::~CompletionHelper()
+{
+ if (schemaResolver)
+ {
+ delete schemaResolver;
+ schemaResolver = nullptr;
+ }
+
+ if (selectResolver)
+ {
+ delete selectResolver;
+ selectResolver = nullptr;
+ }
+
+ if (dbAttacher)
+ {
+ delete dbAttacher;
+ dbAttacher = nullptr;
+ }
+}
+
+CompletionHelper::Results CompletionHelper::getExpectedTokens()
+{
+ if (!db || !db->isValid())
+ return Results();
+
+ // Get SQL up to the current cursor position.
+ QString adjustedSql = fullSql.mid(0, cursorPosition);
+
+ // If asked for completion when being in the middle of keyword or ID,
+ // then remove that unfinished keyword/ID from sql and put it into
+ // the final filter - to be used at the end of this method.
+ QString finalFilter = QString::null;
+ bool wrappedFilter = false;
+ adjustedSql = removeStartedToken(adjustedSql, finalFilter, wrappedFilter);
+
+ // Parse SQL up to cursor position, get accepted tokens and tokens that were parsed.
+ Parser parser(db->getDialect());
+ TokenList tokens = parser.getNextTokenCandidates(adjustedSql);
+ TokenList parsedTokens = parser.getParsedTokens();
+
+ // Parse the full sql in regular mode to extract query statement
+ // for the results comparer and table-alias mapping.
+ parseFullSql();
+
+ // Collect used db names in original query (before using attach names)
+ collectOtherDatabases();
+
+ // Handle transparent db attaching
+ attachDatabases();
+
+ // Get previous ID tokens (db and table) if any
+ extractPreviousIdTokens(parsedTokens);
+
+ // Now, that we have parsed query, we can extract some useful information
+ // depending on the type of query we have.
+ extractQueryAdditionalInfo();
+
+ // Convert accepted tokens to expected tokens
+ QList<ExpectedTokenPtr> results;
+ foreach (TokenPtr token, tokens)
+ results += getExpectedTokens(token);
+
+ // Filter redundant tokens from results
+ filterContextKeywords(results, tokens);
+ filterOtherId(results, tokens);
+ filterDuplicates(results);
+
+ // ...and sort the output.
+ sort(results);
+
+ // Detach any databases attached for the completer needs
+ detachDatabases();
+
+ Results complexResult;
+ complexResult.expectedTokens = results;
+ complexResult.partialToken = finalFilter;
+ complexResult.wrappedToken = wrappedFilter;
+ return complexResult;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getExpectedTokens(TokenPtr token)
+{
+ QList<ExpectedTokenPtr> results;
+
+ // Initial conditions
+ if (previousId)
+ {
+ if (!token->isDbObjectType())
+ return results;
+
+ if (twoIdsBack && token->type != Token::CTX_COLUMN)
+ return results;
+ }
+
+ // Main routines
+ switch (token->type)
+ {
+ case Token::CTX_ROWID_KW:
+ results += getExpectedToken(ExpectedToken::KEYWORD, token->value);
+ break;
+ case Token::CTX_NEW_KW:
+ {
+ if (context == Context::CREATE_TRIGGER)
+ results += getExpectedToken(ExpectedToken::TABLE, "new", QString::null, tr("New row reference"), 1);
+
+ break;
+ }
+ case Token::CTX_OLD_KW:
+ {
+ if (context == Context::CREATE_TRIGGER)
+ results += getExpectedToken(ExpectedToken::TABLE, "old", QString::null, tr("Old row reference"), 1);
+
+ break;
+ }
+ case Token::CTX_TABLE_NEW:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("New table name"));
+ break;
+ case Token::CTX_INDEX_NEW:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("New index name"));
+ break;
+ case Token::CTX_VIEW_NEW:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("New view name"));
+ break;
+ case Token::CTX_TRIGGER_NEW:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("New trigger name"));
+ break;
+ case Token::CTX_ALIAS:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("Table or column alias"));
+ break;
+ case Token::CTX_TRANSACTION:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("transaction name"));
+ break;
+ case Token::CTX_COLUMN_NEW:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("New column name"));
+ break;
+ case Token::CTX_COLUMN_TYPE:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("Column data type"));
+ break;
+ case Token::CTX_CONSTRAINT:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("Constraint name"));
+ break;
+ case Token::CTX_FK_MATCH:
+ {
+ foreach (QString kw, getFkMatchKeywords())
+ results += getExpectedToken(ExpectedToken::KEYWORD, kw);
+
+ break;
+ }
+ case Token::CTX_PRAGMA:
+ results += getPragmas(db->getDialect());
+ break;
+ case Token::CTX_ERROR_MESSAGE:
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("Error message"));
+ break;
+ case Token::CTX_COLUMN:
+ {
+ results += getColumns();
+ break;
+ }
+ case Token::CTX_TABLE:
+ {
+ results += getTables();
+ break;
+ }
+ case Token::CTX_INDEX:
+ {
+ results += getIndexes();
+ break;
+ }
+ case Token::CTX_TRIGGER:
+ {
+ results += getTriggers();
+ break;
+ }
+ case Token::CTX_VIEW:
+ {
+ results += getViews();
+ break;
+ }
+ case Token::CTX_DATABASE:
+ {
+ results += getDatabases();
+ break;
+ }
+ case Token::CTX_FUNCTION:
+ {
+ results += getFunctions(db);
+ break;
+ }
+ case Token::CTX_COLLATION:
+ {
+ if (db->getDialect() == Dialect::Sqlite2)
+ {
+ // SQLite 2 doesn't really support collation. It has collations
+ // in grammar, but doesn't make use of them. There's no list
+ // of collations to be suggested.
+ results += getExpectedToken(ExpectedToken::NO_VALUE, QString::null, QString::null, tr("Collation name"));
+ }
+ else
+ {
+ results += getCollations();
+ }
+ break;
+ }
+ case Token::CTX_JOIN_OPTS:
+ {
+ foreach (QString joinKw, getJoinKeywords())
+ results += getExpectedToken(ExpectedToken::KEYWORD, joinKw);
+ break;
+ }
+ case Token::OTHER:
+ results += getExpectedToken(ExpectedToken::OTHER, QString::null, QString::null, tr("Any word"));
+ break;
+ case Token::STRING:
+ results += getExpectedToken(ExpectedToken::STRING);
+ break;
+ case Token::FLOAT:
+ results += getExpectedToken(ExpectedToken::NUMBER);
+ break;
+ case Token::INTEGER:
+ results += getExpectedToken(ExpectedToken::NUMBER);
+ break;
+ case Token::OPERATOR:
+ results += getExpectedToken(ExpectedToken::OPERATOR, token->value);
+ break;
+ case Token::PAR_LEFT:
+ results += getExpectedToken(ExpectedToken::OPERATOR, "(");
+ break;
+ case Token::PAR_RIGHT:
+ results += getExpectedToken(ExpectedToken::OPERATOR, ")");
+ break;
+ case Token::BLOB:
+ results += getExpectedToken(ExpectedToken::BLOB);
+ break;
+ case Token::KEYWORD:
+ results += getExpectedToken(ExpectedToken::KEYWORD, token->value);
+ break;
+ case Token::INVALID:
+ // No-op
+ break;
+ case Token::BIND_PARAM:
+ // No-op
+ break;
+ case Token::SPACE:
+ // No-op
+ break;
+ case Token::COMMENT:
+ // No-op
+ break;
+ }
+
+ return results;
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type)
+{
+ ExpectedToken* token = new ExpectedToken();
+ token->type = type;
+ return ExpectedTokenPtr(token);
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type, const QString &value)
+{
+ ExpectedTokenPtr token = getExpectedToken(type);
+ token->value = value;
+ return token;
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type, const QString &value,
+ int priority)
+{
+ ExpectedTokenPtr token = getExpectedToken(type, value);
+ token->priority = priority;
+ return token;
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type, const QString &value,
+ const QString& contextInfo)
+{
+ ExpectedTokenPtr token = getExpectedToken(type, value);
+ token->contextInfo = contextInfo;
+ token->label = contextInfo;
+ return token;
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type, const QString &value,
+ const QString& contextInfo, int priority)
+{
+ ExpectedTokenPtr token = getExpectedToken(type, value, contextInfo);
+ token->priority = priority;
+ return token;
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type, const QString &value,
+ const QString& contextInfo,
+ const QString& label)
+{
+ ExpectedTokenPtr token = getExpectedToken(type, value, contextInfo);
+ token->label = label;
+ return token;
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type, const QString &value,
+ const QString& contextInfo,
+ const QString& label, int priority)
+{
+ ExpectedTokenPtr token = getExpectedToken(type, value, contextInfo, label);
+ token->priority = priority;
+ return token;
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type, const QString &value,
+ const QString& contextInfo,
+ const QString& label,
+ const QString& prefix)
+{
+ ExpectedTokenPtr token = getExpectedToken(type, value, contextInfo, label);
+ token->prefix = prefix;
+ return token;
+}
+
+ExpectedTokenPtr CompletionHelper::getExpectedToken(ExpectedToken::Type type, const QString &value,
+ const QString& contextInfo,
+ const QString& label,
+ const QString& prefix,
+ int priority)
+{
+ ExpectedTokenPtr token = getExpectedToken(type, value, contextInfo, label, prefix);
+ token->priority = priority;
+ return token;
+}
+
+bool CompletionHelper::validatePreviousIdForGetObjects(QString* dbName)
+{
+ QString localDbName;
+ if (previousId)
+ {
+ localDbName = previousId->value;
+ QStringList databases = schemaResolver->getDatabases().toList();
+ databases += DBLIST->getDbNames();
+ if (!databases.contains(localDbName, Qt::CaseInsensitive))
+ return false; // if db is not on the set, then getObjects() would return empty list anyway;
+
+ if (dbName)
+ *dbName = localDbName;
+ }
+ return true;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getTables()
+{
+ QString dbName;
+ if (!validatePreviousIdForGetObjects(&dbName))
+ return QList<ExpectedTokenPtr>();
+
+ QList<ExpectedTokenPtr> tables = getObjects(ExpectedToken::TABLE);
+ for (const QString& otherDb : otherDatabasesToLookupFor)
+ tables += getObjects(ExpectedToken::TABLE, otherDb);
+
+ tables += getExpectedToken(ExpectedToken::TABLE, "sqlite_master", dbName);
+ tables += getExpectedToken(ExpectedToken::TABLE, "sqlite_temp_master", dbName);
+ return tables;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getIndexes()
+{
+ if (!validatePreviousIdForGetObjects())
+ return QList<ExpectedTokenPtr>();
+
+ return getObjects(ExpectedToken::INDEX);
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getTriggers()
+{
+ if (!validatePreviousIdForGetObjects())
+ return QList<ExpectedTokenPtr>();
+
+ return getObjects(ExpectedToken::TRIGGER);
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getViews()
+{
+ if (!validatePreviousIdForGetObjects())
+ return QList<ExpectedTokenPtr>();
+
+ return getObjects(ExpectedToken::VIEW);
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getDatabases()
+{
+ QList<ExpectedTokenPtr> results;
+
+ results += getExpectedToken(ExpectedToken::DATABASE, "main", "main", tr("Default database"));
+ results += getExpectedToken(ExpectedToken::DATABASE, "temp", "temp", tr("Temporary objects database"));
+
+ QSet<QString> databases = schemaResolver->getDatabases();
+ foreach (QString dbName, databases)
+ {
+ if (dbAttacher->getDbNameToAttach().containsRight(dbName, Qt::CaseInsensitive))
+ continue;
+
+ results += getExpectedToken(ExpectedToken::DATABASE, dbName);
+ }
+
+ Dialect dialect = db->getDialect();
+
+ foreach (Db* otherDb, DBLIST->getValidDbList())
+ {
+ if (otherDb->getDialect() != dialect)
+ continue;
+
+ results += getExpectedToken(ExpectedToken::DATABASE, otherDb->getName());
+ }
+
+ return results;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getObjects(ExpectedToken::Type type)
+{
+ if (previousId)
+ return getObjects(type, previousId->value);
+ else
+ return getObjects(type, QString());
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getObjects(ExpectedToken::Type type, const QString& database)
+{
+ QString dbName;
+ QString originalDbName;
+ if (!database.isNull())
+ {
+ dbName = translateDatabase(database);
+ originalDbName = database;
+ }
+
+ QString typeStr;
+ switch (type)
+ {
+ case ExpectedToken::TABLE:
+ typeStr = "table";
+ break;
+ case ExpectedToken::INDEX:
+ typeStr = "index";
+ break;
+ case ExpectedToken::TRIGGER:
+ typeStr = "trigger";
+ break;
+ case ExpectedToken::VIEW:
+ typeStr = "view";
+ break;
+ default:
+ qWarning() << "Invalid type passed to CompletionHelper::getObject().";
+ return QList<ExpectedTokenPtr>();
+ }
+
+ QList<ExpectedTokenPtr> results;
+ foreach (QString object, schemaResolver->getObjects(dbName, typeStr))
+ results << getExpectedToken(type, object, originalDbName);
+
+ return results;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getColumns()
+{
+ QList<ExpectedTokenPtr> results;
+ if (previousId)
+ {
+ if (twoIdsBack)
+ results += getColumns(twoIdsBack->value, previousId->value);
+ else
+ results += getColumns(previousId->value);
+ }
+ else
+ {
+ results += getColumnsNoPrefix();
+ }
+
+ if (favoredColumnNames.size() > 0)
+ results += getFavoredColumns(results);
+
+ return results;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getColumnsNoPrefix()
+{
+ QList<ExpectedTokenPtr> results;
+
+ // Use available columns for other databases (transparently attached)
+ QString ctx;
+ for (SelectResolver::Column& column : selectAvailableColumns)
+ {
+ if (column.database.isNull())
+ continue;
+
+ if (column.tableAlias.isNull())
+ ctx = translateDatabaseBack(column.database)+"."+column.table;
+ else
+ ctx = column.tableAlias+" = "+translateDatabaseBack(column.database)+"."+column.table;
+
+ results << getExpectedToken(ExpectedToken::COLUMN, column.column, ctx);
+ }
+
+ // No db or table provided. For each column its table is remembered,
+ // so in case some column repeats in more than one table, then we need
+ // to add prefix for the completion proposal.
+ QHash<QString,QStringList> columnList;
+
+ // Getting all tables for main db. If any column repeats in many tables,
+ // then tables are stored as a list for the same column.
+ foreach (QString table, schemaResolver->getTables(QString::null))
+ foreach (QString column, schemaResolver->getTableColumns(table))
+ columnList[column] += table;
+
+ // Now, for each column the expected token is created.
+ // If a column occured in more tables, then multiple expected tokens
+ // are created to reflect all possible tables.
+ QHashIterator<QString,QStringList> it(columnList);
+ while (it.hasNext())
+ {
+ it.next();
+ results << getColumnsNoPrefix(it.key(), it.value());
+ }
+
+ return results;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getColumnsNoPrefix(const QString& column, const QStringList& tables)
+{
+ QList<ExpectedTokenPtr> results;
+
+ QStringList availableTableNames;
+ foreach (SelectResolver::Table resolverTable, selectAvailableTables + parentSelectAvailableTables)
+ {
+ // This method is called only when collecting columns of tables in "main" database.
+ // If here we have resolved table from other database, we don't compare it.
+ if (!resolverTable.database.isNull() && resolverTable.database.toLower() != "main")
+ continue;
+
+ availableTableNames += resolverTable.table;
+ }
+
+ int availableTableCount = 0;
+ foreach (QString availTable, availableTableNames)
+ if (tables.contains(availTable))
+ availableTableCount++;
+
+ foreach (QString table, tables)
+ {
+ // Table prefix is used only if there is more than one table in FROM clause
+ // that has this column, or table alias was used.
+ if (!currentSelectCore || (availableTableCount <= 1 && !tableToAlias.contains(table)))
+ results << getExpectedToken(ExpectedToken::COLUMN, column, table);
+ else
+ {
+ // The prefix table might need translation to an alias.
+ QString prefix = table;
+ QString label = table;
+ if (tableToAlias.contains(prefix))
+ {
+ foreach (prefix, tableToAlias[prefix])
+ {
+ label = prefix+" = "+table;
+ results << getExpectedToken(ExpectedToken::COLUMN, column, table, label, prefix);
+ }
+ }
+ else
+ results << getExpectedToken(ExpectedToken::COLUMN, column, table, label, prefix);
+ }
+ }
+
+ return results;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getColumns(const QString &prefixTable)
+{
+ QList<ExpectedTokenPtr> results;
+
+ QString label = prefixTable;
+ QString table = prefixTable;
+ QString dbName;
+ if (aliasToTable.contains(prefixTable))
+ {
+ Table tableAndDb = aliasToTable.value(prefixTable);
+ table = tableAndDb.getTable();
+ dbName = tableAndDb.getDatabase();
+ label = prefixTable+" = "+table;
+ }
+
+ // CREATE TRIGGER has a special "old" and "new" keywords as aliases for deleted/inserted/updated rows.
+ // They should refer to a table that the trigger is created for.
+ QString tableLower = table;
+ if (context == Context::CREATE_TRIGGER && (tableLower == "old" || tableLower == "new"))
+ {
+ if (!createTriggerTable.isNull())
+ {
+ table = createTriggerTable;
+ label = createTriggerTable;
+ }
+ else
+ {
+ SqliteCreateTriggerPtr createTrigger = parsedQuery.dynamicCast<SqliteCreateTrigger>();
+ if (createTrigger && !createTrigger->table.isNull())
+ {
+ table = createTrigger->table;
+ label = table;
+ }
+ }
+ }
+
+ // Get columns for given table in main db.
+ foreach (const QString& column, schemaResolver->getTableColumns(dbName, table))
+ results << getExpectedToken(ExpectedToken::COLUMN, column, table, label);
+
+ return results;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getColumns(const QString &prefixDb, const QString &prefixTable)
+{
+ QList<ExpectedTokenPtr> results;
+
+ // Get columns for given table in given db.
+ QString context = prefixDb+"."+prefixTable;
+ foreach (const QString& column, schemaResolver->getTableColumns(translateDatabase(prefixDb), prefixTable))
+ results << getExpectedToken(ExpectedToken::COLUMN, column, context);
+
+ return results;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getFavoredColumns(const QList<ExpectedTokenPtr>& resultsSoFar)
+{
+ // Prepare list that doesn't create duplicates with the results we already have.
+ // Since results so far have more chance to provide context into, we will keep the original ones
+ // from results so far and avoid adding then from favored list.
+ QStringList columnsToAdd = favoredColumnNames;
+ foreach (const ExpectedTokenPtr& token, resultsSoFar)
+ {
+ if (token->prefix.isNull() && columnsToAdd.contains(token->value))
+ columnsToAdd.removeOne(token->value);
+ }
+
+ QString ctxInfo;
+ if (context == Context::CREATE_TABLE && parsedQuery)
+ ctxInfo = parsedQuery.dynamicCast<SqliteCreateTable>()->table;
+
+ QList<ExpectedTokenPtr> results;
+ foreach (const QString& column, columnsToAdd)
+ results << getExpectedToken(ExpectedToken::COLUMN, column, ctxInfo);
+
+ return results;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getFunctions(Db* db)
+{
+ // TODO to do later - make function completion more verbose,
+ // like what are arguments of the function, etc.
+ Dialect dialect = db->getDialect();
+
+ QStringList functions;
+ if (dialect == Dialect::Sqlite2)
+ functions = sqlite2Functions;
+ else
+ functions = sqlite3Functions;
+
+ for (FunctionManager::ScriptFunction* fn : FUNCTIONS->getScriptFunctionsForDatabase(db->getName()))
+ functions << fn->toString();
+
+ for (FunctionManager::NativeFunction* fn : FUNCTIONS->getAllNativeFunctions())
+ functions << fn->toString();
+
+ QList<ExpectedTokenPtr> expectedTokens;
+ foreach (QString function, functions)
+ expectedTokens += getExpectedToken(ExpectedToken::FUNCTION, function);
+
+ return expectedTokens;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getPragmas(Dialect dialect)
+{
+ QStringList pragmas;
+ if (dialect == Dialect::Sqlite2)
+ pragmas = sqlite2Pragmas;
+ else
+ pragmas = sqlite3Pragmas;
+
+ QList<ExpectedTokenPtr> expectedTokens;
+ foreach (QString pragma, pragmas)
+ expectedTokens += getExpectedToken(ExpectedToken::PRAGMA, pragma);
+
+ return expectedTokens;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::getCollations()
+{
+ SqlQueryPtr results = db->exec("PRAGMA collation_list;");
+ if (results->isError())
+ {
+ qWarning() << "Got error when trying to get collation_list: "
+ << results->getErrorText();
+ }
+ QList<ExpectedTokenPtr> expectedTokens;
+ foreach (SqlResultsRowPtr row, results->getAll())
+ expectedTokens += getExpectedToken(ExpectedToken::COLLATION, row->value("name").toString());
+
+ return expectedTokens;
+}
+
+TokenPtr CompletionHelper::getPreviousDbOrTable(const TokenList &parsedTokens)
+{
+ // First check if we even get to deal with db.table or table.column.
+ // In order to do that we iterate backwards starting from the end.
+ TokenPtr token;
+ QListIterator<TokenPtr> it(parsedTokens);
+ it.toBack();
+
+ if (!it.hasPrevious())
+ return TokenPtr(); // No tokens at all. Shouldn't really happen.
+
+ token = it.previous();
+
+ // Skip spaces and comments
+ while ((token->type == Token::SPACE || token->type == Token::COMMENT) && it.hasPrevious())
+ token = it.previous();
+
+ // Check if first non-space and non-comment token is our dot.
+ if (token->type != Token::OPERATOR || token->value != ".")
+ return TokenPtr(); // Previous token is not a dot.
+
+ // We have a dot, now let's look for another token before.
+ if (!it.hasPrevious())
+ return TokenPtr(); // No more tokens left in front.
+
+ token = it.previous();
+
+ // Skip spaces and comments
+ while ((token->type == Token::SPACE || token->type == Token::COMMENT) && it.hasPrevious())
+ token = it.previous();
+
+ // Check if this token is an ID.
+ if (token->type != Token::OTHER)
+ return TokenPtr(); // One more token before is not an ID.
+
+ // Okay, so now we now we have "some_id."
+ // Since this method is called in known context (looking for either db or table),
+ // we don't need to find out what the "some_id" is. We simple return its value.
+ return token;
+}
+
+void CompletionHelper::attachDatabases()
+{
+ if (!parsedQuery)
+ return;
+
+ if (!dbAttacher->attachDatabases(parsedQuery))
+ return;
+
+ QString query = parsedQuery->detokenize();
+
+ Parser parser(db->getDialect());
+ if (parser.parse(query, true) && !parser.getQueries().isEmpty())
+ parsedQuery = parser.getQueries().first();
+}
+
+void CompletionHelper::detachDatabases()
+{
+ dbAttacher->detachDatabases();
+}
+
+QString CompletionHelper::translateDatabase(const QString& dbName)
+{
+ if (!dbAttacher->getDbNameToAttach().containsLeft(dbName, Qt::CaseInsensitive))
+ return dbName;
+
+ return dbAttacher->getDbNameToAttach().valueByLeft(dbName, Qt::CaseInsensitive);
+}
+
+QString CompletionHelper::translateDatabaseBack(const QString& dbName)
+{
+ if (!dbAttacher->getDbNameToAttach().containsRight(dbName, Qt::CaseInsensitive))
+ return dbName;
+
+ return dbAttacher->getDbNameToAttach().valueByRight(dbName, Qt::CaseInsensitive);
+}
+
+void CompletionHelper::collectOtherDatabases()
+{
+ otherDatabasesToLookupFor.clear();
+ if (!parsedQuery)
+ return;
+
+ otherDatabasesToLookupFor = parsedQuery->getContextDatabases();
+}
+
+QString CompletionHelper::removeStartedToken(const QString& adjustedSql, QString& finalFilter, bool& wrappedFilter)
+{
+ QString result = adjustedSql;
+
+ Lexer lexer(db->getDialect());
+ TokenList tokens = lexer.tokenize(adjustedSql);
+ if (tokens.size() == 0)
+ return result;
+
+ TokenPtr lastToken = tokens.last();
+
+ if (isFilterType(lastToken->type))
+ {
+ result = Lexer::detokenize(tokens.mid(0, tokens.size()-1));
+ finalFilter = lastToken->value;
+
+ if (finalFilter.length() > 0 && isWrapperChar(finalFilter[0], db->getDialect()))
+ {
+ finalFilter = finalFilter.mid(1);
+ wrappedFilter = true;
+ }
+ }
+ return result;
+}
+
+void CompletionHelper::filterContextKeywords(QList<ExpectedTokenPtr> &resultsSoFar, const TokenList &tokens)
+{
+ bool wasJoinKw = false;
+ bool wasFkMatchKw = false;
+ foreach (TokenPtr token, tokens)
+ {
+ if (token->type == Token::CTX_JOIN_OPTS)
+ wasJoinKw = true;
+
+ if (token->type == Token::CTX_FK_MATCH)
+ wasFkMatchKw = true;
+ }
+
+ if (wasJoinKw && wasFkMatchKw)
+ return;
+
+ QMutableListIterator<ExpectedTokenPtr> it(resultsSoFar);
+ while (it.hasNext())
+ {
+ ExpectedTokenPtr token = it.next();
+ if (token->type != ExpectedToken::KEYWORD)
+ continue;
+
+ if (
+ (!wasJoinKw && isJoinKeyword(token->value)) ||
+ (!wasFkMatchKw && isFkMatchKeyword(token->value))
+ )
+ it.remove();
+ }
+}
+
+void CompletionHelper::filterOtherId(QList<ExpectedTokenPtr> &resultsSoFar, const TokenList &tokens)
+{
+ bool wasCtx = false;
+ foreach (TokenPtr token, tokens)
+ {
+ switch (token->type)
+ {
+ case Token::CTX_COLUMN:
+ case Token::CTX_TABLE:
+ case Token::CTX_DATABASE:
+ case Token::CTX_FUNCTION:
+ case Token::CTX_COLLATION:
+ case Token::CTX_INDEX:
+ case Token::CTX_TRIGGER:
+ case Token::CTX_VIEW:
+ case Token::CTX_JOIN_OPTS:
+ case Token::CTX_TABLE_NEW:
+ case Token::CTX_INDEX_NEW:
+ case Token::CTX_VIEW_NEW:
+ case Token::CTX_TRIGGER_NEW:
+ case Token::CTX_ALIAS:
+ case Token::CTX_TRANSACTION:
+ case Token::CTX_COLUMN_NEW:
+ case Token::CTX_COLUMN_TYPE:
+ case Token::CTX_CONSTRAINT:
+ case Token::CTX_FK_MATCH:
+ case Token::CTX_PRAGMA:
+ case Token::CTX_ROWID_KW:
+ case Token::CTX_NEW_KW:
+ case Token::CTX_OLD_KW:
+ case Token::CTX_ERROR_MESSAGE:
+ wasCtx = true;
+ break;
+ default:
+ break;
+ }
+ if (wasCtx)
+ break;
+ }
+
+ if (!wasCtx)
+ return;
+
+ QMutableListIterator<ExpectedTokenPtr> it(resultsSoFar);
+ while (it.hasNext())
+ {
+ ExpectedTokenPtr token = it.next();
+ if (token->type == ExpectedToken::OTHER)
+ it.remove();
+ }
+}
+
+void CompletionHelper::filterDuplicates(QList<ExpectedTokenPtr>& resultsSoFar)
+{
+ QSet<ExpectedTokenPtr> set = resultsSoFar.toSet();
+ resultsSoFar = set.toList();
+}
+
+void CompletionHelper::applyFilter(QList<ExpectedTokenPtr>& resultsSoFar, const QString& filter)
+{
+ if (filter.isEmpty())
+ return;
+
+ QMutableListIterator<ExpectedTokenPtr> it(resultsSoFar);
+ while (it.hasNext())
+ {
+ ExpectedTokenPtr token = it.next();
+ if (!token->value.startsWith(filter, Qt::CaseInsensitive))
+ it.remove();
+ }
+}
+
+bool CompletionHelper::isFilterType(Token::Type type)
+{
+ switch (type)
+ {
+ case Token::COMMENT:
+ case Token::SPACE:
+ case Token::PAR_LEFT:
+ case Token::PAR_RIGHT:
+ case Token::OPERATOR:
+ return false;
+ default:
+ return true;
+ }
+}
+
+void CompletionHelper::parseFullSql()
+{
+ Dialect dialect = db->getDialect();
+
+ Parser parser(dialect);
+ parser.setLemonDebug(enableLemonDebug);
+
+ QString sql = fullSql;
+
+ // Selecting query at cursor position
+ QString query = getQueryWithPosition(sql, cursorPosition, dialect);
+
+ // Token list of the query. Also useful, not only parsed query.
+ queryTokens = Lexer::tokenize(query, dialect);
+ queryTokens.trim();
+
+ // Completing query
+ if (!query.trimmed().endsWith(";"))
+ query += ";";
+
+ // Parsing query
+ if (parser.parse(query, true) && !parser.getQueries().isEmpty())
+ {
+ parsedQuery = parser.getQueries().first();
+ originalParsedQuery = SqliteQueryPtr(dynamic_cast<SqliteQuery*>(parsedQuery->clone()));
+ }
+}
+
+void CompletionHelper::sort(QList<ExpectedTokenPtr> &resultsSoFar)
+{
+ CompletionComparer comparer(this);
+ qSort(resultsSoFar.begin(), resultsSoFar.end(), comparer);
+}
+
+void CompletionHelper::extractPreviousIdTokens(const TokenList &parsedTokens)
+{
+ Dialect dialect = db->getDialect();
+
+ // The previous ID token (if any) is being used in
+ // getExpectedToken() and it's always the same token,
+ // so here we find it just once and reuse it.
+ previousId = stripObjName(getPreviousDbOrTable(parsedTokens), dialect);
+
+ // In case of column context we need to know if there was
+ // up to two ID tokens before. If we had one above,
+ // then we check for one more here.
+ twoIdsBack.clear();
+ if (previousId)
+ {
+ int idx = parsedTokens.indexOf(previousId);
+ TokenList parsedTokensSubSet = parsedTokens.mid(0, idx);
+ twoIdsBack = stripObjName(getPreviousDbOrTable(parsedTokensSubSet), dialect);
+ }
+}
+
+void CompletionHelper::extractQueryAdditionalInfo()
+{
+ if (extractSelectCore())
+ {
+ extractSelectAvailableColumnsAndTables();
+ extractTableAliasMap();
+ removeDuplicates(parentSelectAvailableColumns);
+ detectSelectContext();
+ }
+ else if (isInUpdateColumn())
+ {
+ context = Context::UPDATE_COLUMN;
+ }
+ else if (isInUpdateWhere())
+ {
+ context = Context::UPDATE_WHERE;
+ }
+ else if (isInDeleteWhere())
+ {
+ context = Context::DELETE_WHERE;
+ }
+ else if (isInCreateTable())
+ {
+ context = Context::CREATE_TABLE;
+ extractCreateTableColumns();
+ }
+ else if (isInCreateTrigger())
+ {
+ context = Context::CREATE_TRIGGER;
+ }
+ else if (isInExpr())
+ {
+ context = Context::EXPR;
+ }
+}
+
+void CompletionHelper::detectSelectContext()
+{
+ QStringList mapNames = {"SELECT", "distinct", "selcollist", "from", "where_opt", "groupby_opt", "having_opt", "orderby_opt", "limit_opt"};
+ QList<Context> contexts = {Context::SELECT_RESULT_COLUMN, Context::SELECT_FROM, Context::SELECT_WHERE, Context::SELECT_GROUP_BY,
+ Context::SELECT_HAVING, Context::SELECT_ORDER_BY, Context::SELECT_LIMIT};
+
+ // Assert that we have exactly 2 more map names, than defined contexts, cause we will start with 3rd map name and 1st context.
+ Q_ASSERT((mapNames.size() - 2) == contexts.size());
+
+ for (int i = 2; i < mapNames.size(); i++)
+ {
+ if (cursorAfterTokenMaps(currentSelectCore, mapNames.mid(0, i)) && cursorBeforeTokenMaps(currentSelectCore, mapNames.mid(i+1)))
+ {
+ context = contexts[i-2];
+ break;
+ }
+ }
+}
+
+bool CompletionHelper::isInUpdateColumn()
+{
+ // We will never get here if the subquery SELECT was used anywhere in the query,
+ // (and the cursor position is in that subselect), because in that case the extractSelectCore()
+ // will take over the flow before it reaches here.
+ return isIn(SqliteQueryType::Update, "setlist", "SET");
+}
+
+bool CompletionHelper::isInUpdateWhere()
+{
+ return isIn(SqliteQueryType::Update, "where_opt", "WHERE");
+}
+
+bool CompletionHelper::isInDeleteWhere()
+{
+ return isIn(SqliteQueryType::Delete, "where_opt", "WHERE");
+}
+
+bool CompletionHelper::isInCreateTable()
+{
+ if (!parsedQuery)
+ {
+ if (testQueryToken(0, Token::KEYWORD, "CREATE") &&
+ (testQueryToken(1, Token::KEYWORD, "TABLE") ||
+ testQueryToken(2, Token::KEYWORD, "TABLE")))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ if (parsedQuery->queryType != SqliteQueryType::CreateTable)
+ return false;
+
+ return true;
+}
+
+bool CompletionHelper::isInCreateTrigger()
+{
+ if (!parsedQuery)
+ {
+ if (testQueryToken(0, Token::KEYWORD, "CREATE") &&
+ (testQueryToken(1, Token::KEYWORD, "TRIGGER") ||
+ testQueryToken(2, Token::KEYWORD, "TRIGGER")))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ if (parsedQuery->queryType != SqliteQueryType::CreateTrigger)
+ return false;
+
+ return true;
+}
+
+bool CompletionHelper::isIn(SqliteQueryType queryType, const QString &tokenMapKey, const QString &prefixKeyword)
+{
+ if (!parsedQuery)
+ return false;
+
+ if (parsedQuery->queryType != queryType)
+ return false;
+
+ // We're looking for curPos - 1, because tokens indexing ends in the same, with -1 index.
+ TokenPtr token = parsedQuery->tokens.atCursorPosition(cursorPosition - 1);
+ if (!token)
+ return false;
+
+ if (parsedQuery->tokensMap[tokenMapKey].contains(token))
+ return true;
+
+ // In case cursor is just before the requested token map entry, but it is after a whitespace, then we can
+ // assume, that what's coming next is our token map entry.
+ if (token->isWhitespace())
+ {
+ int idx = parsedQuery->tokens.indexOf(token);
+ if (idx < 0)
+ return false;
+
+ TokenList tokens = parsedQuery->tokens.mid(0, idx + 1);
+ tokens.trim();
+ if (tokens.size() > 0 && tokens.last()->type == Token::KEYWORD && tokens.last()->value.compare(prefixKeyword, Qt::CaseInsensitive) == 0)
+ return true;
+ }
+
+ return false;
+}
+
+bool CompletionHelper::isInExpr()
+{
+ if (!parsedQuery)
+ return false;
+
+ // Finding in which statement the cursor is positioned
+ // We're looking for curPos - 1, because tokens indexing ends in the same, with -1 index.
+ SqliteStatement* stmt = parsedQuery->findStatementWithPosition(cursorPosition - 1);
+
+ // Now going up in statements tree in order to find first expr
+ while (stmt && !dynamic_cast<SqliteExpr*>(stmt))
+ stmt = stmt->parentStatement();
+
+ return (stmt && dynamic_cast<SqliteExpr*>(stmt));
+}
+
+bool CompletionHelper::testQueryToken(int tokenPosition, Token::Type type, const QString& value, Qt::CaseSensitivity cs)
+{
+ if (tokenPosition >= queryTokens.size())
+ return false;
+
+ if (tokenPosition < 0)
+ return 0;
+
+ TokenPtr token = queryTokens[tokenPosition];
+ return (token->type == type && token->value.compare(value, cs) == 0);
+}
+
+bool CompletionHelper::cursorAfterTokenMaps(SqliteStatement* stmt, const QStringList& mapNames)
+{
+ TokenList tokens;
+ foreach (const QString& name, mapNames)
+ {
+ if (!stmt->tokensMap.contains(name) || stmt->tokensMap[name].size() == 0)
+ continue;
+
+ tokens = stmt->tokensMap[name];
+ tokens.trimRight();
+ if (tokens.size() == 0)
+ continue;
+
+ if (tokens.last()->end >= cursorPosition)
+ return false;
+ }
+ return true;
+}
+
+bool CompletionHelper::cursorBeforeTokenMaps(SqliteStatement* stmt, const QStringList& mapNames)
+{
+ TokenList tokens;
+ foreach (const QString& name, mapNames)
+ {
+ if (!stmt->tokensMap.contains(name) || stmt->tokensMap[name].size() == 0)
+ continue;
+
+ tokens = stmt->tokensMap[name];
+ tokens.trimLeft();
+ if (tokens.size() == 0)
+ continue;
+
+ if (tokens.first()->start < cursorPosition)
+ return false;
+ }
+ return true;
+}
+QString CompletionHelper::getCreateTriggerTable() const
+{
+ return createTriggerTable;
+}
+
+void CompletionHelper::setCreateTriggerTable(const QString& value)
+{
+ createTriggerTable = value;
+}
+
+DbAttacher* CompletionHelper::getDbAttacher() const
+{
+ return dbAttacher;
+}
+
+void CompletionHelper::setDbAttacher(DbAttacher* value)
+{
+ if (dbAttacher)
+ delete dbAttacher;
+
+ dbAttacher = value;
+}
+
+
+bool CompletionHelper::extractSelectCore()
+{
+ currentSelectCore = extractSelectCore(parsedQuery);
+ originalCurrentSelectCore = extractSelectCore(originalParsedQuery);
+ return (currentSelectCore != nullptr);
+}
+
+SqliteSelect::Core* CompletionHelper::extractSelectCore(SqliteQueryPtr query)
+{
+ if (!query)
+ return nullptr;
+
+ // Finding in which statement the cursor is positioned
+ // We're looking for curPos - 1, because tokens indexing ends in the same, with -1 index.
+ SqliteStatement* stmt = query->findStatementWithPosition(cursorPosition - 1);
+
+ // Now going up in statements tree in order to find first select core
+ while (stmt && !dynamic_cast<SqliteSelect::Core*>(stmt))
+ stmt = stmt->parentStatement();
+
+ if (stmt && dynamic_cast<SqliteSelect::Core*>(stmt))
+ {
+ // We found our select core
+ return dynamic_cast<SqliteSelect::Core*>(stmt);
+ }
+
+ return nullptr;
+}
+
+void CompletionHelper::extractSelectAvailableColumnsAndTables()
+{
+ selectAvailableColumns = selectResolver->resolveAvailableColumns(currentSelectCore);
+ selectAvailableTables = selectResolver->resolveTables(currentSelectCore);
+
+ // Now checking for any parent select cores.
+ SqliteStatement* stmt = currentSelectCore->parentStatement();
+ SqliteSelect::Core* parentCore = nullptr;
+ while (stmt)
+ {
+ while (stmt && !dynamic_cast<SqliteSelect::Core*>(stmt))
+ stmt = stmt->parentStatement();
+
+ if (!stmt || !dynamic_cast<SqliteSelect::Core*>(stmt))
+ return;
+
+ // We got another select core at higher level
+ parentCore = dynamic_cast<SqliteSelect::Core*>(stmt);
+ parentSelectCores += parentCore;
+
+ // Collecting columns and tables
+ parentSelectAvailableColumns += selectResolver->resolveAvailableColumns(parentCore);
+ parentSelectAvailableTables += selectResolver->resolveTables(parentCore);
+
+ // Moving on, until we're on top of the syntax tree.
+ stmt = stmt->parentStatement();
+ }
+}
+
+void CompletionHelper::extractTableAliasMap()
+{
+ foreach (SelectResolver::Column column, selectAvailableColumns)
+ {
+ if (column.type != SelectResolver::Column::COLUMN)
+ continue;
+
+ if (!column.tableAlias.isNull() && !tableToAlias[column.table].contains(column.tableAlias))
+ {
+ tableToAlias[column.table] += column.tableAlias;
+ aliasToTable[column.tableAlias] = Table(column.database, column.table);
+ }
+ }
+
+ // We have sorted list of available columns from parent selects.
+ // Given the above, we can extract table aliases in an order from deepest
+ // to shallowest, skipping any duplicates, becase the deeper alias is mentioned,
+ // the higher is its priority.
+ foreach (SelectResolver::Column column, parentSelectAvailableColumns)
+ {
+ if (column.type != SelectResolver::Column::COLUMN)
+ continue;
+
+ if (tableToAlias.contains(column.table))
+ continue;
+
+ if (!column.tableAlias.isNull() && !tableToAlias[column.table].contains(column.tableAlias))
+ {
+ tableToAlias[column.table] += column.tableAlias;
+ aliasToTable[column.tableAlias] = Table(column.database, column.table);
+ }
+ }
+}
+
+void CompletionHelper::extractCreateTableColumns()
+{
+ if (!parsedQuery)
+ return;
+
+ SqliteCreateTablePtr createTable = parsedQuery.dynamicCast<SqliteCreateTable>();
+ foreach (SqliteCreateTable::Column* col, createTable->columns)
+ favoredColumnNames << col->name;
+}
+
+QList<ExpectedTokenPtr> CompletionHelper::Results::filtered()
+{
+ QList<ExpectedTokenPtr> tokens = expectedTokens;
+ applyFilter(tokens, partialToken);
+ return tokens;
+}