The editor now has context aware completions
This commit is contained in:
parent
1a1bbd0838
commit
960967c8a6
18 changed files with 1984 additions and 0 deletions
|
|
@ -1,11 +1,13 @@
|
|||
add_library(core STATIC
|
||||
my_boost_assert_handler.cpp
|
||||
SqlLexer.cpp
|
||||
SqlContext.cpp
|
||||
CsvWriter.cpp
|
||||
BackupFormatModel.cpp
|
||||
ExplainTreeModelItem.cpp
|
||||
jsoncpp.cpp
|
||||
SqlLexer.h
|
||||
SqlContext.h
|
||||
ScopeGuard.h
|
||||
CsvWriter.h
|
||||
BackupFormatModel.h
|
||||
|
|
|
|||
538
core/SqlContext.cpp
Normal file
538
core/SqlContext.cpp
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
#include "SqlContext.h"
|
||||
#include "SqlLexer.h"
|
||||
#include <QSet>
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Tok {
|
||||
int start = 0;
|
||||
int end = 0;
|
||||
BasicTokenType type = BasicTokenType::None;
|
||||
QString text;
|
||||
QString upper; // only filled for Symbol tokens
|
||||
|
||||
bool isIdent() const
|
||||
{
|
||||
return type == BasicTokenType::QuotedIdentifier
|
||||
|| (type == BasicTokenType::Symbol && !text.isEmpty() && !text[0].isDigit());
|
||||
}
|
||||
bool isSelf(QChar c) const { return type == BasicTokenType::Self && text.size() == 1 && text[0] == c; }
|
||||
bool isKeyword(const char *kw) const { return type == BasicTokenType::Symbol && upper == QLatin1String(kw); }
|
||||
};
|
||||
|
||||
const QSet<QString> &reservedWords()
|
||||
{
|
||||
static const QSet<QString> words = {
|
||||
"SELECT", "FROM", "WHERE", "GROUP", "ORDER", "HAVING", "LIMIT", "OFFSET", "FETCH", "FOR",
|
||||
"UNION", "INTERSECT", "EXCEPT", "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "CROSS", "NATURAL",
|
||||
"OUTER", "ON", "USING", "SET", "RETURNING", "VALUES", "WINDOW", "AS", "LATERAL", "WITH",
|
||||
"INSERT", "INTO", "UPDATE", "DELETE", "ONLY", "TABLESAMPLE"
|
||||
};
|
||||
return words;
|
||||
}
|
||||
|
||||
bool isReserved(const Tok &t)
|
||||
{
|
||||
return t.type == BasicTokenType::Symbol && reservedWords().contains(t.upper);
|
||||
}
|
||||
|
||||
bool isSetOperator(const Tok &t)
|
||||
{
|
||||
return t.isKeyword("UNION") || t.isKeyword("INTERSECT") || t.isKeyword("EXCEPT");
|
||||
}
|
||||
|
||||
QString unquoteIdent(const QString &text, bool "ed)
|
||||
{
|
||||
quoted = false;
|
||||
if (text.startsWith('"')) {
|
||||
quoted = true;
|
||||
QString s = text.mid(1);
|
||||
if (s.endsWith('"'))
|
||||
s.chop(1);
|
||||
s.replace(QStringLiteral("\"\""), QStringLiteral("\""));
|
||||
return s;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Name as used for matching against the catalog
|
||||
QString normalizedIdent(const Tok &t, bool *quotedOut = nullptr)
|
||||
{
|
||||
bool quoted;
|
||||
QString s = unquoteIdent(t.text, quoted);
|
||||
if (quotedOut)
|
||||
*quotedOut = quoted;
|
||||
return quoted ? s : s.toLower();
|
||||
}
|
||||
|
||||
std::vector<Tok> tokenize(const QString &text)
|
||||
{
|
||||
std::vector<Tok> toks;
|
||||
SqlLexer lexer(text, LexerState::Null);
|
||||
for (;;) {
|
||||
SqlToken t = lexer.nextBasicToken();
|
||||
if (t.tokenType == BasicTokenType::End)
|
||||
break;
|
||||
if (t.length <= 0 && t.tokenType == BasicTokenType::None)
|
||||
break;
|
||||
if (t.tokenType == BasicTokenType::WhiteSpace || t.tokenType == BasicTokenType::NewLine)
|
||||
continue;
|
||||
Tok tok;
|
||||
tok.start = t.startPos;
|
||||
tok.end = t.startPos + t.length;
|
||||
tok.type = t.tokenType;
|
||||
tok.text = text.mid(t.startPos, t.length);
|
||||
if (tok.type == BasicTokenType::Symbol)
|
||||
tok.upper = tok.text.toUpper();
|
||||
toks.push_back(std::move(tok));
|
||||
}
|
||||
return toks;
|
||||
}
|
||||
|
||||
bool isUnterminatedQuote(const QString &s)
|
||||
{
|
||||
return s.startsWith('"') && (s.count('"') % 2 == 1);
|
||||
}
|
||||
|
||||
bool isRelationClause(SqlClause c)
|
||||
{
|
||||
return c == SqlClause::From || c == SqlClause::Join || c == SqlClause::Update || c == SqlClause::InsertInto;
|
||||
}
|
||||
|
||||
// Relations of a parent query are visible in sub queries inside these clauses
|
||||
bool parentVisible(SqlClause c)
|
||||
{
|
||||
switch (c) {
|
||||
case SqlClause::Select:
|
||||
case SqlClause::Where:
|
||||
case SqlClause::On:
|
||||
case SqlClause::GroupBy:
|
||||
case SqlClause::OrderBy:
|
||||
case SqlClause::Having:
|
||||
case SqlClause::Set:
|
||||
case SqlClause::Returning:
|
||||
case SqlClause::Values:
|
||||
case SqlClause::InsertInto:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Analyzer {
|
||||
public:
|
||||
using Toks = std::vector<Tok>;
|
||||
|
||||
explicit Analyzer(const Toks &s) : S(s) {}
|
||||
|
||||
struct Level {
|
||||
int begin = 0; // first token index inside the level
|
||||
int end = 0; // one past last token index inside the level
|
||||
int open = -1; // index of "(" or -1 for statement level
|
||||
};
|
||||
|
||||
// Direct tokens of level: nested groups are skipped, but the "(" and ")" markers are kept
|
||||
std::vector<int> directTokens(const Level &lv) const
|
||||
{
|
||||
std::vector<int> d;
|
||||
int depth = 0;
|
||||
for (int i = lv.begin; i < lv.end; ++i) {
|
||||
if (S[i].isSelf('(')) {
|
||||
if (depth == 0)
|
||||
d.push_back(i);
|
||||
++depth;
|
||||
}
|
||||
else if (S[i].isSelf(')')) {
|
||||
if (depth > 0)
|
||||
--depth;
|
||||
if (depth == 0)
|
||||
d.push_back(i);
|
||||
}
|
||||
else if (depth == 0)
|
||||
d.push_back(i);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
struct Segment {
|
||||
std::vector<int> d; // direct tokens of the segment
|
||||
int cursor = 0; // position in d of first token at or after the cursor
|
||||
};
|
||||
|
||||
// The part of the level between set operators that contains token index 'cursorIdx'
|
||||
Segment segment(const Level &lv, int cursorIdx) const
|
||||
{
|
||||
std::vector<int> d = directTokens(lv);
|
||||
size_t dc = 0;
|
||||
while (dc < d.size() && d[dc] < cursorIdx)
|
||||
++dc;
|
||||
size_t b = 0;
|
||||
for (size_t i = 0; i < dc; ++i)
|
||||
if (isSetOperator(S[d[i]]))
|
||||
b = i + 1;
|
||||
size_t e = d.size();
|
||||
for (size_t i = dc; i < d.size(); ++i)
|
||||
if (isSetOperator(S[d[i]])) {
|
||||
e = i;
|
||||
break;
|
||||
}
|
||||
Segment seg;
|
||||
seg.d.assign(d.begin() + b, d.begin() + e);
|
||||
seg.cursor = static_cast<int>(dc - b);
|
||||
return seg;
|
||||
}
|
||||
|
||||
SqlClause clauseAt(const Segment &seg) const
|
||||
{
|
||||
SqlClause clause = SqlClause::None;
|
||||
for (int i = 0; i < seg.cursor; ++i) {
|
||||
const Tok &t = S[seg.d[i]];
|
||||
if (t.type != BasicTokenType::Symbol)
|
||||
continue;
|
||||
const QString &k = t.upper;
|
||||
if (k == "SELECT") clause = SqlClause::Select;
|
||||
else if (k == "FROM") clause = SqlClause::From;
|
||||
else if (k == "JOIN") clause = SqlClause::Join;
|
||||
else if (k == "ON" || k == "USING") clause = SqlClause::On;
|
||||
else if (k == "WHERE") clause = SqlClause::Where;
|
||||
else if (k == "GROUP") clause = SqlClause::GroupBy;
|
||||
else if (k == "ORDER") clause = SqlClause::OrderBy;
|
||||
else if (k == "HAVING") clause = SqlClause::Having;
|
||||
else if (k == "SET") clause = SqlClause::Set;
|
||||
else if (k == "UPDATE") clause = SqlClause::Update;
|
||||
else if (k == "INTO") clause = SqlClause::InsertInto;
|
||||
else if (k == "RETURNING") clause = SqlClause::Returning;
|
||||
else if (k == "VALUES") clause = SqlClause::Values;
|
||||
else if (k == "LIMIT" || k == "OFFSET" || k == "FETCH" || k == "WINDOW" || k == "WITH")
|
||||
clause = SqlClause::None;
|
||||
}
|
||||
return clause;
|
||||
}
|
||||
|
||||
// True when the token before the cursor allows a new relation name
|
||||
bool relationExpected(const Segment &seg) const
|
||||
{
|
||||
if (seg.cursor == 0)
|
||||
return true;
|
||||
const Tok &p = S[seg.d[seg.cursor - 1]];
|
||||
return p.type == BasicTokenType::Comma
|
||||
|| p.isSelf('(')
|
||||
|| p.isKeyword("FROM") || p.isKeyword("JOIN") || p.isKeyword("UPDATE")
|
||||
|| p.isKeyword("INTO") || p.isKeyword("ONLY") || p.isKeyword("LATERAL");
|
||||
}
|
||||
|
||||
// Parse one relation item starting at position d in seg; returns the position after it
|
||||
int parseItem(const Segment &seg, int d, std::vector<RelationRef> &out) const
|
||||
{
|
||||
const int n = static_cast<int>(seg.d.size());
|
||||
auto tok = [&](int i) -> const Tok & { return S[seg.d[i]]; };
|
||||
while (d < n && (tok(d).isKeyword("ONLY") || tok(d).isKeyword("LATERAL")))
|
||||
++d;
|
||||
if (d >= n)
|
||||
return d;
|
||||
|
||||
RelationRef ref;
|
||||
if (tok(d).isSelf('(')) {
|
||||
ref.derived = true;
|
||||
++d;
|
||||
if (d < n && tok(d).isSelf(')'))
|
||||
++d;
|
||||
}
|
||||
else if (tok(d).isIdent() && !isReserved(tok(d))) {
|
||||
std::vector<const Tok *> parts{ &tok(d) };
|
||||
++d;
|
||||
while (d + 1 < n && tok(d).isSelf('.') && tok(d + 1).isIdent()) {
|
||||
parts.push_back(&tok(d + 1));
|
||||
d += 2;
|
||||
}
|
||||
ref.name = normalizedIdent(*parts.back(), &ref.quoted);
|
||||
if (parts.size() >= 2)
|
||||
ref.schema = normalizedIdent(*parts[parts.size() - 2]);
|
||||
}
|
||||
else
|
||||
return d;
|
||||
|
||||
if (d < n && tok(d).isKeyword("AS"))
|
||||
++d;
|
||||
if (d < n && tok(d).isIdent() && !isReserved(tok(d))) {
|
||||
ref.alias = normalizedIdent(tok(d));
|
||||
++d;
|
||||
}
|
||||
if (d < n && tok(d).isSelf('(') && ref.derived == false) {
|
||||
// alias column list or INSERT column list, leave to caller; skip only after an alias
|
||||
if (!ref.alias.isEmpty() && d + 1 < n && tok(d + 1).isSelf(')'))
|
||||
d += 2;
|
||||
}
|
||||
if (ref.derived) {
|
||||
if (ref.alias.isEmpty())
|
||||
return d;
|
||||
ref.name = ref.alias;
|
||||
}
|
||||
out.push_back(std::move(ref));
|
||||
return d;
|
||||
}
|
||||
|
||||
void collectRelations(const Segment &seg, std::vector<RelationRef> &out) const
|
||||
{
|
||||
const int n = static_cast<int>(seg.d.size());
|
||||
bool hasSelect = false;
|
||||
for (int i = 0; i < n; ++i)
|
||||
if (S[seg.d[i]].isKeyword("SELECT"))
|
||||
hasSelect = true;
|
||||
|
||||
bool inFrom = false;
|
||||
int d = 0;
|
||||
while (d < n) {
|
||||
const Tok &t = S[seg.d[d]];
|
||||
if (t.isKeyword("FROM") || t.isKeyword("JOIN") || t.isKeyword("UPDATE")) {
|
||||
inFrom = true;
|
||||
d = parseItem(seg, d + 1, out);
|
||||
}
|
||||
else if (t.isKeyword("INTO") && d > 0 && S[seg.d[d - 1]].isKeyword("INSERT")) {
|
||||
inFrom = true;
|
||||
if (!hasSelect)
|
||||
d = parseItem(seg, d + 1, out);
|
||||
else {
|
||||
std::vector<RelationRef> dummy;
|
||||
d = parseItem(seg, d + 1, dummy);
|
||||
}
|
||||
}
|
||||
else if (t.type == BasicTokenType::Comma && inFrom) {
|
||||
d = parseItem(seg, d + 1, out);
|
||||
}
|
||||
else {
|
||||
if (t.type == BasicTokenType::Symbol && reservedWords().contains(t.upper))
|
||||
inFrom = false;
|
||||
++d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Names of common table expressions defined by the WITH clause of the statement
|
||||
void collectCtes(std::vector<RelationRef> &out) const
|
||||
{
|
||||
Level top{ 0, static_cast<int>(S.size()), -1 };
|
||||
std::vector<int> d = directTokens(top);
|
||||
size_t i = 0;
|
||||
if (d.empty() || !S[d[0]].isKeyword("WITH"))
|
||||
return;
|
||||
i = 1;
|
||||
if (i < d.size() && S[d[i]].isKeyword("RECURSIVE"))
|
||||
++i;
|
||||
while (i < d.size()) {
|
||||
const Tok &nameTok = S[d[i]];
|
||||
if (!nameTok.isIdent() || isReserved(nameTok))
|
||||
return;
|
||||
RelationRef ref;
|
||||
ref.name = normalizedIdent(nameTok, &ref.quoted);
|
||||
ref.derived = true;
|
||||
++i;
|
||||
if (i < d.size() && S[d[i]].isSelf('(')) { // column list
|
||||
++i;
|
||||
if (i < d.size() && S[d[i]].isSelf(')'))
|
||||
++i;
|
||||
}
|
||||
out.push_back(std::move(ref));
|
||||
if (i >= d.size() || !S[d[i]].isKeyword("AS"))
|
||||
return;
|
||||
++i;
|
||||
while (i < d.size() && (S[d[i]].isKeyword("NOT") || S[d[i]].isKeyword("MATERIALIZED")))
|
||||
++i;
|
||||
if (i < d.size() && S[d[i]].isSelf('(')) {
|
||||
++i;
|
||||
if (i < d.size() && S[d[i]].isSelf(')'))
|
||||
++i;
|
||||
}
|
||||
else
|
||||
return;
|
||||
if (i < d.size() && S[d[i]].type == BasicTokenType::Comma)
|
||||
++i;
|
||||
else
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const Toks &S;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SqlContext analyzeSqlContext(const QString &text, int cursorPos)
|
||||
{
|
||||
SqlContext ctx;
|
||||
cursorPos = std::clamp(cursorPos, 0, static_cast<int>(text.size()));
|
||||
ctx.prefixStart = cursorPos;
|
||||
|
||||
// An unterminated quoted identifier before the cursor would swallow the remaining text
|
||||
QString work = text;
|
||||
{
|
||||
std::vector<Tok> head = tokenize(text.left(cursorPos));
|
||||
if (!head.empty() && head.back().end == cursorPos && isUnterminatedQuote(head.back().text))
|
||||
work.insert(cursorPos, QLatin1Char('"'));
|
||||
}
|
||||
|
||||
std::vector<Tok> T = tokenize(work);
|
||||
|
||||
// Inside comments and strings nothing is completed
|
||||
for (const Tok &t : T) {
|
||||
if (t.start < cursorPos && cursorPos <= t.end) {
|
||||
switch (t.type) {
|
||||
case BasicTokenType::Comment:
|
||||
case BasicTokenType::BlockComment:
|
||||
case BasicTokenType::OpenBlockComment:
|
||||
case BasicTokenType::QuotedString:
|
||||
case BasicTokenType::DollarQuote:
|
||||
return ctx;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (t.type == BasicTokenType::OpenBlockComment && t.start < cursorPos)
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// Prefix token
|
||||
int prefIdx = -1;
|
||||
int cut = static_cast<int>(T.size());
|
||||
for (int i = 0; i < static_cast<int>(T.size()); ++i) {
|
||||
const Tok &t = T[i];
|
||||
if (t.start < cursorPos && cursorPos <= t.end && t.isIdent()) {
|
||||
prefIdx = i;
|
||||
cut = i;
|
||||
break;
|
||||
}
|
||||
if (t.start >= cursorPos) {
|
||||
cut = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (prefIdx >= 0) {
|
||||
const Tok &t = T[prefIdx];
|
||||
QString typed = text.mid(t.start, cursorPos - t.start);
|
||||
ctx.prefixStart = t.start;
|
||||
ctx.prefixLen = cursorPos - t.start;
|
||||
ctx.prefix = unquoteIdent(typed, ctx.prefixWasQuoted);
|
||||
}
|
||||
|
||||
// Qualifier: [schema .] qualifier .
|
||||
int qualStart = cut;
|
||||
if (cut >= 2 && T[cut - 1].isSelf('.') && T[cut - 2].isIdent()) {
|
||||
ctx.qualifier = normalizedIdent(T[cut - 2]);
|
||||
qualStart = cut - 2;
|
||||
if (cut >= 4 && T[cut - 3].isSelf('.') && T[cut - 4].isIdent()) {
|
||||
ctx.qualifierSchema = normalizedIdent(T[cut - 4]);
|
||||
qualStart = cut - 4;
|
||||
}
|
||||
}
|
||||
const int removedEnd = prefIdx >= 0 ? cut + 1 : cut; // one past last removed token
|
||||
|
||||
// Statement bounds
|
||||
int sb = 0;
|
||||
for (int i = qualStart - 1; i >= 0; --i)
|
||||
if (T[i].isSelf(';')) {
|
||||
sb = i + 1;
|
||||
break;
|
||||
}
|
||||
int se = static_cast<int>(T.size());
|
||||
for (int i = removedEnd; i < static_cast<int>(T.size()); ++i)
|
||||
if (T[i].isSelf(';')) {
|
||||
se = i;
|
||||
break;
|
||||
}
|
||||
|
||||
std::vector<Tok> S;
|
||||
for (int i = sb; i < se; ++i)
|
||||
if (i < qualStart || i >= removedEnd)
|
||||
S.push_back(T[i]);
|
||||
const int cur = qualStart - sb; // index in S of first token after the cursor
|
||||
|
||||
Analyzer an(S);
|
||||
|
||||
// Enclosing paren groups
|
||||
std::vector<int> opens;
|
||||
for (int i = 0; i < cur; ++i) {
|
||||
if (S[i].isSelf('('))
|
||||
opens.push_back(i);
|
||||
else if (S[i].isSelf(')') && !opens.empty())
|
||||
opens.pop_back();
|
||||
}
|
||||
auto levelFor = [&](int open) {
|
||||
Analyzer::Level lv;
|
||||
lv.open = open;
|
||||
lv.begin = open + 1;
|
||||
lv.end = static_cast<int>(S.size());
|
||||
if (open >= 0) {
|
||||
int depth = 1;
|
||||
for (int i = open + 1; i < static_cast<int>(S.size()); ++i) {
|
||||
if (S[i].isSelf('(')) ++depth;
|
||||
else if (S[i].isSelf(')') && --depth == 0) {
|
||||
lv.end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
lv.begin = 0;
|
||||
return lv;
|
||||
};
|
||||
|
||||
// Levels from innermost to outermost
|
||||
std::vector<Analyzer::Level> levels;
|
||||
for (int i = static_cast<int>(opens.size()) - 1; i >= 0; --i)
|
||||
levels.push_back(levelFor(opens[i]));
|
||||
levels.push_back(levelFor(-1));
|
||||
|
||||
SqlClause clause = SqlClause::None;
|
||||
bool relExpected = false;
|
||||
bool clauseResolved = false;
|
||||
bool visible = true;
|
||||
for (size_t li = 0; li < levels.size(); ++li) {
|
||||
const Analyzer::Level &lv = levels[li];
|
||||
const int cursorInLevel = li == 0 ? cur : levels[li - 1].open;
|
||||
Analyzer::Segment seg = an.segment(lv, cursorInLevel);
|
||||
const SqlClause lc = an.clauseAt(seg);
|
||||
|
||||
// a level without clause keyword is a parenthesised expression and inherits the clause of its parent
|
||||
if (!clauseResolved && lc != SqlClause::None) {
|
||||
clause = lc;
|
||||
relExpected = isRelationClause(lc) && an.relationExpected(seg);
|
||||
clauseResolved = true;
|
||||
}
|
||||
if (li >= 1)
|
||||
visible = visible && parentVisible(lc);
|
||||
if (visible)
|
||||
an.collectRelations(seg, ctx.relations);
|
||||
}
|
||||
an.collectCtes(ctx.relations);
|
||||
|
||||
ctx.clause = clause;
|
||||
const bool hasQualifier = !ctx.qualifier.isEmpty();
|
||||
switch (clause) {
|
||||
case SqlClause::Select:
|
||||
case SqlClause::On:
|
||||
case SqlClause::Where:
|
||||
case SqlClause::GroupBy:
|
||||
case SqlClause::OrderBy:
|
||||
case SqlClause::Having:
|
||||
case SqlClause::Set:
|
||||
case SqlClause::Returning:
|
||||
case SqlClause::Values:
|
||||
ctx.kind = hasQualifier ? CompletionKind::QualifiedColumn : CompletionKind::Column;
|
||||
break;
|
||||
case SqlClause::From:
|
||||
case SqlClause::Join:
|
||||
case SqlClause::Update:
|
||||
case SqlClause::InsertInto:
|
||||
if (hasQualifier)
|
||||
ctx.kind = CompletionKind::Relation;
|
||||
else if (relExpected)
|
||||
ctx.kind = clause == SqlClause::Join ? CompletionKind::JoinTarget : CompletionKind::Relation;
|
||||
else if (clause == SqlClause::InsertInto && !levels.empty() && levels[0].open >= 0)
|
||||
ctx.kind = CompletionKind::Column;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
70
core/SqlContext.h
Normal file
70
core/SqlContext.h
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#ifndef SQLCONTEXT_H
|
||||
#define SQLCONTEXT_H
|
||||
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Kind of names that are expected at the cursor position.
|
||||
*/
|
||||
enum class CompletionKind {
|
||||
None,
|
||||
Relation, // schema, table or view name
|
||||
Column, // column name of the relations in scope
|
||||
QualifiedColumn, // column after "alias." or "table."
|
||||
Schema,
|
||||
Function,
|
||||
JoinTarget // complete "table alias ON ..." proposal after JOIN
|
||||
};
|
||||
|
||||
/**
|
||||
* Part of the statement the cursor is in.
|
||||
*/
|
||||
enum class SqlClause {
|
||||
None,
|
||||
Select,
|
||||
From,
|
||||
Join,
|
||||
On,
|
||||
Where,
|
||||
GroupBy,
|
||||
OrderBy,
|
||||
Having,
|
||||
Set,
|
||||
Update,
|
||||
InsertInto,
|
||||
Returning,
|
||||
Values
|
||||
};
|
||||
|
||||
/**
|
||||
* An item of a FROM clause (or UPDATE/INSERT target, CTE, derived table).
|
||||
* Names are normalised: unquoted identifiers are folded to lower case.
|
||||
*/
|
||||
struct RelationRef {
|
||||
QString schema;
|
||||
QString name; // may be a CTE name or the alias of a derived table
|
||||
QString alias;
|
||||
bool quoted = false; // name was written as quoted identifier
|
||||
bool derived = false; // CTE or sub query, columns are not in the catalog
|
||||
};
|
||||
|
||||
struct SqlContext {
|
||||
CompletionKind kind = CompletionKind::None;
|
||||
SqlClause clause = SqlClause::None;
|
||||
QString prefix; // identifier text typed so far (without opening quote)
|
||||
int prefixStart = 0; // replace range in document
|
||||
int prefixLen = 0;
|
||||
QString qualifier; // text before '.', if any
|
||||
QString qualifierSchema; // for schema.table.
|
||||
std::vector<RelationRef> relations; // relations in scope, innermost query first
|
||||
bool prefixWasQuoted = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tolerant analysis of possibly incomplete SQL around the cursor.
|
||||
* @param cursorPos position in text (between characters) where completion is requested.
|
||||
*/
|
||||
SqlContext analyzeSqlContext(const QString &text, int cursorPos);
|
||||
|
||||
#endif // SQLCONTEXT_H
|
||||
Loading…
Add table
Add a link
Reference in a new issue