pgLab/pglablib/SqlCompletionProvider.cpp

431 lines
13 KiB
C++
Raw Normal View History

#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 <set>
#include <algorithm>
#include <map>
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<QString> 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<CompletionItem> 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<CompletionItem> SqlCompletionProvider::relationItems(const SqlContext &ctx, const QString &schema) const
{
std::vector<CompletionItem> 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<CompletionItem> SqlCompletionProvider::columnItems(const SqlContext &ctx) const
{
struct Source {
const RelationRef *ref;
const CompletionRelation *rel;
};
std::vector<Source> sources;
for (const auto &ref : ctx.relations) {
const CompletionRelation *rel = resolve(ref);
if (rel)
sources.push_back({ &ref, rel });
}
std::map<QString, int> 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<CompletionItem> 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<QString> 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<QString> &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<CompletionItem> SqlCompletionProvider::joinItems(const SqlContext &ctx) const
{
std::vector<CompletionItem> items;
std::set<QString> 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<CompletionItem> &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<QString> 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<CompletionItem> SqlCompletionProvider::qualifiedColumnItems(const SqlContext &ctx) const
{
std::vector<CompletionItem> 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;
}