#include "SqlCompletionProvider.h" #include "SqlFormattingUtils.h" #include "catalog/PgDatabaseCatalog.h" #include "catalog/PgAttributeContainer.h" #include "catalog/PgClassContainer.h" #include "catalog/PgNamespaceContainer.h" #include "catalog/PgProcContainer.h" #include "catalog/PgConstraintContainer.h" #include #include #include namespace { bool matchesPrefix(const QString &name, const SqlContext &ctx) { return name.startsWith(ctx.prefix, ctx.prefixWasQuoted ? Qt::CaseSensitive : Qt::CaseInsensitive); } bool isQueryable(char kind) { return kind == 'r' || kind == 'v' || kind == 'm' || kind == 'f' || kind == 'p'; } QString kindDescription(char kind) { switch (kind) { case 'v': return QStringLiteral("view"); case 'm': return QStringLiteral("materialized view"); case 'f': return QStringLiteral("foreign table"); default: return QStringLiteral("table"); } } // The name used to refer to a relation in the query QString referenceName(const RelationRef &ref) { return ref.alias.isEmpty() ? ref.name : ref.alias; } } // namespace CompletionCatalog buildCompletionCatalog(const PgDatabaseCatalog &catalog) { CompletionCatalog cc; auto namespaces = catalog.namespaces(); auto classes = catalog.classes(); auto attributes = catalog.attributes(); if (namespaces) { for (const auto &ns : *namespaces) { const QString name = ns.objectName(); if (name == QLatin1String("pg_toast") || name.startsWith(QLatin1String("pg_temp")) || name.startsWith(QLatin1String("pg_toast_temp"))) continue; cc.schemas.push_back(name); } } if (classes) { for (const auto &cls : *classes) { char kind = '\0'; switch (cls.kind) { case RelKind::Table: kind = 'r'; break; case RelKind::View: kind = 'v'; break; case RelKind::MaterializedView: kind = 'm'; break; case RelKind::ForeignTable: kind = 'f'; break; case RelKind::PartitionedTable: kind = 'p'; break; default: break; } if (kind == '\0') continue; CompletionRelation rel; rel.schema = cls.nsName(); rel.name = cls.objectName(); rel.kind = kind; if (attributes) { for (const auto &att : attributes->getColumnsForRelation(cls.oid())) if (att.num > 0 && !att.isdropped) rel.columns.push_back(att.name); } cc.relations.push_back(std::move(rel)); } } auto procs = catalog.procs(); if (procs) { for (const auto &proc : *procs) { char kind = 'f'; if (proc.isAggregate()) kind = 'a'; else if (proc.isWindow()) kind = 'w'; else if (!proc.isFunction()) continue; cc.functions.push_back({ proc.nsName(), proc.objectName(), kind }); } } auto constraints = catalog.constraints(); if (constraints && classes && attributes) { auto columnNames = [&](Oid relid, const auto &keys) { std::vector names; for (auto num : keys) { const PgAttribute *att = attributes->getByKey(PgAttribute::Key(relid, num)); names.push_back(att ? att->name : QString()); } return names; }; for (const auto &con : *constraints) { if (con.type != ConstraintType::ForeignKey || con.key.size() != con.fkey.size()) continue; const PgClass *child = classes->getByKey(con.relid); const PgClass *parent = classes->getByKey(con.frelid); if (!child || !parent) continue; CompletionForeignKey fk; fk.childSchema = child->nsName(); fk.childTable = child->objectName(); fk.parentSchema = parent->nsName(); fk.parentTable = parent->objectName(); fk.childColumns = columnNames(con.relid, con.key); fk.parentColumns = columnNames(con.frelid, con.fkey); if (std::find(fk.childColumns.begin(), fk.childColumns.end(), QString()) != fk.childColumns.end() || std::find(fk.parentColumns.begin(), fk.parentColumns.end(), QString()) != fk.parentColumns.end()) continue; cc.foreignKeys.push_back(std::move(fk)); } } return cc; } SqlCompletionProvider::SqlCompletionProvider(CompletionCatalog catalog) : m_catalog(std::move(catalog)) { } const CompletionRelation* SqlCompletionProvider::resolve(const RelationRef &ref) const { if (ref.derived) return nullptr; if (!ref.schema.isEmpty()) { for (const auto &r : m_catalog.relations) if (r.name == ref.name && r.schema == ref.schema) return &r; return nullptr; } for (const auto &schema : m_catalog.searchPath) for (const auto &r : m_catalog.relations) if (r.name == ref.name && r.schema == schema) return &r; for (const auto &r : m_catalog.relations) if (r.name == ref.name) return &r; return nullptr; } std::vector SqlCompletionProvider::complete(const SqlContext &ctx) const { switch (ctx.kind) { case CompletionKind::Relation: return relationItems(ctx, ctx.qualifier); case CompletionKind::JoinTarget: { auto items = joinItems(ctx); auto rest = relationItems(ctx, QString()); items.insert(items.end(), std::make_move_iterator(rest.begin()), std::make_move_iterator(rest.end())); return items; } case CompletionKind::Column: return columnItems(ctx); case CompletionKind::QualifiedColumn: return qualifiedColumnItems(ctx); default: return {}; } } std::vector SqlCompletionProvider::relationItems(const SqlContext &ctx, const QString &schema) const { std::vector items; auto addRelation = [&](const CompletionRelation &r, bool qualified) { if (!isQueryable(r.kind) || !matchesPrefix(r.name, ctx)) return; CompletionItem item; item.display = r.name; item.detail = qualified ? kindDescription(r.kind) : r.schema + QStringLiteral(" ") + kindDescription(r.kind); item.insertText = quoteIdent(r.name); item.kind = CompletionKind::Relation; items.push_back(std::move(item)); }; if (!schema.isEmpty()) { for (const auto &r : m_catalog.relations) if (r.schema == schema) addRelation(r, true); } else { for (const auto &s : m_catalog.schemas) { if (!matchesPrefix(s, ctx)) continue; CompletionItem item; item.display = s; item.detail = QStringLiteral("schema"); item.insertText = quoteIdent(s); item.kind = CompletionKind::Schema; items.push_back(std::move(item)); } for (const auto &sp : m_catalog.searchPath) for (const auto &r : m_catalog.relations) if (r.schema == sp) addRelation(r, false); } auto byName = [](const CompletionItem &a, const CompletionItem &b) { if (a.kind != b.kind) return a.kind == CompletionKind::Schema; // schemas first return QString::compare(a.display, b.display, Qt::CaseInsensitive) < 0; }; std::stable_sort(items.begin(), items.end(), byName); return items; } std::vector SqlCompletionProvider::columnItems(const SqlContext &ctx) const { struct Source { const RelationRef *ref; const CompletionRelation *rel; }; std::vector sources; for (const auto &ref : ctx.relations) { const CompletionRelation *rel = resolve(ref); if (rel) sources.push_back({ &ref, rel }); } std::map counts; // number of in scope relations having this column for (const auto &s : sources) for (const auto &c : s.rel->columns) ++counts[c]; std::vector items; for (const auto &s : sources) { const QString refName = referenceName(*s.ref); for (const auto &c : s.rel->columns) { if (!matchesPrefix(c, ctx)) continue; CompletionItem item; item.display = c; item.detail = refName; item.ambiguous = counts[c] > 1; item.insertText = item.ambiguous ? quoteIdent(refName) + QLatin1Char('.') + quoteIdent(c) : quoteIdent(c); item.kind = CompletionKind::Column; items.push_back(std::move(item)); } } addFunctionItems(ctx, items); // names usable as qualifier std::vector seen; for (const auto &ref : ctx.relations) { const QString refName = referenceName(ref); if (!matchesPrefix(refName, ctx) || std::find(seen.begin(), seen.end(), refName) != seen.end()) continue; seen.push_back(refName); CompletionItem item; item.display = refName; item.detail = ref.alias.isEmpty() ? QStringLiteral("relation") : QStringLiteral("alias of ") + ref.name; item.insertText = quoteIdent(refName); item.kind = CompletionKind::Relation; items.push_back(std::move(item)); } return items; } namespace { QString initialsOf(const QString &name) { QString result; bool startOfWord = true; for (QChar c : name) { if (c.isLetterOrNumber()) { if (startOfWord) result += c.toLower(); startOfWord = false; } else startOfWord = true; } return result.isEmpty() ? QStringLiteral("t") : result; } QString uniqueAlias(const QString &tableName, const std::set &taken) { const QString base = initialsOf(tableName); QString candidate = base; int n = 2; while (taken.count(candidate.toLower()) || identNeedsQuotes(candidate)) candidate = base + QString::number(n++); return candidate; } } // namespace std::vector SqlCompletionProvider::joinItems(const SqlContext &ctx) const { std::vector items; std::set taken; for (const auto &ref : ctx.relations) { taken.insert(ref.name.toLower()); if (!ref.alias.isEmpty()) taken.insert(ref.alias.toLower()); } auto isSearchPath = [&](const QString &schema) { return std::find(m_catalog.searchPath.begin(), m_catalog.searchPath.end(), schema) != m_catalog.searchPath.end(); }; for (const auto &ref : ctx.relations) { const CompletionRelation *rel = resolve(ref); if (!rel) continue; const QString refName = quoteIdent(referenceName(ref)); for (const auto &fk : m_catalog.foreignKeys) { // both directions, a self referencing key gives both for (int dir = 0; dir < 2; ++dir) { const bool fromChild = dir == 0; const QString &fromSchema = fromChild ? fk.childSchema : fk.parentSchema; const QString &fromTable = fromChild ? fk.childTable : fk.parentTable; if (fromTable != rel->name || fromSchema != rel->schema) continue; const QString &toSchema = fromChild ? fk.parentSchema : fk.childSchema; const QString &toTable = fromChild ? fk.parentTable : fk.childTable; const auto &fromCols = fromChild ? fk.childColumns : fk.parentColumns; const auto &toCols = fromChild ? fk.parentColumns : fk.childColumns; if (!matchesPrefix(toTable, ctx)) continue; const QString alias = uniqueAlias(toTable, taken); QString on; for (size_t i = 0; i < fromCols.size(); ++i) { if (i > 0) on += QStringLiteral(" AND "); on += refName + QLatin1Char('.') + quoteIdent(fromCols[i]) + QStringLiteral(" = ") + alias + QLatin1Char('.') + quoteIdent(toCols[i]); } QString target = quoteIdent(toTable); if (!isSearchPath(toSchema)) target = quoteIdent(toSchema) + QLatin1Char('.') + target; CompletionItem item; item.display = toTable + QLatin1Char(' ') + alias; item.detail = QStringLiteral("ON ") + on; item.insertText = target + QLatin1Char(' ') + alias + QStringLiteral(" ON ") + on; item.kind = CompletionKind::JoinTarget; const bool duplicate = std::any_of(items.begin(), items.end(), [&](const CompletionItem &i) { return i.insertText == item.insertText; }); if (!duplicate) items.push_back(std::move(item)); } } } return items; } void SqlCompletionProvider::addFunctionItems(const SqlContext &ctx, std::vector &items) const { // the list of functions is long, wait until something has been typed if (ctx.prefix.isEmpty() || ctx.clause == SqlClause::InsertInto) return; std::set seen; for (const auto &f : m_catalog.functions) { if (std::find(m_catalog.searchPath.begin(), m_catalog.searchPath.end(), f.schema) == m_catalog.searchPath.end()) continue; if (!matchesPrefix(f.name, ctx) || !seen.insert(f.name).second) continue; CompletionItem item; item.display = f.name; item.detail = f.kind == 'a' ? QStringLiteral("aggregate") : f.kind == 'w' ? QStringLiteral("window function") : QStringLiteral("function"); item.insertText = quoteIdent(f.name) + QLatin1Char('('); item.kind = CompletionKind::Function; items.push_back(std::move(item)); } } std::vector SqlCompletionProvider::qualifiedColumnItems(const SqlContext &ctx) const { std::vector items; bool found = false; for (const auto &ref : ctx.relations) { bool match = false; if (!ref.alias.isEmpty()) match = ref.alias == ctx.qualifier; else match = ref.name == ctx.qualifier && (ctx.qualifierSchema.isEmpty() || ref.schema == ctx.qualifierSchema); if (!match) continue; found = true; const CompletionRelation *rel = resolve(ref); if (!rel) continue; for (const auto &c : rel->columns) { if (!matchesPrefix(c, ctx)) continue; CompletionItem item; item.display = c; item.detail = rel->name; item.insertText = quoteIdent(c); item.kind = CompletionKind::Column; items.push_back(std::move(item)); } } if (!found) { // maybe the qualifier is a schema, or a table that is not mentioned in the FROM clause if (!ctx.qualifierSchema.isEmpty()) { for (const auto &r : m_catalog.relations) { if (r.schema != ctx.qualifierSchema || r.name != ctx.qualifier) continue; for (const auto &c : r.columns) { if (!matchesPrefix(c, ctx)) continue; CompletionItem item; item.display = c; item.detail = r.name; item.insertText = quoteIdent(c); item.kind = CompletionKind::Column; items.push_back(std::move(item)); } } } else return relationItems(ctx, ctx.qualifier); } return items; }