diff --git a/.gitignore b/.gitignore index 8f82fb7..07d3264 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,4 @@ pglabAll.pro.user.4.8-pre1 *.user /pglabAll.pro.user* /public/ -/Testing/Temporary -.junie/ +/Testing/Temporary \ No newline at end of file diff --git a/README.md b/README.md index 1c765c8..5cd7a00 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Configure with the vcpkg toolchain file. The first configure downloads and build ``` cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake -cmake --build build --parallel +cmake --build build ``` To skip the test executables, add `-DBUILD_TESTS=OFF` to the configure command. @@ -39,7 +39,7 @@ Tested with Visual Studio 2022 (Professional) and Qt 6.8.2 (MSVC 2022, 64-bit). ``` cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%\scripts\buildsystems\vcpkg.cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.8.2/msvc2022_64 -cmake --build build --config Release --parallel +cmake --build build --config Release ctest --test-dir build -C Release --output-on-failure ``` @@ -51,23 +51,6 @@ C:\Qt\6.8.2\msvc2022_64\bin\windeployqt.exe --release build\pglab\Release\pglab. If another Qt installation (for example one shipped with another program) is on your `PATH`, its `Qt6Core.dll` may be loaded instead, which causes "entry point not found" errors at startup. Deploying the DLLs next to the executables avoids this. -### Parallel builds with Ninja on Windows - -Open an x64 Visual Studio 2022 Developer PowerShell. Ninja requires the compiler and Windows SDK environment from that shell; adding `cl.exe` to `PATH` alone can leave the linker unable to find `kernel32.lib`. - -With `VCPKG_ROOT` set, run these PowerShell commands (adjust the Qt path for your installation): - -```powershell -cmake -S . -B build/ninja-release -G Ninja -DCMAKE_BUILD_TYPE=Release "-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" -DCMAKE_PREFIX_PATH=C:/Qt/6.8.2/msvc2022_64 -cmake --build build/ninja-release --parallel 8 -$env:PATH = "C:\Qt\6.8.2\msvc2022_64\bin;$PWD\build\ninja-release\vcpkg_installed\x64-windows\bin;$env:PATH" -ctest --test-dir build/ninja-release --output-on-failure -``` - -`--parallel 8` allows eight build jobs; adjust the number to suit your machine. Ninja also builds in parallel by default. `CMAKE_BUILD_TYPE` selects Release at configure time, and the app is written to `build/ninja-release/pglab/pglab.exe`. - -Use a separate build directory when changing generators or adding a toolchain to an existing configuration. The directory above keeps the Ninja configuration separate from an existing Visual Studio build. The Qt prefix and vcpkg toolchain are both needed: Qt is installed separately, while vcpkg supplies the other dependencies. - ## Running the tests From the build directory, run: diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 782a87c..ec63cf3 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,13 +1,11 @@ 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 diff --git a/core/Expected.h b/core/Expected.h index 30ef615..a5f054b 100644 --- a/core/Expected.h +++ b/core/Expected.h @@ -63,6 +63,32 @@ public: } } +// void swap(Expected& rhs) +// { +// if (m_valid) { +// if (rhs.m_valid) { +// using std::swap; +// swap(m_value, rhs.m_value); +// } +// else { +// auto t = std::move(rhs.m_error); +// new(&rhs.m_value) T(std::move(m_value)); +// new(&m_error) std::exception_ptr(t); +// std::swap(m_valid, rhs.m_valid); +// } +// } +// else { +// if (rhs.m_valid) { +// rhs.swap(*this); +// } +// else { +// using std::swap; +// swap(m_error, rhs.m_error); +// std::swap(m_valid, rhs.m_valid); +// } +// } +// } + template static Expected fromException(const E& exception) { @@ -118,7 +144,6 @@ public: return true; } catch (...) { - // The stored exception is not of type E. It is still held in m_error. } return false; } @@ -143,7 +168,7 @@ class Expected { public: - Expected() + Expected() : m_valid(true) {} @@ -161,6 +186,26 @@ public: ~Expected() {} +// void swap(Expected& rhs) +// { +// if (m_valid) { +// if (!rhs.m_valid) { +// m_error = rhs.m_error; +// std::swap(m_valid, rhs.m_valid); +// } +// } +// else { +// if (rhs.m_valid) { +// rhs.swap(*this); +// } +// else { +// using std::swap; +// swap(m_error, rhs.m_error); +// std::swap(m_valid, rhs.m_valid); +// } +// } +// } + template static Expected fromException(const E& exception) { @@ -208,7 +253,6 @@ public: return true; } catch (...) { - // The stored exception is not of type E. It is still held in m_error. } return false; } diff --git a/core/SqlContext.cpp b/core/SqlContext.cpp deleted file mode 100644 index 5a30d03..0000000 --- a/core/SqlContext.cpp +++ /dev/null @@ -1,538 +0,0 @@ -#include "SqlContext.h" -#include "SqlLexer.h" -#include -#include - -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 &reservedWords() -{ - static const QSet 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 tokenize(const QString &text) -{ - std::vector 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; - - 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 directTokens(const Level &lv) const - { - std::vector 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 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 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(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 &out) const - { - const int n = static_cast(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 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 &out) const - { - const int n = static_cast(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 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 &out) const - { - Level top{ 0, static_cast(S.size()), -1 }; - std::vector 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(text.size())); - ctx.prefixStart = cursorPos; - - // An unterminated quoted identifier before the cursor would swallow the remaining text - QString work = text; - { - std::vector head = tokenize(text.left(cursorPos)); - if (!head.empty() && head.back().end == cursorPos && isUnterminatedQuote(head.back().text)) - work.insert(cursorPos, QLatin1Char('"')); - } - - std::vector 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(T.size()); - for (int i = 0; i < static_cast(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(T.size()); - for (int i = removedEnd; i < static_cast(T.size()); ++i) - if (T[i].isSelf(';')) { - se = i; - break; - } - - std::vector 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 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(S.size()); - if (open >= 0) { - int depth = 1; - for (int i = open + 1; i < static_cast(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 levels; - for (int i = static_cast(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; -} diff --git a/core/SqlContext.h b/core/SqlContext.h deleted file mode 100644 index 323e844..0000000 --- a/core/SqlContext.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef SQLCONTEXT_H -#define SQLCONTEXT_H - -#include -#include - -/** - * 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 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 diff --git a/docs/databasewindow/index.rst b/docs/databasewindow/index.rst index 32c9fd5..b2fb251 100644 --- a/docs/databasewindow/index.rst +++ b/docs/databasewindow/index.rst @@ -25,28 +25,6 @@ and opened with :kbd:`Ctrl+O` or :menuselection:`File --> Open query`. You can a drag and drop a bunch of files onto the window and they will be opened in seperate query tabs. -Completion -========== - -The query editor proposes names while you type, or when you press :kbd:`Ctrl+Space`. -After ``FROM``, ``JOIN`` and ``UPDATE`` schemas and tables are proposed, -and after ``schema.`` the relations of that schema. In other places the columns of the -tables in the query are proposed, also when the ``FROM`` clause comes after the cursor. -Type ``alias.`` or ``table.`` to only see the columns of that relation. -Use :kbd:`Up`/:kbd:`Down` to select, :kbd:`Enter` or :kbd:`Tab` to accept and :kbd:`Esc` to close the list. - -Names that need quotes, for instance reserved words or names with capitals, are inserted -quoted. When a column name occurs in more than one table of the query, every -occurrence is listed with its table or alias in bold and accepting it inserts the -name qualified, for example ``o.id``. - -After ``JOIN`` the list starts with complete join clauses such as -``customers c ON o.customer_id = c.id``, deduced from the foreign keys of the -tables already in the query. Both directions and multi-column keys are supported -and the generated alias never clashes with a name already in use. - -Functions are proposed once you have typed the start of their name. - Explain ======= diff --git a/pglab/BackupDialog.cpp b/pglab/BackupDialog.cpp index 57c92ec..30e5ee8 100644 --- a/pglab/BackupDialog.cpp +++ b/pglab/BackupDialog.cpp @@ -2,7 +2,6 @@ #include "BackupFormatModel.h" #include "PgDumpOutputHighlighter.h" -#include "UserConfiguration.h" #include #include #include @@ -52,7 +51,10 @@ QPlainTextEdit* createStdOutput(QWidget *parent) brush3.setStyle(Qt::SolidPattern); palette.setBrush(QPalette::Disabled, QPalette::Base, brush3); stdOutput->setPalette(palette); - stdOutput->setFont(UserConfiguration::instance()->codeFont()); + QFont font; + font.setFamily(QString::fromUtf8("Source Code Pro")); + font.setPointSize(10); + stdOutput->setFont(font); stdOutput->setLineWrapMode(QPlainTextEdit::NoWrap); stdOutput->setReadOnly(true); @@ -437,7 +439,7 @@ void BackupDialog::on_btnStart_clicked() QSettings user_settings; // Default constructor is for UserScope QVariant var = user_settings.value("pg_tools_path", {}); QDir dir; - if (var.typeId() == QMetaType::QString) { + if (var.type() == QMetaType::QString) { dir.setPath(var.toString()); } if (!dir.exists() || !dir.exists("pg_dump.exe")) { diff --git a/pglab/CMakeLists.txt b/pglab/CMakeLists.txt index 10362a1..bf2f110 100644 --- a/pglab/CMakeLists.txt +++ b/pglab/CMakeLists.txt @@ -1,4 +1,5 @@ set(PGLAB_SOURCES + main.cpp About.cpp catalog/delegates/IconColumnDelegate.cpp catalog/models/BaseTableModel.cpp @@ -25,19 +26,14 @@ set(PGLAB_SOURCES catalog/widgets/DependantsPage.cpp catalog/widgets/TriggerPage.cpp codeeditor/CodeEditor.cpp - codeeditor/CompletionPopup.cpp codeeditor/EditorGutter.cpp codeeditor/GutterPainter.cpp crud/CrudModel.cpp crud/CrudTab.cpp crud/DataGridWidget.cpp - crud/RecordPanel.cpp querytool/QueryExplainModel.cpp querytool/QueryParamListController.cpp querytool/QueryTool.cpp - querytool/SqlDocument.cpp - querytool/SqlDocumentRegistry.cpp - querytool/UnsavedDocumentsDialog.cpp serverinspector/DatabasesPage.cpp serverinspector/RolesPage.cpp serverinspector/ServerInspector.cpp @@ -72,6 +68,8 @@ set(PGLAB_SOURCES EditTableWidget.cpp EditColumnTableModel.cpp DatabaseWindow.cpp + widgets/SingleRecordModel.cpp + widgets/SingleRecordWidget.cpp ) set(PGLAB_FORMS @@ -83,18 +81,21 @@ set(PGLAB_FORMS ProcessStdioWidget.ui crud/CrudTab.ui CodeGenerator.ui + widgets/SingleRecordWidget.ui ) -add_library(pglabapp STATIC +add_executable(pglab ${PGLAB_SOURCES} ${PGLAB_FORMS} + resources.qrc ) if(WIN32) - target_link_libraries(pglabapp PUBLIC ws2_32) + set_target_properties(pglab PROPERTIES WIN32_EXECUTABLE ON) + target_link_libraries(pglab PRIVATE ws2_32) endif() -target_link_libraries(pglabapp PUBLIC +target_link_libraries(pglab PRIVATE pglablib pgsql core @@ -105,27 +106,10 @@ target_link_libraries(pglabapp PUBLIC Qt6::Concurrent ) -target_include_directories(pglabapp PUBLIC +target_include_directories(pglab PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/crud ) -set_target_properties(pglabapp PROPERTIES - CXX_STANDARD 20 - CXX_STANDARD_REQUIRED ON - ) - -add_executable(pglab - main.cpp - resources.qrc - ) - -if(WIN32) - set_target_properties(pglab PROPERTIES WIN32_EXECUTABLE ON) -endif() - -target_link_libraries(pglab PRIVATE pglabapp) - set_target_properties(pglab PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON diff --git a/pglab/ConnectionConfigurationWidget.cpp b/pglab/ConnectionConfigurationWidget.cpp index 6434cd8..52f0e2b 100644 --- a/pglab/ConnectionConfigurationWidget.cpp +++ b/pglab/ConnectionConfigurationWidget.cpp @@ -117,9 +117,10 @@ ConnectionConfigurationWidget::ConnectionConfigurationWidget( SET_OBJECT_NAME(lblSsl); cmbbxSsl = new QComboBox; SET_OBJECT_NAME(cmbbxSsl); - cmbbxSsl->setModelColumn(SslModeModel::Name); + cmbbxSsl->setModelColumn(0); auto ssl_model = new SslModeModel(this); cmbbxSsl->setModel(ssl_model); + cmbbxSsl->setEditable(true); lblSsl->setBuddy(cmbbxSsl); lblCert = new QLabel; @@ -206,13 +207,7 @@ void ConnectionConfigurationWidget::setData(const ConnectionConfig &cfg) edtPassword->setText(""); cmbDbname->setCurrentText(cfg.dbname()); - auto sslMode = cfg.getParameter("sslmode"); - if (sslMode == "verify_ca") - sslMode = "verify-ca"; - else if (sslMode == "verify_full") - sslMode = "verify-full"; - auto sslIndex = cmbbxSsl->findData(sslMode); - cmbbxSsl->setCurrentIndex(sslIndex < 0 ? 0 : sslIndex); + cmbbxSsl->setCurrentText(cfg.getParameter("sslmode")); edtCert->setText(cfg.sslCert()); edtKey->setText(cfg.sslKey()); edtRootCert->setText(cfg.sslRootCert()); @@ -239,7 +234,7 @@ ConnectionConfig ConnectionConfigurationWidget::data() const cfg.setUser(edtUser->text()); cfg.setPassword(edtPassword->text()); cfg.setDbname(cmbDbname->currentText()); - cfg.setParameter("sslmode", cmbbxSsl->currentData().toString()); + cfg.setParameter("sslmode", cmbbxSsl->currentText()); cfg.setSslCert(edtCert->text()); cfg.setSslKey(edtKey->text()); cfg.setSslRootCert(edtRootCert->text()); diff --git a/pglab/ConnectionManagerWindow.cpp b/pglab/ConnectionManagerWindow.cpp index ddc0702..68a1a49 100644 --- a/pglab/ConnectionManagerWindow.cpp +++ b/pglab/ConnectionManagerWindow.cpp @@ -62,7 +62,7 @@ void ConnectionManagerWindow::on_actionQuit_application_triggered() { auto res = QMessageBox::question(this, "pglab", tr("Close all windows?"), QMessageBox::Yes, QMessageBox::No); - if (res == QMessageBox::Yes && m_masterController->confirmQuit(this)) { + if (res == QMessageBox::Yes) { QApplication::quit(); } } diff --git a/pglab/DatabaseWindow.cpp b/pglab/DatabaseWindow.cpp index 66bbacd..9d1fee8 100644 --- a/pglab/DatabaseWindow.cpp +++ b/pglab/DatabaseWindow.cpp @@ -20,13 +20,10 @@ #include #include #include -#include -#include #include "EditTableWidget.h" #include "CodeGenerator.h" #include "querytool/QueryTool.h" -#include "querytool/UnsavedDocumentsDialog.h" #include namespace pg = Pgsql; @@ -94,18 +91,12 @@ void DatabaseWindow::closeEvent(QCloseEvent *event) { for (int idx = 0; idx < ui->mainTabs->count(); ++idx) { - auto mp = dynamic_cast(ui->mainTabs->widget(idx)); - if (mp && !dynamic_cast(mp) && !mp->CanClose(true)) + if (!canCloseTab(idx)) { event->ignore(); return; } - } - - auto docs = documentsClosedWith(m_masterController->sqlDocumentRegistry().modifiedDocuments(), - [this](const QWidget *editor) { return editor->window() == this; }); - if (!confirmClose(this, docs)) - event->ignore(); + } } void DatabaseWindow::setConfig(const ConnectionConfig &config) @@ -208,14 +199,14 @@ void DatabaseWindow::openSqlFile(QString file_name) { if (!file_name.isEmpty()) { - auto document = std::make_shared(); - if (document->load(file_name)) + auto *ct = new QueryTool(this, this); + if (ct->load(file_name)) { - m_masterController->sqlDocumentRegistry().add(document); - auto *ct = new QueryTool(this, document, this); ct->addAction(ui->actionExecute_query); addPage(ct, ct->title()); } + else + delete ct; } } @@ -400,53 +391,13 @@ void DatabaseWindow::on_actionInspect_server_triggered() newServerInspectorPage(); } -void DatabaseWindow::openSharedSqlDocument(std::shared_ptr document) -{ - auto *ct = new QueryTool(this, std::move(document), this); - ct->addAction(ui->actionExecute_query); - addPage(ct, ct->title()); - ct->updateTitle(); -} - void DatabaseWindow::on_actionNew_Query_triggered() { - auto document = std::make_shared(); - m_masterController->sqlDocumentRegistry().add(document); - auto *ct = new QueryTool(this, document, this); + auto *ct = new QueryTool(this, this); ct->addAction(ui->actionExecute_query); addPage(ct, "new"); -} + ct->newdoc(); -void DatabaseWindow::on_actionOpen_in_other_window_triggered() -{ - auto ct = dynamic_cast(ui->mainTabs->currentWidget()); - if (!ct) - return; - - QList others; - for (auto *widget : QApplication::topLevelWidgets()) { - auto dw = qobject_cast(widget); - if (dw && dw != this && dw->isVisible()) - others.append(dw); - } - - if (others.isEmpty()) { - QMessageBox::information(this, "pglab", tr("No other window is open.")); - return; - } - - QStringList names; - for (int i = 0; i < others.size(); ++i) - names.append(QString("%1 (%2)").arg(others[i]->windowTitle()).arg(i + 1)); - - bool ok = false; - QString selected = QInputDialog::getItem(this, tr("Open in other window"), - tr("Select window:"), names, 0, false, &ok); - if (!ok) - return; - - auto target = others[names.indexOf(selected)]; - target->openSharedSqlDocument(ct->document()); } void DatabaseWindow::on_actionOpen_Query_triggered() diff --git a/pglab/DatabaseWindow.h b/pglab/DatabaseWindow.h index 0512f46..b86d917 100644 --- a/pglab/DatabaseWindow.h +++ b/pglab/DatabaseWindow.h @@ -6,7 +6,6 @@ #include "OpenDatabase.h" #include "Pgsql_Connection.h" #include "IDatabaseWindow.h" -#include "querytool/SqlDocument.h" #include #include @@ -39,7 +38,6 @@ public: ~DatabaseWindow(); void setConfig(const ConnectionConfig &config); - void openSharedSqlDocument(std::shared_ptr document); std::shared_ptr getDatabase() { return m_database; } @@ -100,7 +98,6 @@ private slots: void on_actionInspect_server_triggered(); void on_actionNew_Query_triggered(); void on_actionOpen_Query_triggered(); - void on_actionOpen_in_other_window_triggered(); void on_actionSave_Query_triggered(); void on_actionPaste_lang_string_triggered(); void on_actionRefreshCatalog_triggered(); diff --git a/pglab/DatabaseWindow.ui b/pglab/DatabaseWindow.ui index 8880462..0db9810 100644 --- a/pglab/DatabaseWindow.ui +++ b/pglab/DatabaseWindow.ui @@ -43,7 +43,6 @@ - @@ -107,11 +106,6 @@ - - - Open in other window / Duplicate in window... - - diff --git a/pglab/MasterController.cpp b/pglab/MasterController.cpp index df9032f..5a25833 100644 --- a/pglab/MasterController.cpp +++ b/pglab/MasterController.cpp @@ -1,6 +1,5 @@ #include "MasterController.h" #include "ConnectionController.h" -#include "querytool/UnsavedDocumentsDialog.h" #include "utils/PostgresqlUrlParser.h" #include #include @@ -60,9 +59,4 @@ SQLiteConnection& MasterController::userConfigDatabase() return m_userConfigDatabase; } -bool MasterController::confirmQuit(QWidget *parent) -{ - return confirmClose(parent, m_sqlDocumentRegistry.modifiedDocuments()); -} - diff --git a/pglab/MasterController.h b/pglab/MasterController.h index 33709ce..eeffc78 100644 --- a/pglab/MasterController.h +++ b/pglab/MasterController.h @@ -7,7 +7,6 @@ #include #include #include "sqlite/SQLiteConnection.h" -#include "querytool/SqlDocumentRegistry.h" class ConnectionController; @@ -25,10 +24,6 @@ public: ConnectionController* connectionController(); SQLiteConnection& userConfigDatabase(); - - SqlDocumentRegistry& sqlDocumentRegistry() { return m_sqlDocumentRegistry; } - /// Asks once about all modified documents of all windows. Returns false when quitting must be aborted. - bool confirmQuit(QWidget *parent); signals: @@ -36,7 +31,6 @@ public slots: private: SQLiteConnection m_userConfigDatabase; - SqlDocumentRegistry m_sqlDocumentRegistry; ConnectionController* m_connectionController = nullptr; }; diff --git a/pglab/SslModeModel.cpp b/pglab/SslModeModel.cpp index 2ed2e7a..9694d2f 100644 --- a/pglab/SslModeModel.cpp +++ b/pglab/SslModeModel.cpp @@ -1,20 +1,12 @@ #include "SslModeModel.h" -#include - -namespace { - -const QStringList sslModes = {QString(), "disable", "allow", "prefer", "require", "verify-ca", "verify-full"}; - -} - SslModeModel::SslModeModel(QObject *parent) : QAbstractListModel(parent) {} -int SslModeModel::rowCount(const QModelIndex &parent) const +int SslModeModel::rowCount(const QModelIndex &) const { - return parent.isValid() ? 0 : static_cast(sslModes.size()); + return 6; } int SslModeModel::columnCount(const QModelIndex &) const @@ -24,43 +16,49 @@ int SslModeModel::columnCount(const QModelIndex &) const QVariant SslModeModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= rowCount() - || index.column() < 0 || index.column() >= ColCount) - return {}; - - if (index.column() == Name) { - const auto &mode = sslModes.at(index.row()); - if (role == Qt::UserRole) - return mode; - if (role == Qt::DisplayRole || role == Qt::EditRole) - return index.row() == 0 ? tr("Default") : mode; - return {}; - } - QVariant v; if (role == Qt::DisplayRole) { switch (index.column()) { + case Name: + switch(index.row()) { + case 0: + v = tr("disable"); + break; + case 1: + v = tr("allow"); + break; + case 2: + v = tr("prefer"); + break; + case 3: + v = tr("require"); + break; + case 4: + v = tr("verify_ca"); + break; + case 5: + v = tr("verify_full"); + break; + } + break; case Description: switch(index.row()) { case 0: - v = tr("use the PostgreSQL default SSL mode"); - break; - case 1: v = tr("try a non encrypted connection only"); break; - case 2: + case 1: v = tr("try no encryption first then try encrypted"); break; - case 3: + case 2: v = tr("try encrypted first then not encrypted"); break; - case 4: + case 3: v = tr("require an encrypted connection"); break; - case 5: + case 4: v = tr("verify encryption certificate has a valid signature"); break; - case 6: + case 5: v = tr("verify encryption certificate has a valid signature and matches the host"); break; } diff --git a/pglab/UserConfiguration.cpp b/pglab/UserConfiguration.cpp index 4ee0aea..cea1be2 100644 --- a/pglab/UserConfiguration.cpp +++ b/pglab/UserConfiguration.cpp @@ -1,7 +1,5 @@ #include "UserConfiguration.h" #include -#include -#include UserConfiguration* UserConfiguration::instance() { @@ -16,19 +14,12 @@ UserConfiguration::UserConfiguration() QFont UserConfiguration::codeFont() const { - QString family = m_settings.value("fonts/code/family").toString(); - QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - font.setStyleHint(QFont::Monospace); + QString family = m_settings.value("fonts/code/family", QString("Source Code Pro")).toString(); + int size = m_settings.value("fonts/code/size", 10).toInt(); + QFont font; + font.setFamily(family); font.setFixedPitch(true); - if (!family.isEmpty()) - font.setFamily(family); - else if (!QFontInfo(font).fixedPitch()) - font.setFamilies({ "Source Code Pro", "Consolas", "Courier New", "monospace" }); - - bool ok = false; - int size = m_settings.value("fonts/code/size").toInt(&ok); - if (ok && size > 0) - font.setPointSize(size); + font.setPointSize(size); return font; } diff --git a/pglab/codeeditor/CodeEditor.cpp b/pglab/codeeditor/CodeEditor.cpp index 31f648c..0107c7f 100644 --- a/pglab/codeeditor/CodeEditor.cpp +++ b/pglab/codeeditor/CodeEditor.cpp @@ -1,6 +1,5 @@ #include "CodeEditor.h" #include "EditorGutter.h" -#include #include #include #include @@ -125,125 +124,9 @@ bool CodeEditor::lineHasError(int blockNumber) const return errorLines.count(blockNumber) > 0; } -void CodeEditor::setCompletionCallback(EditorCompletionCallback callback) -{ - m_completionCallback = std::move(callback); - if (!m_completionCallback) - hideCompletions(); -} - -void CodeEditor::showCompletions() -{ - updateCompletions(true); -} - -void CodeEditor::hideCompletions() -{ - if (m_completionPopup) - m_completionPopup->hide(); -} - -bool CodeEditor::updateCompletions(bool explicitRequest) -{ - if (!m_completionCallback) { - hideCompletions(); - return false; - } - const int pos = textCursor().position(); - EditorCompletionResult result = m_completionCallback(toPlainText(), pos); - bool show = !result.items.empty(); - if (show && !explicitRequest) { - // do not bother the user while nothing identifier like has been typed - if (result.replaceLen == 0 && !result.hasQualifier) - show = false; - // the only candidate is what has been typed already - else if (result.items.size() == 1 && result.replaceLen > 0) { - QTextCursor c = textCursor(); - c.setPosition(result.replaceStart); - c.setPosition(result.replaceStart + result.replaceLen, QTextCursor::KeepAnchor); - if (c.selectedText() == result.items[0].insertText) - show = false; - } - } - if (!show) { - hideCompletions(); - return false; - } - - if (!m_completionPopup) { - m_completionPopup = new CompletionPopup(this); - connect(m_completionPopup, &CompletionPopup::itemAccepted, this, &CodeEditor::acceptCompletion); - } - m_completionResult = std::move(result); - m_completionPopup->setFont(font()); - m_completionPopup->setItems(m_completionResult.items); - - QTextCursor c = textCursor(); - c.setPosition(m_completionResult.replaceStart); - QRect r = cursorRect(c); - m_completionPopup->showAt(viewport()->mapToGlobal(r.bottomLeft())); - return true; -} - -void CodeEditor::acceptCompletion(int row) -{ - if (row < 0 || row >= static_cast(m_completionResult.items.size())) - return; - const QString text = m_completionResult.items[row].insertText; - QTextCursor c = textCursor(); - c.beginEditBlock(); - c.setPosition(m_completionResult.replaceStart); - c.setPosition(m_completionResult.replaceStart + m_completionResult.replaceLen, QTextCursor::KeepAnchor); - c.insertText(text); - c.endEditBlock(); - setTextCursor(c); - hideCompletions(); -} - -void CodeEditor::focusOutEvent(QFocusEvent *e) -{ - hideCompletions(); - QPlainTextEdit::focusOutEvent(e); -} - -void CodeEditor::mousePressEvent(QMouseEvent *e) -{ - hideCompletions(); - QPlainTextEdit::mousePressEvent(e); -} - void CodeEditor::keyPressEvent(QKeyEvent *e) { auto k = e->key(); - const bool popupVisible = m_completionPopup && m_completionPopup->isVisible(); - if (popupVisible) { - switch (k) { - case Qt::Key_Down: - m_completionPopup->moveSelection(1); - return; - case Qt::Key_Up: - m_completionPopup->moveSelection(-1); - return; - case Qt::Key_PageDown: - m_completionPopup->moveSelection(10); - return; - case Qt::Key_PageUp: - m_completionPopup->moveSelection(-10); - return; - case Qt::Key_Return: - case Qt::Key_Enter: - case Qt::Key_Tab: - acceptCompletion(m_completionPopup->currentRow()); - return; - case Qt::Key_Escape: - hideCompletions(); - return; - } - } - if (k == Qt::Key_Space && e->modifiers().testFlag(Qt::ControlModifier)) { - showCompletions(); - return; - } switch (k) { case Qt::Key_Tab: if (indentSelection(true)) @@ -272,20 +155,6 @@ void CodeEditor::keyPressEvent(QKeyEvent *e) break; } QPlainTextEdit::keyPressEvent(e); - - if (m_completionCallback) { - if (popupVisible) { - if (k == Qt::Key_Backspace || k == Qt::Key_Delete || !e->text().isEmpty()) - updateCompletions(false); - else if (k == Qt::Key_Left || k == Qt::Key_Right || k == Qt::Key_Home || k == Qt::Key_End) - hideCompletions(); - } - else if (!e->text().isEmpty() && !e->modifiers().testFlag(Qt::ControlModifier)) { - const QChar ch = e->text()[0]; - if (ch.isLetterOrNumber() || ch == QLatin1Char('_') || ch == QLatin1Char('.') || ch == QLatin1Char('"')) - updateCompletions(false); - } - } } diff --git a/pglab/codeeditor/CodeEditor.h b/pglab/codeeditor/CodeEditor.h index 83adde7..4a3303d 100644 --- a/pglab/codeeditor/CodeEditor.h +++ b/pglab/codeeditor/CodeEditor.h @@ -4,7 +4,6 @@ #include #include #include "util/Colors.h" -#include "CompletionPopup.h" /** This class adds some capabilities to QPlainTextEdit that are useful * in code editor scenarios. @@ -21,16 +20,9 @@ public: void clearErrorMarkers(); void setFont(const QFont &); void setTabSize(int chars); - - /// Install the function that supplies completions, an empty function disables completion - void setCompletionCallback(EditorCompletionCallback callback); - /// Show completions at the cursor, also when nothing has been typed yet - void showCompletions(); protected: void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *e) override; - void focusOutEvent(QFocusEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; signals: @@ -52,14 +44,6 @@ private: std::set errorLines; - EditorCompletionCallback m_completionCallback; - CompletionPopup *m_completionPopup = nullptr; - EditorCompletionResult m_completionResult; - - bool updateCompletions(bool explicitRequest); - void hideCompletions(); - void acceptCompletion(int row); - // Settings int m_tabSize = 0; // tabSize in characters bool m_useTab = false; diff --git a/pglab/codeeditor/CompletionPopup.cpp b/pglab/codeeditor/CompletionPopup.cpp deleted file mode 100644 index d796137..0000000 --- a/pglab/codeeditor/CompletionPopup.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "CompletionPopup.h" -#include -#include -#include -#include - -namespace { - -constexpr int DetailRole = Qt::UserRole + 1; -constexpr int AmbiguousRole = Qt::UserRole + 2; - -class CompletionDelegate : public QStyledItemDelegate { -public: - using QStyledItemDelegate::QStyledItemDelegate; - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override - { - QStyleOptionViewItem opt = option; - initStyleOption(&opt, index); - const QString display = opt.text; - opt.text.clear(); - const QWidget *w = opt.widget; - (w ? w->style() : QApplication::style())->drawControl(QStyle::CE_ItemViewItem, &opt, painter, w); - - const bool selected = opt.state & QStyle::State_Selected; - const QPalette &pal = opt.palette; - const QString detail = index.data(DetailRole).toString(); - const bool ambiguous = index.data(AmbiguousRole).toBool(); - - painter->save(); - painter->setPen(selected ? pal.highlightedText().color() : pal.text().color()); - QRect r = opt.rect.adjusted(4, 0, -4, 0); - painter->drawText(r, Qt::AlignVCenter | Qt::AlignLeft, display); - if (!detail.isEmpty()) { - const int x = painter->fontMetrics().horizontalAdvance(display) + 12; - QColor dim = selected ? pal.highlightedText().color() : pal.color(QPalette::Disabled, QPalette::Text); - painter->setPen(dim); - if (ambiguous) { - QFont f = painter->font(); - f.setBold(true); - painter->setFont(f); - } - painter->drawText(r.adjusted(x, 0, 0, 0), Qt::AlignVCenter | Qt::AlignLeft, QStringLiteral("\u2014 ") + detail); - } - painter->restore(); - } - - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override - { - QSize s = QStyledItemDelegate::sizeHint(option, index); - const QFontMetrics fm(option.font); - s.setWidth(fm.horizontalAdvance(index.data(Qt::DisplayRole).toString()) - + fm.horizontalAdvance(index.data(DetailRole).toString()) + 40); - return s; - } -}; - -} // namespace - -CompletionPopup::CompletionPopup(QWidget *parent) - : QListWidget(parent) -{ - setWindowFlags(Qt::ToolTip); - setAttribute(Qt::WA_ShowWithoutActivating); - setFocusPolicy(Qt::NoFocus); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setUniformItemSizes(true); - setItemDelegate(new CompletionDelegate(this)); - connect(this, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) { - emit itemAccepted(row(item)); - }); -} - -void CompletionPopup::setItems(const std::vector &items) -{ - clear(); - int width = 150; - const QFontMetrics fm(font()); - for (const auto &i : items) { - auto *item = new QListWidgetItem(i.display, this); - item->setData(DetailRole, i.detail); - item->setData(AmbiguousRole, i.ambiguous); - width = std::max(width, fm.horizontalAdvance(i.display) + fm.horizontalAdvance(i.detail) + 50); - } - if (!items.empty()) - setCurrentRow(0); - const int rows = static_cast(std::min(items.size(), 10)); - const int rowHeight = fm.height() + 6; - resize(std::min(width, 600), rows * rowHeight + 2 * frameWidth() + 2); -} - -void CompletionPopup::moveSelection(int delta) -{ - const int n = count(); - if (n == 0) - return; - int r = QListWidget::currentRow() + delta; - r = std::clamp(r, 0, n - 1); - setCurrentRow(r); -} - -void CompletionPopup::showAt(const QPoint &globalPos) -{ - QPoint p = globalPos; - if (auto *screen = QApplication::screenAt(globalPos)) { - const QRect avail = screen->availableGeometry(); - if (p.x() + width() > avail.right()) - p.setX(std::max(avail.left(), avail.right() - width())); - } - move(p); - show(); -} diff --git a/pglab/codeeditor/CompletionPopup.h b/pglab/codeeditor/CompletionPopup.h deleted file mode 100644 index 47c4768..0000000 --- a/pglab/codeeditor/CompletionPopup.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef COMPLETIONPOPUP_H -#define COMPLETIONPOPUP_H - -#include -#include -#include - -struct EditorCompletionItem { - QString display; - QString detail; - QString insertText; - bool ambiguous = false; -}; - -struct EditorCompletionResult { - int replaceStart = 0; - int replaceLen = 0; - bool hasQualifier = false; // completing behind "name." - std::vector items; -}; - -/// Callback that determines the completions for a cursor position in the text -using EditorCompletionCallback = std::function; - -/// List shown below the cursor, it never takes keyboard focus so typing continues in the editor. -class CompletionPopup : public QListWidget { - Q_OBJECT -public: - explicit CompletionPopup(QWidget *parent); - - void setItems(const std::vector &items); - void moveSelection(int delta); - int currentRow() const { return QListWidget::currentRow(); } - void showAt(const QPoint &globalPos); - -signals: - void itemAccepted(int row); -}; - -#endif // COMPLETIONPOPUP_H diff --git a/pglab/crud/CrudModel.cpp b/pglab/crud/CrudModel.cpp index 9b9b965..c30a1bc 100644 --- a/pglab/crud/CrudModel.cpp +++ b/pglab/crud/CrudModel.cpp @@ -6,6 +6,7 @@ #include "catalog/PgConstraintContainer.h" #include "catalog/PgClassContainer.h" #include "SqlFormattingUtils.h" +#include "Pgsql_oids.h" #include "CustomDataRole.h" #include #include @@ -97,46 +98,45 @@ Oid CrudModel::getType(int column) const CrudModel::Value CrudModel::getLatestData(int columnIndex, int rowIndex) const { - if (!m_roData) - return {}; - - const RowMapping &row_mapping = m_rowMapping[rowIndex]; - if (row_mapping.pending) + if (m_roData) { - // an edited cell can be NULL, that is different from not having been edited - if (auto edited = m_pendingRowList.getValue(columnIndex, row_mapping.rowKey)) - return *edited; + auto row_mapping = m_rowMapping[rowIndex]; + + std::optional val; + if (row_mapping.pending) + val = m_pendingRowList.getValue(columnIndex, row_mapping.rowKey); + + if (!val.has_value()) + val = getSavedData(row_mapping, columnIndex); + + return val.value_or(Value()); } - return getSavedData(row_mapping, columnIndex); + return {}; +} + +CrudModel::Value CrudModel::getSavedData(int columnIndex, int rowIndex) const +{ + if (m_roData) + { + auto row_mapping = m_rowMapping[rowIndex]; + return getSavedData(row_mapping, columnIndex); + } + return {}; } CrudModel::Value CrudModel::getSavedData(const RowMapping &row_mapping, int columnIndex) const { - // rows that have been saved (or are new) have their own copy of the data - if (row_mapping.isModified()) - return row_mapping.modifiedValue(columnIndex); + std::optional val; + if (!val.has_value() && row_mapping.isModified()) + val = row_mapping.modifiedValue(columnIndex); - if (row_mapping.rowKey < m_roData->rows() && !m_roData->null(columnIndex, row_mapping.rowKey)) - return std::string(m_roData->val(columnIndex, row_mapping.rowKey)); + if (!val.has_value() && row_mapping.rowKey < m_roData->rows()) + if (!m_roData->null(columnIndex, row_mapping.rowKey)) + val = std::string(m_roData->val(columnIndex, row_mapping.rowKey)); - return {}; + return val.value_or(Value()); } -CrudModel::RowMapping* CrudModel::findRowMapping(int rowKey) -{ - auto iter = std::find_if(m_rowMapping.begin(), m_rowMapping.end(), - [rowKey](const RowMapping &rhs) { return rhs.rowKey == rowKey; }); - return iter != m_rowMapping.end() ? &*iter : nullptr; -} - -void CrudModel::updatePendingState(bool was_dirty) -{ - if (was_dirty != hasPendingChanges()) - emit pendingChangesChanged(hasPendingChanges()); -} - -namespace { - QVariant StringToVariant(const std::string &str, Oid oid) { Pgsql::Value v(str.c_str(), oid); @@ -174,9 +174,8 @@ QVariant StringToVariant(const std::string &str, Oid oid) case Pgsql::float8_oid: return v.operator double(); } -} -} // namespace +} QVariant CrudModel::data(const QModelIndex &index, int role) const { @@ -255,7 +254,8 @@ void CrudModel::loadIntoModel(std::shared_ptr data) if (m_wholeTable) appendNewRowInternal(); endResetModel(); - updatePendingState(was_dirty); + if (was_dirty) + emit pendingChangesChanged(false); } void CrudModel::initRowMapping() @@ -269,11 +269,26 @@ void CrudModel::initRowMapping() void CrudModel::connectionStateChanged() { - if (m_dbConn.state() == ASyncDBConnection::State::Connected && callLoadData) - { - callLoadData = false; - loadData(); - } + switch (m_dbConn.state()) { + case ASyncDBConnection::State::NotConnected: + break; + case ASyncDBConnection::State::Connecting: + break; + case ASyncDBConnection::State::Connected: + if (callLoadData) + { + callLoadData = false; + loadData(); + } + break; + case ASyncDBConnection::State::QuerySend: + + break; + case ASyncDBConnection::State::CancelSend: + break; + case ASyncDBConnection::State::Terminating: + break; + } } Qt::ItemFlags CrudModel::flags(const QModelIndex &index) const @@ -311,7 +326,8 @@ bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int rol } bool was_dirty = hasPendingChanges(); m_pendingRowList.setValue(col, row_mapping.rowKey, val); - updatePendingState(was_dirty); + if (!was_dirty) + emit pendingChangesChanged(true); emit dataChanged(index, index, QVector() << role); return true; @@ -321,7 +337,8 @@ bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int rol std::tuple> CrudModel::saveRow(const PendingRow &pending_row, const RowMapping &row_mapping) { - if (pending_row.data().empty()) + auto data = pending_row.data(); + if (data.empty()) return { true, {} }; if (row_mapping.isNew()) @@ -331,16 +348,31 @@ std::tuple> CrudModel::saveRow(const Pending std::tie(buffer, params) = createInsertQuery(pending_row); Pgsql::Connection db_update_conn; - connectForUpdate(db_update_conn); + auto dbconfig = m_database->config(); + db_update_conn.connect(dbconfig.connectionString()); try { auto result = db_update_conn.queryParam(buffer, params); if (result && result.rows() == 1) - return { true, rowValues(*result.begin()) }; + { + std::vector values; + auto row = *result.begin(); + for (auto v : row) + { + if (v.null()) + values.push_back(Value()); + else + values.push_back(std::string(v.c_str())); + } + + return { true, values }; + } } catch (const Pgsql::PgResultError &ex) { - reportError(ex.what()); + QMessageBox msgBox; + msgBox.setText(ex.what()); + msgBox.exec(); } return { false, {} }; } @@ -357,7 +389,8 @@ std::tuple> CrudModel::saveUpdate(const Pend try { Pgsql::Connection db_update_conn; - connectForUpdate(db_update_conn); + auto dbconfig = m_database->config(); + db_update_conn.connect(dbconfig.connectionString()); auto tx = Pgsql::Transaction::startTransaction(db_update_conn); for (Oid table_oid : tables) { @@ -385,47 +418,32 @@ std::tuple> CrudModel::saveUpdate(const Pend } catch (const Pgsql::PgResultError &ex) { - reportError(ex.what()); + QMessageBox msgBox; + msgBox.setText(ex.what()); + msgBox.exec(); } return { false, {} }; } -void CrudModel::connectForUpdate(Pgsql::Connection &connection) const -{ - auto dbconfig = m_database->config(); - connection.connect(dbconfig.connectionString()); -} - -void CrudModel::reportError(const QString &message) const -{ - QMessageBox msgBox; - msgBox.setText(message); - msgBox.exec(); -} - -std::vector CrudModel::rowValues(const Pgsql::Row &row) -{ - std::vector values; - for (auto v : row) - { - if (v.null()) - values.push_back(Value()); - else - values.push_back(std::string(v.c_str())); - } - return values; -} - QString CrudModel::tableName(Oid table_oid) const { auto table = m_database->catalog()->classes()->getByKey(table_oid); return table ? table->fullyQualifiedQuotedObjectName() : QString(); } +std::vector CrudModel::resultColumnsOfTable(Oid table_oid) const +{ + std::vector cols; + for (int col = 0; col < m_roData->cols(); ++col) + if (m_roData->ftable(col) == table_oid && m_roData->ftableCol(col) > 0) + cols.push_back(col); + return cols; +} + QString CrudModel::attributeName(int col) const { - if (col < static_cast(m_columnList.size()) && m_columnList[col]) - return m_columnList[col]->name; + if (col < static_cast(columnList.size()) && columnList[col]) + return columnList[col]->name; return QString::fromUtf8(m_roData->getColName(col)); } @@ -475,7 +493,7 @@ std::tuple CrudModel::createUpdateQuery(Oid table_oid, c std::tuple CrudModel::createInsertQuery(const PendingRow &pending_row) { Pgsql::Params params; - const auto &data = pending_row.data(); + auto data = pending_row.data(); QString table_name = m_table->fullyQualifiedQuotedObjectName(); QString buffer; QTextStream q(&buffer); @@ -505,6 +523,7 @@ std::tuple CrudModel::createInsertQuery(const PendingRow QString CrudModel::createDeleteStatement() const { + Pgsql::Params params; QString table_name = m_table->fullyQualifiedQuotedObjectName(); QString buffer; QTextStream q(&buffer); @@ -528,25 +547,40 @@ bool CrudModel::savePendingChanges() while (!m_pendingRowList.m_rows.empty()) { auto iter = m_pendingRowList.m_rows.begin(); - RowMapping *mapping = findRowMapping(iter->first); - if (!mapping) + int pendingKey = iter->first; + auto mapping_iter = std::find_if(m_rowMapping.begin(), m_rowMapping.end(), + [pendingKey](const RowMapping &rhs) -> bool { return rhs.rowKey == pendingKey; }); + if (mapping_iter == m_rowMapping.end()) return false; - auto [ok, modified_row] = saveRow(iter->second, *mapping); - if (!ok) - return false; + auto [ok, modified_row] = saveRow(iter->second, *mapping_iter); + if (ok) + { + int rowKey = iter->first; + m_pendingRowList.m_rows.erase(iter); - m_pendingRowList.m_rows.erase(iter); - mapping->setModifiedRowData(modified_row); - mapping->pending = false; - // appending invalidates mapping, so keep this last - if (mapping == &m_rowMapping.back()) - appendNewRow(); + auto iter = std::find_if(m_rowMapping.begin(), m_rowMapping.end(), + [rowKey](const RowMapping &rhs) -> bool { return rhs.rowKey == rowKey; }); + if (iter != m_rowMapping.end()) + { + iter->setModifiedRowData(modified_row); + iter->pending = false; + if (IsLastRow(iter)) + appendNewRow(); + } + } + else + return false; } return true; } +bool CrudModel::IsLastRow(RowMappingVector::iterator mapping_iter) const +{ + return mapping_iter == --m_rowMapping.end(); +} + bool CrudModel::columnIsReadOnly(int column_index) const { if (m_roData == nullptr) @@ -556,30 +590,30 @@ bool CrudModel::columnIsReadOnly(int column_index) const if (!m_mapping || !m_mapping->isEditable(col)) return true; - const auto &column = m_columnList[col]; + const auto &column = columnList[col]; return !column || column->getIdentity() == PgAttribute::Identity::Always || column->getGenerated() != PgAttribute::Generated::None; } void CrudModel::initializeColumnList() { - m_columnList.clear(); - m_columnList.reserve(m_roData->cols()); + columnList.clear(); + columnList.reserve(m_roData->cols()); auto atts = m_database->catalog()->attributes(); for (int col = 0; col < m_roData->cols(); ++col) { int attnum = m_roData->ftableCol(col); if (attnum <= 0) { - m_columnList.emplace_back(); + columnList.emplace_back(); continue; } auto columns = atts->getColumnsForRelation(m_roData->ftable(col)); auto find_result = std::find_if(columns.begin(), columns.end(), [attnum](const PgAttribute &att) { return att.num == attnum; }); if (find_result != columns.end()) - m_columnList.emplace_back(*find_result); + columnList.emplace_back(*find_result); else - m_columnList.emplace_back(); + columnList.emplace_back(); } } @@ -587,7 +621,8 @@ bool CrudModel::saveChanges() { bool was_dirty = hasPendingChanges(); bool ok = savePendingChanges(); - updatePendingState(was_dirty); + if (was_dirty != hasPendingChanges()) + emit pendingChangesChanged(hasPendingChanges()); return ok; } @@ -629,42 +664,49 @@ std::tuple CrudModel::removeRows(const std::set return { false, "Range error" }; if (!m_wholeTable || !m_table || m_mapping->primaryKeyColumns(m_table->oid()).empty()) return { false, "Rows can only be removed from a table with a primary key" }; + // When removing rows there is no direct mapping anymore between the rows in the grid + // and the rows in m_roData + + // Therefor we need an indirection to keep track of which rows are visible and which + // grid row maps to what data row. Maybe we can also keep track where the current data + // of the row is located original data, pending data or modified data + + // 1. Get PKEY and remove that row from table - // When removing rows there is no direct mapping anymore between the rows in the grid - // and the rows in m_roData, that is why the grid rows are mapped through m_rowMapping. try { - deleteRowsFromDatabase(row_ranges); - // If something goes wrong after the commit we should reload contents of model + Pgsql::Connection db_update_conn; + auto dbconfig = m_database->config(); + db_update_conn.connect(dbconfig.connectionString()); + + // First delete rows in table + QString delete_statement = createDeleteStatement(); + { + auto tx = Pgsql::Transaction::startTransaction(db_update_conn); + for (auto range : row_ranges) + { + for (int current_row = range.start(); current_row < range.end(); ++current_row) + { + auto&& mapping = m_rowMapping[static_cast(current_row)]; + auto params = getPKeyParamsForRow(mapping, m_table->oid()); + if (!params.empty()) + db_update_conn.queryParam(delete_statement, params); + } + } + tx.commit(); + // If something goes wrong after this commit we should reload contents of model + } + // Then from model bool was_dirty = hasPendingChanges(); - removeRangesFromModel(row_ranges); - updatePendingState(was_dirty); - return { true, "" }; + RemoveRangesOfRowsFromModel(row_ranges); + if (was_dirty != hasPendingChanges()) + emit pendingChangesChanged(hasPendingChanges()); + return { true, "" }; } catch (const Pgsql::PgResultError &error) { - return { false, QString::fromUtf8(error.details().messageDetail.c_str()) }; - } -} - -void CrudModel::deleteRowsFromDatabase(const std::set> &row_ranges) -{ - Pgsql::Connection db_update_conn; - connectForUpdate(db_update_conn); - - QString delete_statement = createDeleteStatement(); - auto tx = Pgsql::Transaction::startTransaction(db_update_conn); - for (const auto &range : row_ranges) - { - for (int current_row = range.start(); current_row < range.end(); ++current_row) - { - const auto &mapping = m_rowMapping[static_cast(current_row)]; - auto params = getPKeyParamsForRow(mapping, m_table->oid()); - if (!params.empty()) - db_update_conn.queryParam(delete_statement, params); - } - } - tx.commit(); + return { false, QString::fromUtf8(error.details().messageDetail.c_str()) }; + } } bool CrudModel::removeRows(int row, int count, const QModelIndex &) @@ -677,18 +719,18 @@ bool CrudModel::removeRows(int row, int count, const QModelIndex &) return res; } -void CrudModel::removeRangesFromModel(const std::set> &row_ranges) +void CrudModel::RemoveRangesOfRowsFromModel(const std::set> &row_ranges) { int rows_deleted = 0; for (auto range : row_ranges) { range.setStart(range.start() - rows_deleted); - removeRangeFromModel(range); + RemoveRangeOfRowsFromModel(range); rows_deleted += range.length(); } } -void CrudModel::removeRangeFromModel(IntegerRange range) +void CrudModel::RemoveRangeOfRowsFromModel(IntegerRange range) { beginRemoveRows(QModelIndex(), range.start(), range.end() - 1); SCOPE_EXIT { endRemoveRows(); }; diff --git a/pglab/crud/CrudModel.h b/pglab/crud/CrudModel.h index 3fbe91e..485b6c2 100644 --- a/pglab/crud/CrudModel.h +++ b/pglab/crud/CrudModel.h @@ -31,9 +31,6 @@ public: /// Show an arbitrary query result, cells are editable where the result maps them to a table with a primary key void setResult(std::shared_ptr db, std::shared_ptr result); - /// Number of grid columns in front of the result columns (the row number column) - static constexpr int PreColumnCount = 1; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; @@ -63,8 +60,13 @@ signals: /// Emitted when the model goes from clean to having pending changes or back void pendingChangesChanged(bool hasChanges); +public slots: + private: + const int PreColumnCount = 1; + + class ColumnSort { public: enum Direction { Ascending, Descending }; @@ -123,6 +125,17 @@ private: public: using Map = std::map; + PendingRow& getRow(int row) + { + auto iter = m_rows.lower_bound(row); + if (iter != m_rows.end() && iter->first == row) { + return iter->second; + } + else { + return m_rows.insert(iter, {row, PendingRow(row)})->second; + } + } + void setValue(int col, int row, const Value &value) { auto iter = m_rows.find(row); @@ -213,7 +226,13 @@ private: std::shared_ptr m_roData; - std::vector> m_columnList; // attribute per column in m_roData, empty for expression columns + struct ColumnData + { + std::string name; + // identity allways or generated + }; + + std::vector> columnList; // attribute per column in m_roData, empty for expression columns PendingRowList m_pendingRowList; @@ -223,64 +242,47 @@ private: /// call on initial load to fill in the mappings void initRowMapping(); - /// Value as shown in the grid, pending edits take precedence over saved data Value getLatestData(int column, int row) const; Value getLatestData(const QModelIndex &index) const { return getLatestData(index.column() - PreColumnCount, index.row()); } - /// Value as last loaded from or saved to the database + Value getSavedData(int columnIndex, int rowIndex) const; Value getSavedData(const RowMapping &row_mapping, int columnIndex) const; - /// Mapping of the row with the given key, nullptr when the row is not in the grid (anymore) - RowMapping* findRowMapping(int rowKey); - /// Call after changing the pending rows to emit pendingChangesChanged when the state toggled - void updatePendingState(bool was_dirty); - Oid getType(int column) const; - // ---- Saving, all of these only use the data of the pending row and the row mapping + + /// Get the PKey values from when the row was last restored or retrieved. + Pgsql::Params getPKeyParamsForRow(const RowMapping &row_mapping, Oid table_oid) const; bool savePendingChanges(); - /// Inserts the row when it is new otherwise updates it. Returns the values of the row as stored in the database. - std::tuple> saveRow(const PendingRow &pending_row, const RowMapping &row_mapping); - std::tuple> saveUpdate(const PendingRow &pending_row, const RowMapping &row_mapping); - /// Get the PKey values from when the row was last restored or retrieved. - Pgsql::Params getPKeyParamsForRow(const RowMapping &row_mapping, Oid table_oid) const; - std::tuple createUpdateQuery(Oid table_oid, const PendingRow &pending_row, const RowMapping &row_mapping); + std::tuple createUpdateQuery(Oid table_oid, const PendingRow &pending_row, const RowMapping &row_mapping); std::tuple createInsertQuery(const PendingRow &pending_row); + QString createDeleteStatement() const; - - /// Opens a separate connection for writing so edits don't interfere with the loading connection - void connectForUpdate(Pgsql::Connection &connection) const; - /// Tells the user about a failed write, the one place where the model shows a dialog - void reportError(const QString &message) const; - static std::vector rowValues(const Pgsql::Row &row); - - // ---- Rows + std::tuple> saveRow(const PendingRow &pending_row, const RowMapping &row_mapping); + std::tuple> saveUpdate(const PendingRow &pending_row, const RowMapping &row_mapping); void appendNewRow(); - void appendNewRowInternal(); + void appendNewRowInternal(); - int lastRowKey = -1; + int lastRowKey = -1; int allocNewRowKey() { return ++lastRowKey; } - - /// Deletes the rows from the database in one transaction, throws on failure - void deleteRowsFromDatabase(const std::set> &row_ranges); - /// Removes the rows from the grid and drops their pending edits - void removeRangesFromModel(const std::set> &row_ranges); - void removeRangeFromModel(IntegerRange row_range); - - // ---- Table and column information - /// Name of the table with the given oid, quoted and schema qualified QString tableName(Oid table_oid) const; + /// Result columns that are user columns (attnum > 0) of the given table, in result order + std::vector resultColumnsOfTable(Oid table_oid) const; /// Catalog name of the attribute behind a result column, falls back to the result column name QString attributeName(int col) const; - bool columnIsReadOnly(int column_index) const; - void initializeColumnList(); + void RemoveRangesOfRowsFromModel(const std::set> &row_ranges); + void RemoveRangeOfRowsFromModel(IntegerRange row_range); + + bool IsLastRow(RowMappingVector::iterator mapping_iter) const; + bool columnIsReadOnly(int column_index) const; + void initializeColumnList(); private slots: void loadIntoModel(std::shared_ptr data); diff --git a/pglab/crud/DataGridWidget.cpp b/pglab/crud/DataGridWidget.cpp index bd9e2ae..65708f7 100644 --- a/pglab/crud/DataGridWidget.cpp +++ b/pglab/crud/DataGridWidget.cpp @@ -1,13 +1,10 @@ #include "DataGridWidget.h" #include "CrudModel.h" #include "CustomDataRole.h" -#include "RecordPanel.h" #include "ResultTableModelUtil.h" #include "util/PgLabItemDelegate.h" #include -#include #include -#include #include #include #include @@ -39,28 +36,12 @@ DataGridWidget::DataGridWidget(QWidget *parent) m_toolbar->addAction(m_actRevert); connect(m_actRevert, &QAction::triggered, this, &DataGridWidget::onRevert); - m_actRecord = new QAction(tr("Record"), this); - m_actRecord->setCheckable(true); - m_actRecord->setChecked(true); - m_actRecord->setToolTip(tr("Show the values of the current row next to the grid")); - m_toolbar->addAction(m_actRecord); - m_view = new QTableView(this); + layout->addWidget(m_view); + SetTableViewDefault(m_view); auto delegate = new PgLabItemDelegate(m_view); m_view->setItemDelegate(delegate); - - m_splitter = new QSplitter(Qt::Horizontal, this); - layout->addWidget(m_splitter); - m_splitter->addWidget(m_view); - - m_recordPanel = new RecordPanel(this); - m_recordPanel->setFirstDataColumn(CrudModel::PreColumnCount); - m_splitter->addWidget(m_recordPanel); - m_splitter->setStretchFactor(0, 1); - m_splitter->setStretchFactor(1, 0); - - connect(m_actRecord, &QAction::toggled, m_recordPanel, &QWidget::setVisible); } void DataGridWidget::setSourceModel(QAbstractItemModel *model) @@ -70,18 +51,13 @@ void DataGridWidget::setSourceModel(QAbstractItemModel *model) m_proxy->setSortRole(CustomSortRole); } disconnect(m_pendingChangesConnection); - disconnect(m_currentRowConnection); m_proxy->setSourceModel(model); m_view->setModel(m_proxy); m_view->setSortingEnabled(true); m_proxy->sort(0, Qt::AscendingOrder); - m_recordPanel->setModel(m_proxy); - m_currentRowConnection = connect(m_view->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DataGridWidget::onCurrentRowChanged); - auto crud = qobject_cast(model); - m_actSave->setVisible(crud != nullptr); - m_actRevert->setVisible(crud != nullptr); + m_toolbar->setVisible(crud != nullptr); if (crud) { m_pendingChangesConnection = connect(crud, &CrudModel::pendingChangesChanged, this, &DataGridWidget::onPendingChangesChanged); onPendingChangesChanged(crud->hasPendingChanges()); @@ -91,11 +67,6 @@ void DataGridWidget::setSourceModel(QAbstractItemModel *model) } } -void DataGridWidget::onCurrentRowChanged(const QModelIndex ¤t) -{ - m_recordPanel->setCurrentRow(current.row()); -} - void DataGridWidget::onPendingChangesChanged(bool hasChanges) { m_actSave->setEnabled(hasChanges); diff --git a/pglab/crud/DataGridWidget.h b/pglab/crud/DataGridWidget.h index bfcda78..d605053 100644 --- a/pglab/crud/DataGridWidget.h +++ b/pglab/crud/DataGridWidget.h @@ -5,12 +5,9 @@ class QAbstractItemModel; class QAction; -class QModelIndex; class QSortFilterProxyModel; -class QSplitter; class QTableView; class QToolBar; -class RecordPanel; /** Sortable and editable grid shared by the CRUD tab and the query result tab. */ @@ -29,7 +26,6 @@ private slots: void onPendingChangesChanged(bool hasChanges); void onSave(); void onRevert(); - void onCurrentRowChanged(const QModelIndex ¤t); private: /// Closes an open cell editor so its value is written to the model before saving or reverting @@ -37,14 +33,10 @@ private: QTableView *m_view = nullptr; QSortFilterProxyModel *m_proxy = nullptr; - QSplitter *m_splitter = nullptr; - RecordPanel *m_recordPanel = nullptr; QToolBar *m_toolbar = nullptr; QAction *m_actSave = nullptr; QAction *m_actRevert = nullptr; - QAction *m_actRecord = nullptr; QMetaObject::Connection m_pendingChangesConnection; - QMetaObject::Connection m_currentRowConnection; }; #endif // DATAGRIDWIDGET_H diff --git a/pglab/crud/RecordPanel.cpp b/pglab/crud/RecordPanel.cpp deleted file mode 100644 index 771bb0e..0000000 --- a/pglab/crud/RecordPanel.cpp +++ /dev/null @@ -1,274 +0,0 @@ -#include "RecordPanel.h" -#include "CustomDataRole.h" -#include "Pgsql_oids.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - -constexpr int CollapsedTextLines = 2; -constexpr int ExpandedTextLines = 10; - -bool isLongTextType(Oid oid) -{ - switch (oid) { - case Pgsql::text_oid: - case Pgsql::varchar_oid: - case Pgsql::bpchar_oid: - case Pgsql::json_oid: - case Pgsql::jsonb_oid: - case Pgsql::xml_oid: - return true; - default: - return false; - } -} - -int textEditHeight(const QPlainTextEdit *edit, int lines) -{ - return edit->fontMetrics().lineSpacing() * lines + 2 * edit->frameWidth() + 6; -} - -QString editorText(const QWidget *editor) -{ - if (auto line = qobject_cast(editor)) - return line->text(); - if (auto plain = qobject_cast(editor)) - return plain->toPlainText(); - return {}; -} - -} // namespace - -RecordPanel::RecordPanel(QWidget *parent) - : QScrollArea(parent) -{ - setWidgetResizable(true); -} - -void RecordPanel::setModel(QAbstractItemModel *model) -{ - if (m_model) - m_model->disconnect(this); - - m_model = model; - if (m_model) { - connect(m_model, &QAbstractItemModel::dataChanged, this, &RecordPanel::refresh); - connect(m_model, &QAbstractItemModel::layoutChanged, this, &RecordPanel::refresh); - connect(m_model, &QAbstractItemModel::rowsInserted, this, &RecordPanel::refresh); - connect(m_model, &QAbstractItemModel::rowsRemoved, this, &RecordPanel::refresh); - connect(m_model, &QAbstractItemModel::modelReset, this, &RecordPanel::onModelReset); - } - onModelReset(); -} - -void RecordPanel::setFirstDataColumn(int column) -{ - m_firstDataColumn = column; - clearFields(); - refresh(); -} - -void RecordPanel::setCurrentRow(int row) -{ - if (m_model && row >= 0 && row < m_model->rowCount()) - m_rowIndex = QPersistentModelIndex(m_model->index(row, 0)); - else - m_rowIndex = QPersistentModelIndex(); - refresh(); -} - -void RecordPanel::onModelReset() -{ - m_rowIndex = QPersistentModelIndex(); - refresh(); -} - -void RecordPanel::refresh() -{ - if (!m_model || !m_rowIndex.isValid()) { - clearFields(); - return; - } - - int columns = m_model->columnCount() - m_firstDataColumn; - if (columns < 0) - columns = 0; - if (m_fields.size() != static_cast(columns)) - rebuild(); - - for (auto &field : m_fields) { - QModelIndex index = m_model->index(m_rowIndex.row(), field.column); - bool editable = m_model->flags(index).testFlag(Qt::ItemIsEditable); - QVariant value = index.data(Qt::EditRole); - QString text; - if (value.isValid() && !value.isNull()) { - text = value.toString(); - // An empty string is shown as '' so it can be told apart from NULL, the placeholder shows NULL - if (text.isEmpty()) - text = QStringLiteral("''"); - } - showValue(field, text, editable); - } -} - -void RecordPanel::clearFields() -{ - m_fields.clear(); - if (QWidget *old = takeWidget()) - old->deleteLater(); -} - -void RecordPanel::rebuild() -{ - clearFields(); - - auto content = new QWidget; - auto outer = new QVBoxLayout(content); - outer->setContentsMargins(0, 0, 0, 0); - auto form = new QWidget(content); - auto layout = new QFormLayout(form); - layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - - int row = m_rowIndex.row(); - for (int column = m_firstDataColumn; column < m_model->columnCount(); ++column) { - QString header = m_model->headerData(column, Qt::Horizontal, Qt::DisplayRole).toString(); - int newline = header.indexOf('\n'); - QString name = newline < 0 ? header : header.left(newline); - QString typeName = newline < 0 ? QString() : header.mid(newline + 1); - - auto caption = new QWidget(form); - auto captionLayout = new QVBoxLayout(caption); - captionLayout->setContentsMargins(0, 0, 0, 0); - auto nameLabel = new QLabel(name, caption); - QFont nameFont = nameLabel->font(); - nameFont.setBold(true); - nameLabel->setFont(nameFont); - auto typeLabel = new QLabel(typeName, caption); - captionLayout->addWidget(nameLabel); - captionLayout->addWidget(typeLabel); - - Oid oid = m_model->index(row, column).data(CustomDataTypeRole).toUInt(); - FieldRow field; - field.column = column; - QWidget *fieldWidget = nullptr; - - if (isLongTextType(oid)) { - auto container = new QWidget(form); - auto containerLayout = new QVBoxLayout(container); - containerLayout->setContentsMargins(0, 0, 0, 0); - - auto edit = new QPlainTextEdit(container); - edit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); - edit->setPlaceholderText(tr("null")); - edit->setFixedHeight(textEditHeight(edit, CollapsedTextLines)); - edit->installEventFilter(this); - - auto expand = new QToolButton(container); - expand->setCheckable(true); - expand->setArrowType(Qt::DownArrow); - expand->setToolTip(tr("Expand or collapse the value")); - connect(expand, &QToolButton::toggled, edit, [edit, expand](bool expanded) { - edit->setFixedHeight(textEditHeight(edit, expanded ? ExpandedTextLines : CollapsedTextLines)); - expand->setArrowType(expanded ? Qt::UpArrow : Qt::DownArrow); - }); - - auto buttonRow = new QHBoxLayout; - buttonRow->addStretch(); - buttonRow->addWidget(expand); - containerLayout->addWidget(edit); - containerLayout->addLayout(buttonRow); - - field.editor = edit; - field.expandButton = expand; - fieldWidget = container; - } - else { - auto edit = new QLineEdit(form); - edit->setPlaceholderText(tr("null")); - connect(edit, &QLineEdit::editingFinished, this, [this, edit]() { commitEditor(edit); }); - - field.editor = edit; - fieldWidget = edit; - } - - layout->addRow(caption, fieldWidget); - m_fields.push_back(field); - } - - outer->addWidget(form); - outer->addStretch(1); - - setWidget(content); -} - -void RecordPanel::showValue(FieldRow &field, const QString &text, bool editable) -{ - QWidget *editor = field.editor; - if (auto line = qobject_cast(editor)) - line->setReadOnly(!editable); - else if (auto plain = qobject_cast(editor)) - plain->setReadOnly(!editable); - - // Do not replace the text while the user is typing in it - if (editor->hasFocus()) - return; - - if (auto line = qobject_cast(editor)) - line->setText(text); - else if (auto plain = qobject_cast(editor)) - plain->setPlainText(text); - field.original = text; -} - -void RecordPanel::commitEditor(QWidget *editor) -{ - if (m_updating || !m_model || !m_rowIndex.isValid()) - return; - - FieldRow *field = findField(editor); - if (!field) - return; - - QString text = editorText(editor); - if (text == field->original) - return; - - QModelIndex index = m_model->index(m_rowIndex.row(), field->column); - if (!m_model->flags(index).testFlag(Qt::ItemIsEditable)) - return; - - m_updating = true; - m_model->setData(index, text, Qt::EditRole); - m_updating = false; - - // The model may have rebuilt the fields while storing the value, so look the field up again - if (FieldRow *updated = findField(editor)) - updated->original = text; -} - -RecordPanel::FieldRow *RecordPanel::findField(QWidget *editor) -{ - for (auto &field : m_fields) { - if (field.editor == editor) - return &field; - } - return nullptr; -} - -bool RecordPanel::eventFilter(QObject *watched, QEvent *event) -{ - if (event->type() == QEvent::FocusOut) { - if (auto edit = qobject_cast(watched)) - commitEditor(edit); - } - return QScrollArea::eventFilter(watched, event); -} diff --git a/pglab/crud/RecordPanel.h b/pglab/crud/RecordPanel.h deleted file mode 100644 index 463038d..0000000 --- a/pglab/crud/RecordPanel.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef RECORDPANEL_H -#define RECORDPANEL_H - -#include -#include -#include - -class QAbstractItemModel; -class QEvent; -class QToolButton; - -/** Shows the values of one row of a model as a scrollable list, one line per column. - * Values are edited through the model so the pending change logic of the model is reused. - */ -class RecordPanel : public QScrollArea { - Q_OBJECT -public: - explicit RecordPanel(QWidget *parent = nullptr); - - /// Sets the model to show, the columns from firstDataColumn on are shown - void setModel(QAbstractItemModel *model); - /// Columns before this one are skipped, e.g. the row number column of the CRUD model - void setFirstDataColumn(int column); - /// Shows the values of the given row of the model, -1 shows nothing - void setCurrentRow(int row); - -protected: - bool eventFilter(QObject *watched, QEvent *event) override; - -private slots: - void refresh(); - void onModelReset(); - -private: - struct FieldRow { - int column = -1; - QString original; ///< Text the editor shows, only a different text is written back - QWidget *editor = nullptr; - QToolButton *expandButton = nullptr; - }; - - void clearFields(); - void rebuild(); - void showValue(FieldRow &field, const QString &text, bool editable); - void commitEditor(QWidget *editor); - FieldRow *findField(QWidget *editor); - - QAbstractItemModel *m_model = nullptr; - int m_firstDataColumn = 0; - QPersistentModelIndex m_rowIndex; - std::vector m_fields; - bool m_updating = false; -}; - -#endif // RECORDPANEL_H diff --git a/pglab/querytool/QueryTool.cpp b/pglab/querytool/QueryTool.cpp index 9934977..f808335 100644 --- a/pglab/querytool/QueryTool.cpp +++ b/pglab/querytool/QueryTool.cpp @@ -16,22 +16,17 @@ #include "json/json.h" #include "OpenDatabase.h" #include "catalog/PgDatabaseCatalog.h" -#include "SqlCompletionProvider.h" #include "Pgsql_PgException.h" #include "QueryParamListController.h" #include "util.h" #include "UserConfiguration.h" #include "IDatabaseWindow.h" -#include "UnsavedDocumentsDialog.h" -#include -#include -QueryTool::QueryTool(IDatabaseWindow *context, std::shared_ptr document, QWidget *parent) +QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent) : ManagedPage(parent) , m_context(context) , ui(new Ui::QueryTab) - , m_document(std::move(document)) , m_dbConnection() { ui->setupUi(this); @@ -43,20 +38,16 @@ QueryTool::QueryTool(IDatabaseWindow *context, std::shared_ptr docu connect(&m_dbConnection, &ASyncDBConnection::onStateChanged, this, &QueryTool::connectionStateChanged); connect(&m_dbConnection, &ASyncDBConnection::onNotice, this, &QueryTool::receiveNotice); - const QFont codeFont = UserConfiguration::instance()->codeFont(); - m_document->textDocument()->setDefaultFont(codeFont); - ui->queryEdit->setDocument(m_document->textDocument()); - ui->queryEdit->setFont(codeFont); - m_document->addEditor(this); + ui->queryEdit->setFont(UserConfiguration::instance()->codeFont()); + + highlighter = new SqlSyntaxHighlighter(ui->queryEdit->document()); auto types = m_catalog->types(); if (types) { - m_document->highlighter()->setTypes(*types); + highlighter->setTypes(*types); } - connect(m_document.get(), &SqlDocument::modifiedChanged, this, &QueryTool::updateTitle); - connect(m_document.get(), &SqlDocument::fileNameChanged, this, &QueryTool::updateTitle); + connect(ui->queryEdit, &QPlainTextEdit::textChanged, this, &QueryTool::queryTextChanged); ui->queryEdit->setAcceptDrops(false); - setupCompletion(); m_queryParamListController = new QueryParamListController(ui->paramTableView, m_context->openDatabase(), this); connect(ui->addButton, &QPushButton::clicked, m_queryParamListController, @@ -67,73 +58,89 @@ QueryTool::QueryTool(IDatabaseWindow *context, std::shared_ptr docu startConnect(); } -void QueryTool::setupCompletion() -{ - connect(m_catalog.get(), &PgDatabaseCatalog::refreshed, this, [this]() { - m_completionProvider.reset(); - }); - - ui->queryEdit->setCompletionCallback([this](const QString &text, int pos) { - EditorCompletionResult result; - if (!m_catalog) - return result; - if (!m_completionProvider) - m_completionProvider = std::make_shared(buildCompletionCatalog(*m_catalog)); - - const SqlContext ctx = analyzeSqlContext(text, pos); - result.replaceStart = ctx.prefixStart; - result.replaceLen = ctx.prefixLen; - result.hasQualifier = !ctx.qualifier.isEmpty(); - for (const auto &item : m_completionProvider->complete(ctx)) { - EditorCompletionItem i; - i.display = item.display; - i.detail = item.detail; - i.insertText = item.insertText; - i.ambiguous = item.ambiguous; - result.items.push_back(std::move(i)); - } - return result; - }); -} - QueryTool::~QueryTool() { - // Detach the editor from the shared document, it may be destroyed together with this tool - auto detached = new QTextDocument(ui->queryEdit); - detached->setDocumentLayout(new QPlainTextDocumentLayout(detached)); - ui->queryEdit->setDocument(detached); - m_document->removeEditor(this); delete ui; } bool QueryTool::CanClose(bool prompt_user) { - if (!m_document->isModified()) - return true; + bool can_close; + if (m_queryTextChanged) { + if (prompt_user) + can_close = continueWithoutSavingWarning(); + else + can_close = false; + } + else { + can_close = true; + } + return can_close; +} - // The document stays open in another editor, so it is not closed with this tool - if (m_document->editors().size() > 1) - return true; +void QueryTool::newdoc() +{ + ui->queryEdit->clear(); + setFileName(tr("new")); + setQueryTextChanged(false); + m_new = true; +} - if (!prompt_user) - return false; +bool QueryTool::load(const QString &filename) +{ + bool result = false; + QFile file(filename); + if (file.open(QIODevice::ReadOnly)) { + QByteArray ba = file.readAll(); + auto toUtf16 = QStringDecoder(QStringDecoder::Utf8); + QString text = toUtf16(ba); + if (toUtf16.hasError()) { + file.reset(); + QTextStream stream(&file); + text = stream.readAll(); + } - return confirmClose(this, {m_document}); + ui->queryEdit->setPlainText(text); + setQueryTextChanged(false); + setFileName(filename); + m_new = false; + result = true; + } + return result; } bool QueryTool::save() { - return m_document->save(this); + bool result; + if (m_fileName.isEmpty() || m_new) { + result = saveAs(); + } + else { + result = saveSqlTo(m_fileName); + } + return result; } bool QueryTool::saveAs() { - return m_document->saveAs(this); + bool result = false; + QString filename = promptUserForSaveSqlFilename(); + if (!filename.isEmpty()) { + result = saveSqlTo(filename); + if (result) { + setFileName(filename); + m_new = false; + } + } + return result; } void QueryTool::saveCopyAs() { - m_document->saveCopyAs(this); + QString filename = promptUserForSaveSqlFilename(); + if (!filename.isEmpty()) { + saveSqlTo(filename); + } } void QueryTool::execute() @@ -238,21 +245,81 @@ void QueryTool::cancel() QString QueryTool::title() const { - QFileInfo fileInfo(m_document->fileName()); + QFileInfo fileInfo(m_fileName); QString fn(fileInfo.fileName()); return fn; } +void QueryTool::setFileName(const QString &filename) +{ + m_fileName = filename; + updateTitle(); +} + void QueryTool::updateTitle() { - QFileInfo fileInfo(m_document->fileName()); + QFileInfo fileInfo(m_fileName); QString title = fileInfo.fileName(); - if (m_document->isModified()) { + if (m_queryTextChanged) { title = "*" + title; } - m_context->setTitleForWidget(this, title, m_document->fileName()); + m_context->setTitleForWidget(this, title, m_fileName); +} + +bool QueryTool::continueWithoutSavingWarning() +{ + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Warning); + msgBox.setText(QString("Save changes in document \"%1\" before closing?").arg(m_fileName)); + msgBox.setInformativeText("The changes will be lost when you choose Discard."); + msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + int ret = msgBox.exec(); + if (ret == QMessageBox::Save) { + if (!save()) { + // save failed or was a saveAs and was cancelled, don't close! + ret = QMessageBox::Cancel; + } + } + + return ret != QMessageBox::Cancel; +} + +bool QueryTool::saveSqlTo(const QString &filename) +{ + bool result = false; + QFileInfo fileinfo(filename); + + + QFile file(filename); + if (file.open(QIODevice::WriteOnly)) { + QTextStream stream(&file); + //stream.setCodec("utf-8"); + stream.setEncoding(QStringConverter::Utf8); + QString text = ui->queryEdit->toPlainText(); + stream << text; + + stream.flush(); + if (stream.status() == QTextStream::Ok) { + setQueryTextChanged(false); + result = true; + } + } + return result; +} + + +QString QueryTool::promptUserForSaveSqlFilename() +{ + QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory); + return QFileDialog::getSaveFileName(this, tr("Save query"), home_dir, tr("SQL file (*.sql)")); +} + +void QueryTool::queryTextChanged() +{ + setQueryTextChanged(true); } void QueryTool::connectionStateChanged() @@ -551,3 +618,10 @@ void QueryTool::exportData() exportDataToFilename(file_name); } +void QueryTool::setQueryTextChanged(bool queryTextChanged) +{ + if (m_queryTextChanged != queryTextChanged) { + m_queryTextChanged = queryTextChanged; + updateTitle(); + } +} diff --git a/pglab/querytool/QueryTool.h b/pglab/querytool/QueryTool.h index 854c44b..1274642 100644 --- a/pglab/querytool/QueryTool.h +++ b/pglab/querytool/QueryTool.h @@ -6,7 +6,6 @@ #include "QueryExplainModel.h" #include "stopwatch.h" #include "tuplesresultwidget.h" -#include "SqlDocument.h" #include #include @@ -25,7 +24,6 @@ class QTableView; class QTabWidget; class SqlSyntaxHighlighter; -class SqlCompletionProvider; class ExplainRoot; class QueryExplainModel; class PgTypeContainer; @@ -36,9 +34,12 @@ class PgDatabaseCatalog; class QueryTool : public ManagedPage { Q_OBJECT public: - QueryTool(IDatabaseWindow *context, std::shared_ptr document, QWidget *parent = nullptr); + QueryTool(IDatabaseWindow *context, QWidget *parent = nullptr); ~QueryTool() override; + void newdoc(); + bool load(const QString &filename); + void explain(bool analyze); bool CanClose(bool prompt_user); @@ -46,12 +47,12 @@ public: void generateCode(); void exportDataToFilename(const QString &filename); - QString fileName() const { return m_document->fileName(); } - std::shared_ptr document() const { return m_document; } + QString fileName() const { return m_fileName; } + bool isChanged() const { return m_queryTextChanged; } + bool isNew() const { return m_new; } void focusEditor(); QString title() const; - void updateTitle(); public slots: @@ -73,22 +74,27 @@ private: using ResultTabContainer = std::vector; IDatabaseWindow *m_context; Ui::QueryTab *ui; - std::shared_ptr m_document; + SqlSyntaxHighlighter* highlighter; ConnectionConfig m_config; StopWatch m_stopwatch; QueryParamListController *m_queryParamListController = nullptr; + bool m_new = true; + QString m_fileName; ///< use setFileName function to set + bool m_queryTextChanged = false; std::shared_ptr m_catalog; - std::shared_ptr m_completionProvider; ///< built on first use, reset when the catalog is refreshed - - void setupCompletion(); ASyncDBConnection m_dbConnection; std::unique_ptr explainModel; ResultTabContainer resultList; + void setFileName(const QString &filename); + bool continueWithoutSavingWarning(); + bool saveSqlTo(const QString &filename); + QString promptUserForSaveSqlFilename(); + void addLog(QString s); QString getCommand() const; @@ -96,12 +102,15 @@ private: void clearResult(); void markError(const Pgsql::ErrorDetails &details); + void updateTitle(); + void setQueryTextChanged(bool queryTextChanged); private slots: void explain_ready(ExplainRoot::SPtr explain); void query_ready(std::shared_ptr, qint64 elapsedms); + void queryTextChanged(); void connectionStateChanged(); void receiveNotice(Pgsql::ErrorDetails notice); diff --git a/pglab/querytool/SqlDocument.cpp b/pglab/querytool/SqlDocument.cpp deleted file mode 100644 index cdd1bc2..0000000 --- a/pglab/querytool/SqlDocument.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include "SqlDocument.h" -#include "util/SqlSyntaxHighlighter.h" -#include -#include -#include -#include -#include -#include -#include -#include - -SqlDocument::SqlDocument(QObject *parent) - : QObject(parent) - , m_document(new QTextDocument(this)) - , m_highlighter(new SqlSyntaxHighlighter(m_document)) -{ - // QPlainTextEdit only accepts documents that use a QPlainTextDocumentLayout - m_document->setDocumentLayout(new QPlainTextDocumentLayout(m_document)); - connect(m_document, &QTextDocument::modificationChanged, this, &SqlDocument::modifiedChanged); - setFileName(tr("new")); -} - -SqlDocument::~SqlDocument() = default; - -bool SqlDocument::isModified() const -{ - return m_document->isModified(); -} - -void SqlDocument::setModified(bool modified) -{ - m_document->setModified(modified); -} - -void SqlDocument::clear() -{ - m_document->clear(); - setFileName(tr("new")); - m_new = true; - setModified(false); -} - -bool SqlDocument::load(const QString &filename) -{ - bool result = false; - QFile file(filename); - if (file.open(QIODevice::ReadOnly)) { - QByteArray ba = file.readAll(); - auto toUtf16 = QStringDecoder(QStringDecoder::Utf8); - QString text = toUtf16(ba); - if (toUtf16.hasError()) { - file.reset(); - QTextStream stream(&file); - text = stream.readAll(); - } - - m_document->setPlainText(text); - setModified(false); - setFileName(filename); - m_new = false; - result = true; - } - return result; -} - -bool SqlDocument::save(QWidget *parent) -{ - bool result; - if (m_fileName.isEmpty() || m_new) { - result = saveAs(parent); - } - else { - result = saveSqlTo(m_fileName); - } - return result; -} - -bool SqlDocument::saveAs(QWidget *parent) -{ - bool result = false; - QString filename = promptUserForSaveSqlFilename(parent); - if (!filename.isEmpty()) { - result = saveSqlTo(filename); - if (result) { - setFileName(filename); - m_new = false; - } - } - return result; -} - -bool SqlDocument::saveCopyAs(QWidget *parent) -{ - bool result = false; - QString filename = promptUserForSaveSqlFilename(parent); - if (!filename.isEmpty()) { - result = saveSqlTo(filename); - } - return result; -} - -void SqlDocument::addEditor(QWidget *editor) -{ - if (!m_editors.contains(editor)) - m_editors.append(editor); -} - -void SqlDocument::removeEditor(QWidget *editor) -{ - m_editors.removeAll(editor); -} - -void SqlDocument::setFileName(const QString &filename) -{ - m_fileName = filename; - emit fileNameChanged(); -} - -bool SqlDocument::saveSqlTo(const QString &filename) -{ - bool result = false; - - QFile file(filename); - if (file.open(QIODevice::WriteOnly)) { - QTextStream stream(&file); - stream.setEncoding(QStringConverter::Utf8); - QString text = m_document->toPlainText(); - stream << text; - - stream.flush(); - if (stream.status() == QTextStream::Ok) { - setModified(false); - result = true; - } - } - return result; -} - -QString SqlDocument::promptUserForSaveSqlFilename(QWidget *parent) -{ - QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory); - return QFileDialog::getSaveFileName(parent, tr("Save query"), home_dir, tr("SQL file (*.sql)")); -} diff --git a/pglab/querytool/SqlDocument.h b/pglab/querytool/SqlDocument.h deleted file mode 100644 index 6974462..0000000 --- a/pglab/querytool/SqlDocument.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef SQLDOCUMENT_H -#define SQLDOCUMENT_H - -#include -#include -#include - -class QTextDocument; -class QWidget; -class SqlSyntaxHighlighter; - -/** Owns the text of a SQL script together with its file state. - * Several query tools (editors) can show the same document, they all - * use the same QTextDocument. - */ -class SqlDocument : public QObject { - Q_OBJECT -public: - explicit SqlDocument(QObject *parent = nullptr); - ~SqlDocument() override; - - QTextDocument *textDocument() const { return m_document; } - SqlSyntaxHighlighter *highlighter() const { return m_highlighter; } - - QString fileName() const { return m_fileName; } - bool isNew() const { return m_new; } - bool isModified() const; - void setModified(bool modified); - - /// Empties the document and makes it a new unnamed document. - void clear(); - bool load(const QString &filename); - /// Save the document under its current name, a file save dialog will be shown if this is a new document - bool save(QWidget *parent); - /// Saves the document under a new name and continue editing the document under this new name. - bool saveAs(QWidget *parent); - /// Save the document under a new name but continue editing the document under its old name. - bool saveCopyAs(QWidget *parent); - - void addEditor(QWidget *editor); - void removeEditor(QWidget *editor); - const QList &editors() const { return m_editors; } - -signals: - void modifiedChanged(bool modified); - void fileNameChanged(); - -private: - QTextDocument *m_document; - SqlSyntaxHighlighter *m_highlighter; - QString m_fileName; - bool m_new = true; - QList m_editors; - - void setFileName(const QString &filename); - bool saveSqlTo(const QString &filename); - QString promptUserForSaveSqlFilename(QWidget *parent); -}; - -#endif // SQLDOCUMENT_H diff --git a/pglab/querytool/SqlDocumentRegistry.cpp b/pglab/querytool/SqlDocumentRegistry.cpp deleted file mode 100644 index 8d7364a..0000000 --- a/pglab/querytool/SqlDocumentRegistry.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "SqlDocumentRegistry.h" -#include "SqlDocument.h" - -void SqlDocumentRegistry::add(const std::shared_ptr &document) -{ - m_documents.push_back(document); -} - -QList> SqlDocumentRegistry::documents() const -{ - QList> result; - for (const auto &weak : m_documents) { - if (auto doc = weak.lock()) - result.append(doc); - } - return result; -} - -QList> SqlDocumentRegistry::modifiedDocuments() const -{ - QList> result; - for (auto &doc : documents()) { - if (doc->isModified()) - result.append(doc); - } - return result; -} diff --git a/pglab/querytool/SqlDocumentRegistry.h b/pglab/querytool/SqlDocumentRegistry.h deleted file mode 100644 index b63f63b..0000000 --- a/pglab/querytool/SqlDocumentRegistry.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef SQLDOCUMENTREGISTRY_H -#define SQLDOCUMENTREGISTRY_H - -#include -#include -#include - -class SqlDocument; - -/** Keeps track of all SQL documents that are still in use, without - * extending their lifetime. A document lives as long as at least one - * query tool holds a shared pointer to it. - */ -class SqlDocumentRegistry { -public: - void add(const std::shared_ptr &document); - - /// All documents that are still alive. - QList> documents() const; - /// All documents that are alive and have unsaved changes. - QList> modifiedDocuments() const; - -private: - std::vector> m_documents; -}; - -#endif // SQLDOCUMENTREGISTRY_H diff --git a/pglab/querytool/UnsavedDocumentsDialog.cpp b/pglab/querytool/UnsavedDocumentsDialog.cpp deleted file mode 100644 index f8d1050..0000000 --- a/pglab/querytool/UnsavedDocumentsDialog.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "UnsavedDocumentsDialog.h" -#include -#include -#include -#include -#include -#include - -UnsavedDocumentsDialog::UnsavedDocumentsDialog(const QList> &docs, QWidget *parent) - : QDialog(parent) - , m_docs(docs) - , m_list(new QListWidget(this)) -{ - setWindowTitle(tr("Unsaved changes")); - - auto layout = new QVBoxLayout(this); - layout->addWidget(new QLabel(tr("The following documents have unsaved changes. Select the documents to save or discard:"), this)); - - for (const auto &doc : m_docs) { - QStringList windows; - for (const QWidget *editor : doc->editors()) { - QString title = editor->window()->windowTitle(); - if (!windows.contains(title)) - windows.append(title); - } - - auto item = new QListWidgetItem(QString("%1 (%2)").arg(doc->fileName(), windows.join(", ")), m_list); - item->setToolTip(doc->fileName()); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Checked); - } - layout->addWidget(m_list); - - auto buttons = new QDialogButtonBox(this); - auto save_button = buttons->addButton(tr("Save selected"), QDialogButtonBox::AcceptRole); - auto discard_button = buttons->addButton(tr("Discard selected"), QDialogButtonBox::DestructiveRole); - buttons->addButton(QDialogButtonBox::Cancel); - layout->addWidget(buttons); - - connect(save_button, &QPushButton::clicked, this, [this]() { - m_choice = Choice::Save; - accept(); - }); - connect(discard_button, &QPushButton::clicked, this, [this]() { - m_choice = Choice::Discard; - accept(); - }); - connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -QList> UnsavedDocumentsDialog::checkedDocuments() const -{ - QList> result; - for (int i = 0; i < m_docs.size(); ++i) { - if (m_list->item(i)->checkState() == Qt::Checked) - result.append(m_docs[i]); - } - return result; -} - -QList> UnsavedDocumentsDialog::uncheckedDocuments() const -{ - QList> result; - for (int i = 0; i < m_docs.size(); ++i) { - if (m_list->item(i)->checkState() != Qt::Checked) - result.append(m_docs[i]); - } - return result; -} - -QList> documentsClosedWith(const QList> &docs, - const std::function &isClosing) -{ - QList> result; - for (const auto &doc : docs) { - const auto &editors = doc->editors(); - if (!doc->isModified() || editors.isEmpty()) - continue; - - bool all_closing = std::all_of(editors.begin(), editors.end(), - [&isClosing](const QWidget *editor) { return isClosing(editor); }); - if (all_closing) - result.append(doc); - } - return result; -} - -bool confirmClose(QWidget *parent, const QList> &docs) -{ - auto remaining = docs; - while (!remaining.isEmpty()) { - UnsavedDocumentsDialog dlg(remaining, parent); - if (dlg.exec() != QDialog::Accepted || dlg.choice() == UnsavedDocumentsDialog::Choice::Cancel) - return false; - - bool save = dlg.choice() == UnsavedDocumentsDialog::Choice::Save; - for (const auto &doc : dlg.checkedDocuments()) { - if (save) { - if (!doc->save(parent)) - return false; - } - else { - doc->setModified(false); - } - } - - // Show the dialog again for the documents the user did not select. - remaining = dlg.uncheckedDocuments(); - } - - return true; -} diff --git a/pglab/querytool/UnsavedDocumentsDialog.h b/pglab/querytool/UnsavedDocumentsDialog.h deleted file mode 100644 index c499876..0000000 --- a/pglab/querytool/UnsavedDocumentsDialog.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef UNSAVEDDOCUMENTSDIALOG_H -#define UNSAVEDDOCUMENTSDIALOG_H - -#include "SqlDocument.h" -#include -#include -#include -#include - -class QListWidget; - -/** Lists modified documents with a checkbox per document. The user picks - * whether the checked documents should be saved or discarded. - */ -class UnsavedDocumentsDialog : public QDialog { - Q_OBJECT -public: - enum class Choice { - Cancel, - Save, - Discard - }; - - UnsavedDocumentsDialog(const QList> &docs, QWidget *parent = nullptr); - - Choice choice() const { return m_choice; } - QList> checkedDocuments() const; - QList> uncheckedDocuments() const; - -private: - QList> m_docs; - QListWidget *m_list; - Choice m_choice = Choice::Cancel; -}; - -/** Selects the documents that are closed when every editor for which - * isClosing returns true is closed. Only modified documents are returned. - */ -QList> documentsClosedWith(const QList> &docs, - const std::function &isClosing); - -/** Asks the user what to do with the modified documents. Returns true when - * the close may continue, i.e. nothing was cancelled, no save failed and - * every listed document was checked. - */ -bool confirmClose(QWidget *parent, const QList> &docs); - -#endif // UNSAVEDDOCUMENTSDIALOG_H diff --git a/pglab/tuplesresultwidget.cpp b/pglab/tuplesresultwidget.cpp index f116d29..21b090a 100644 --- a/pglab/tuplesresultwidget.cpp +++ b/pglab/tuplesresultwidget.cpp @@ -21,6 +21,7 @@ TuplesResultWidget::TuplesResultWidget(QWidget *parent) : m_grid = new DataGridWidget(this); ui->verticalLayout->insertWidget(0, m_grid); + m_grid->view()->setFont(QFont("Source Sans Pro", 10)); m_grid->view()->setAlternatingRowColors(true); m_grid->view()->setWordWrap(false); m_grid->view()->verticalHeader()->setDefaultSectionSize(21); diff --git a/pglab/tuplesresultwidget.ui b/pglab/tuplesresultwidget.ui index 9acdd3d..b13909e 100644 --- a/pglab/tuplesresultwidget.ui +++ b/pglab/tuplesresultwidget.ui @@ -31,12 +31,6 @@ - - - 0 - 0 - - 4 diff --git a/pglab/widgets/SingleRecordModel.cpp b/pglab/widgets/SingleRecordModel.cpp new file mode 100644 index 0000000..18128e4 --- /dev/null +++ b/pglab/widgets/SingleRecordModel.cpp @@ -0,0 +1,61 @@ +#include "SingleRecordModel.h" +#include "CustomDataRole.h" + +SingleRecordModel::SingleRecordModel(QObject *parent) + : QAbstractTableModel(parent) +{ +} + +QVariant SingleRecordModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + return {}; +} + +int SingleRecordModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return elems.size(); +} + +int SingleRecordModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return ColCount; +} + +QVariant SingleRecordModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + const int row = index.row(); + if (role == Qt::DisplayRole) + { + switch (index.column()) + { + case NameCol: + return elems[row].name; + case ValueCol: + return elems[row].value; + } + } + else if (role == CustomDataTypeRole) + { + switch (index.column()) + { + case NameCol: + return Pgsql::text_oid; + case ValueCol: + return elems[row].oid; + } + } + else if (role == CustomDataMeaningRole) + { + } + + return QVariant(); +} diff --git a/pglab/widgets/SingleRecordModel.h b/pglab/widgets/SingleRecordModel.h new file mode 100644 index 0000000..8107508 --- /dev/null +++ b/pglab/widgets/SingleRecordModel.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include "Pgsql_oids.h" + +class SingleRecordModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit SingleRecordModel(QObject *parent = nullptr); + + // Header: + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // Basic functionality: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +private: + enum Columns { + NameCol, + ValueCol, + + ColCount + }; + + class Elem { + public: + QString name; + QString value; + Oid oid; + }; + using Elems = std::vector; + Elems elems; +}; + diff --git a/pglab/widgets/SingleRecordWidget.cpp b/pglab/widgets/SingleRecordWidget.cpp new file mode 100644 index 0000000..e617918 --- /dev/null +++ b/pglab/widgets/SingleRecordWidget.cpp @@ -0,0 +1,14 @@ +#include "SingleRecordWidget.h" +#include "ui_SingleRecordWidget.h" + +SingleRecordWidget::SingleRecordWidget(QWidget *parent) : + QWidget(parent), + ui(new Ui::SingleRecordWidget) +{ + ui->setupUi(this); +} + +SingleRecordWidget::~SingleRecordWidget() +{ + delete ui; +} diff --git a/pglab/widgets/SingleRecordWidget.h b/pglab/widgets/SingleRecordWidget.h new file mode 100644 index 0000000..29e4033 --- /dev/null +++ b/pglab/widgets/SingleRecordWidget.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace Ui { + class SingleRecordWidget; +} + +class SingleRecordWidget : public QWidget +{ + Q_OBJECT + +public: + explicit SingleRecordWidget(QWidget *parent = nullptr); + ~SingleRecordWidget(); + +private: + Ui::SingleRecordWidget *ui; +}; + diff --git a/pglab/widgets/SingleRecordWidget.ui b/pglab/widgets/SingleRecordWidget.ui new file mode 100644 index 0000000..bfc73fd --- /dev/null +++ b/pglab/widgets/SingleRecordWidget.ui @@ -0,0 +1,31 @@ + + + SingleRecordWidget + + + + 0 + 0 + 500 + 679 + + + + Form + + + + + + + + + + PgLabTableView + QTableView +
util/PgLabTableView.h
+
+
+ + +
diff --git a/pglablib/CMakeLists.txt b/pglablib/CMakeLists.txt index 689be13..0542f05 100644 --- a/pglablib/CMakeLists.txt +++ b/pglablib/CMakeLists.txt @@ -31,7 +31,6 @@ add_library(pglablib STATIC ui/catalog/tables/TableTreeBuilder.cpp util.cpp SqlFormattingUtils.cpp - SqlCompletionProvider.cpp catalog/PgKeywordList.cpp QueryGenerator.cpp ResultSourceMapping.cpp @@ -108,7 +107,6 @@ add_library(pglablib STATIC ui/catalog/tables/TableTreeBuilder.h util.h SqlFormattingUtils.h - SqlCompletionProvider.h catalog/PgCatalogTypes.h catalog/PgKeywordList.h QueryGenerator.h diff --git a/pglablib/SqlCompletionProvider.cpp b/pglablib/SqlCompletionProvider.cpp deleted file mode 100644 index 3569fae..0000000 --- a/pglablib/SqlCompletionProvider.cpp +++ /dev/null @@ -1,430 +0,0 @@ -#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; -} diff --git a/pglablib/SqlCompletionProvider.h b/pglablib/SqlCompletionProvider.h deleted file mode 100644 index 8a6ea18..0000000 --- a/pglablib/SqlCompletionProvider.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef SQLCOMPLETIONPROVIDER_H -#define SQLCOMPLETIONPROVIDER_H - -#include "SqlContext.h" -#include -#include -#include - -class PgDatabaseCatalog; - -struct CompletionItem { - QString display; // text shown in the list - QString detail; // dimmed extra information, for example the relation a column belongs to - QString insertText; // replaces the typed prefix when accepted - CompletionKind kind = CompletionKind::None; - bool ambiguous = false; // name occurs in more than one relation in scope -}; - -/// Plain snapshot of the parts of the catalog needed for completion, so the provider does not need a database. -struct CompletionRelation { - QString schema; - QString name; - char kind = 'r'; // pg_class.relkind - std::vector columns; // in column order, without dropped and system columns -}; - -struct CompletionFunction { - QString schema; - QString name; - char kind = 'f'; // pg_proc.prokind: f function, a aggregate, w window -}; - -struct CompletionForeignKey { - QString childSchema, childTable; // table that has the constraint - QString parentSchema, parentTable; // referenced table - std::vector childColumns; - std::vector parentColumns; // same length as childColumns -}; - -struct CompletionCatalog { - std::vector schemas; - std::vector relations; - std::vector functions; - std::vector foreignKeys; - /// Schemas that are searched when a relation is not schema qualified, in order - std::vector searchPath = { QStringLiteral("public"), QStringLiteral("pg_catalog") }; -}; - -CompletionCatalog buildCompletionCatalog(const PgDatabaseCatalog &catalog); - -class SqlCompletionProvider { -public: - SqlCompletionProvider() = default; - explicit SqlCompletionProvider(CompletionCatalog catalog); - - std::vector complete(const SqlContext &ctx) const; - -private: - CompletionCatalog m_catalog; - - const CompletionRelation* resolve(const RelationRef &ref) const; - std::vector relationItems(const SqlContext &ctx, const QString &schema) const; - std::vector columnItems(const SqlContext &ctx) const; - std::vector qualifiedColumnItems(const SqlContext &ctx) const; - std::vector joinItems(const SqlContext &ctx) const; - void addFunctionItems(const SqlContext &ctx, std::vector &items) const; -}; - -#endif // SQLCOMPLETIONPROVIDER_H diff --git a/pglablib/catalog/PgSequenceContainer.cpp b/pglablib/catalog/PgSequenceContainer.cpp index 0d3604f..5a31a6d 100644 --- a/pglablib/catalog/PgSequenceContainer.cpp +++ b/pglablib/catalog/PgSequenceContainer.cpp @@ -77,7 +77,7 @@ void PgSequenceContainer::loadAll(Pgsql::Connection &conn) q += ", start_value, increment_by, max_value, \n" " min_value, cache_value, is_cycled"; } - q += "\nFROM " + std::string(fqn.data(), static_cast(fqn.size())); + q += "\nFROM " + std::string(fqn.data(), static_cast(fqn.count())); auto && res = conn.query(q.c_str()); if (res.rows() == 1) { Pgsql::Row row(res, 0); diff --git a/pglablib/utils/PasswordManager.cpp b/pglablib/utils/PasswordManager.cpp index 7d7c073..bc553cb 100644 --- a/pglablib/utils/PasswordManager.cpp +++ b/pglablib/utils/PasswordManager.cpp @@ -267,9 +267,13 @@ KeyStrengthener PasswordManager::createKeyStrengthener() const std::string algo = "Scrypt"; auto pwh_fam = Botan::PasswordHashFamily::create(algo); return KeyStrengthener( + #ifdef _WIN32 pwh_fam->tune_params(key_size, 2000, 130), - salt, - key_size - ); + #else + pwh_fam->tune(key_size, std::chrono::seconds(2), 130), + #endif + salt, + key_size + ); } diff --git a/releasenotes/notes/datagrid-record-panel-6d2b9e4a1c7f8350.yaml b/releasenotes/notes/datagrid-record-panel-6d2b9e4a1c7f8350.yaml deleted file mode 100644 index da962b9..0000000 --- a/releasenotes/notes/datagrid-record-panel-6d2b9e4a1c7f8350.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -features: - - | - Data grids in the table editor and in query results now have a record - panel next to the grid. It shows the column name, type and value of the - current row, long text, varchar, json, jsonb and xml values get an - expandable multi-line box. Values are editable exactly when the grid cell - is editable. The panel can be hidden with the :menuselection:`Record` - button. diff --git a/releasenotes/notes/fix-ssl-mode-selection-45cc41625d61af90.yaml b/releasenotes/notes/fix-ssl-mode-selection-45cc41625d61af90.yaml deleted file mode 100644 index 391022b..0000000 --- a/releasenotes/notes/fix-ssl-mode-selection-45cc41625d61af90.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -fixes: - - | - Fixed SSL mode selection and saving in the connection configuration dialog. - The field now offers the supported modes, including the corrected verify-ca - and verify-full values, and a Default option to leave the mode unset. - Existing verify_ca and verify_full values are corrected when saved. diff --git a/releasenotes/notes/record-panel-compact-layout-8a5f1c3d7e2b6049.yaml b/releasenotes/notes/record-panel-compact-layout-8a5f1c3d7e2b6049.yaml deleted file mode 100644 index 8feb4a6..0000000 --- a/releasenotes/notes/record-panel-compact-layout-8a5f1c3d7e2b6049.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -fixes: - - | - Keep record panel fields compact at the top instead of spreading them - over the available height when a record has few fields. diff --git a/releasenotes/notes/sql-completion-8d3f1a6b2c4e7f90.yaml b/releasenotes/notes/sql-completion-8d3f1a6b2c4e7f90.yaml deleted file mode 100644 index dd66446..0000000 --- a/releasenotes/notes/sql-completion-8d3f1a6b2c4e7f90.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -features: - - | - The query editor now offers context aware completion of schema, table and column names, while typing or with Ctrl+Space. Names that need quoting are inserted quoted, and column names that exist in more than one table of the query are shown with their table or alias and inserted qualified. - - | - Completion also works in UPDATE, INSERT, DELETE and RETURNING, offers functions, and after JOIN proposes complete "table alias ON ..." clauses deduced from foreign keys, in both directions and for multi-column keys. diff --git a/releasenotes/notes/unsaved-docs-dialog-shared-docs-5c1e9a7d2b4f6e80.yaml b/releasenotes/notes/unsaved-docs-dialog-shared-docs-5c1e9a7d2b4f6e80.yaml deleted file mode 100644 index 9074dbc..0000000 --- a/releasenotes/notes/unsaved-docs-dialog-shared-docs-5c1e9a7d2b4f6e80.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -features: - - | - Closing a window or quitting the application now shows a single "Unsaved changes" dialog listing all modified SQL documents. Select the documents to save or discard; after the action has completed the dialog is shown again for the remaining documents, until all are handled or you cancel. - - | - A SQL document can be opened in another window with its own connection via the new "Open in other window / Duplicate in window..." action in the File menu. Both windows share the same text, and the document is only prompted for when its last editor closes. -fixes: - - | - Quitting the application from the connection manager no longer discards unsaved SQL changes silently. diff --git a/tests/PgsqlTests/tst_Value.cpp b/tests/PgsqlTests/tst_Value.cpp index 14e690f..6642b8e 100644 --- a/tests/PgsqlTests/tst_Value.cpp +++ b/tests/PgsqlTests/tst_Value.cpp @@ -2,7 +2,6 @@ #include #include "Pgsql_Value.h" #include "PrintTo_Qt.h" -#include #include #include #include @@ -30,7 +29,7 @@ TEST(Pgsql_Value, test_QDateTime_2) Pgsql::Value v("2017-12-01 09:38:17.339817+02", timestamptz_oid); QDateTime dt = (QDateTime)v; QDateTime exp(QDate(2017, 12, 1), QTime(9, 38, 17, 340), - QTimeZone::fromSecondsAheadOfUtc(2 * 3600)); + Qt::OffsetFromUTC, 2*3600); ASSERT_EQ(dt, exp); } diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index f4b3695..1284cdb 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -3,21 +3,13 @@ add_executable(IntegrationTests tst_Smoke.cpp tst_CatalogLoad.cpp tst_DatabaseSizes.cpp - tst_DataGridMapping.cpp - tst_DataGridUpdate.cpp - tst_CrudModelBehavior.cpp ) target_link_libraries(IntegrationTests PRIVATE pgintegration - pglabapp pglablib GTest::gtest Qt6::Core - Qt6::Gui - Qt6::Widgets - Qt6::Sql - Qt6::Concurrent ) target_compile_definitions(IntegrationTests PRIVATE diff --git a/tests/integration/DataGridTest.h b/tests/integration/DataGridTest.h deleted file mode 100644 index b1b6562..0000000 --- a/tests/integration/DataGridTest.h +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include - -#include "DbTest.h" -#include "ConnectionConfig.h" -#include "OpenDatabase.h" -#include "crud/CrudModel.h" -#include "crud/DataGridWidget.h" -#include "Pgsql_Connection.h" - -#include -#include -#include -#include "catalog/PgClassContainer.h" -#include "catalog/PgDatabaseCatalog.h" -#include -#include -#include -#include -#include - -class DataGridTest : public pgtest::DbTest { -protected: - std::shared_ptr openDb() - { - QUrl url(dbUrl()); - ConnectionConfig cfg; - cfg.setHost(url.host()); - cfg.setPort(static_cast(url.port(5432))); - cfg.setUser(url.userName()); - cfg.setPassword(url.password()); - cfg.setDbname(url.path().mid(1)); - return OpenDatabase::createOpenDatabase(cfg); - } - - /// Runs the query and shows its result in a grid - void showQuery(const char *sql) - { - db = openDb(); - auto result = std::make_shared(conn().query(sql)); - model = std::make_unique(); - widget = std::make_unique(); - widget->setSourceModel(model.get()); - model->setResult(db, result); - } - - /// Shows a whole table (CRUD mode) and waits for the asynchronous load to finish - bool showTable(const QString &name) - { - db = openDb(); - auto table = db->catalog()->classes()->findIf([&name](const PgClass &c) - { return c.fullyQualifiedQuotedObjectName().endsWith("." + name); }); - if (!table) - return false; - model = std::make_unique(); - widget = std::make_unique(); - widget->setSourceModel(model.get()); - bool reset = false; - QObject::connect(model.get(), &QAbstractItemModel::modelReset, model.get(), [&reset] { reset = true; }); - model->setConfig(db, *table); - QDeadlineTimer deadline(10000); - while (!reset && !deadline.hasExpired()) - QCoreApplication::processEvents(QEventLoop::AllEvents, 50); - return reset; - } - - /// Editable flag of every result column, as seen through the widget's proxy - std::vector editableColumns() - { - auto proxy = widget->proxy(); - std::vector r; - for (int c = 1; c < proxy->columnCount(); ++c) - r.push_back(proxy->flags(proxy->index(0, c)) & Qt::ItemIsEditable); - return r; - } - - QString columnName(int resultCol) - { - return widget->proxy()->headerData(resultCol + 1, Qt::Horizontal).toString().section('\n', 0, 0); - } - - /// Reads a single cell with the test's own connection - std::optional dbValue(const char *sql) - { - auto res = conn().query(sql); - auto v = res.get(0, 0); - if (v.null()) - return std::nullopt; - return std::string(v.c_str()); - } - - std::shared_ptr db; - std::unique_ptr model; - std::unique_ptr widget; -}; diff --git a/tests/integration/fixtures/schema.sql b/tests/integration/fixtures/schema.sql index 28c37a2..ee8554f 100644 --- a/tests/integration/fixtures/schema.sql +++ b/tests/integration/fixtures/schema.sql @@ -4,57 +4,3 @@ CREATE TABLE fixture_item ( ); INSERT INTO fixture_item (id, name) VALUES (1, 'one'), (2, 'two'); - -CREATE TABLE grid_parent ( - id integer PRIMARY KEY, - name text NOT NULL -); -INSERT INTO grid_parent (id, name) VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma'); - -CREATE TABLE grid_child ( - id integer PRIMARY KEY, - parent_id integer, - note text -); -INSERT INTO grid_child (id, parent_id, note) VALUES (10, 1, 'c1'), (11, 2, 'c2'), (12, 3, 'c3'); - -CREATE TABLE grid_composite ( - a integer, - b integer, - val text, - PRIMARY KEY (a, b) -); -INSERT INTO grid_composite (a, b, val) VALUES (1, 1, 'x'), (1, 2, 'y'), (2, 1, 'z'); - -CREATE TABLE grid_nopk ( - x integer, - y text -); -INSERT INTO grid_nopk (x, y) VALUES (1, 'a'), (2, 'b'); - -CREATE TABLE crud_item ( - id serial PRIMARY KEY, - name text NOT NULL, - qty integer, - flag boolean, - born date, - tag text DEFAULT 'dflt' -); -INSERT INTO crud_item (name, qty, flag, born) VALUES - ('a', 1, true, '2020-01-02'), - ('b', 2, false, NULL), - ('c', NULL, NULL, '2021-03-04'), - ('d', 4, true, NULL), - ('e', 5, false, NULL); - -CREATE TABLE crud_empty ( - id serial PRIMARY KEY, - v text -); - -CREATE TABLE crud_gen ( - id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - a integer, - b integer GENERATED ALWAYS AS (a * 2) STORED -); -INSERT INTO crud_gen (a) VALUES (1), (2); diff --git a/tests/integration/main.cpp b/tests/integration/main.cpp index e3214e3..60bed8d 100644 --- a/tests/integration/main.cpp +++ b/tests/integration/main.cpp @@ -1,15 +1,9 @@ #include -#include -#include - #include "PgTestEnvironment.h" int main(int argc, char *argv[]) { - if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) - qputenv("QT_QPA_PLATFORM", "offscreen"); - QApplication app(argc, argv); ::testing::InitGoogleTest(&argc, argv); ::testing::AddGlobalTestEnvironment(new pgtest::PgEnv(PGTEST_FIXTURE_DIR)); return RUN_ALL_TESTS(); diff --git a/tests/integration/tst_CrudModelBehavior.cpp b/tests/integration/tst_CrudModelBehavior.cpp deleted file mode 100644 index 76c7ed2..0000000 --- a/tests/integration/tst_CrudModelBehavior.cpp +++ /dev/null @@ -1,656 +0,0 @@ -// Characterization tests for CrudModel, they lock in the behavior of the model -// so it can be refactored safely. -#include "DataGridTest.h" - -#include "CustomDataRole.h" -#include "Pgsql_oids.h" - -#include -#include -#include -#include - -namespace { - -/// Failed saves show a modal message box, this closes it so the test can continue -void autoCloseModalDialogs(int triesLeft = 100) -{ - if (auto w = QApplication::activeModalWidget()) - w->close(); - else if (triesLeft > 0) - QTimer::singleShot(20, [triesLeft] { autoCloseModalDialogs(triesLeft - 1); }); -} - -/// Records the values of CrudModel::pendingChangesChanged -struct PendingSpy { - explicit PendingSpy(CrudModel *m) - { - QObject::connect(m, &CrudModel::pendingChangesChanged, m, [this](bool b) { values.push_back(b); }); - } - std::vector values; -}; - -/// Counts the emissions of a signal -struct CountSpy { - template - CountSpy(Sender *sender, Func signal) - { - QObject::connect(sender, signal, sender, [this] { ++n; }); - } - int count() const { return n; } - int n = 0; -}; - -// crud_item columns -constexpr int ColId = 0; -constexpr int ColName = 1; -constexpr int ColQty = 2; -constexpr int ColFlag = 3; -constexpr int ColBorn = 4; -constexpr int ColTag = 5; - -class CrudModelBehaviorTest : public DataGridTest { -protected: - QModelIndex idx(int row, int resultCol) const { return model->index(row, resultCol + 1); } - - QVariant cell(int row, int resultCol, int role = Qt::DisplayRole) const - { - return model->data(idx(row, resultCol), role); - } - - bool edit(int row, int resultCol, const QString &value) - { - return model->setData(idx(row, resultCol), value, Qt::EditRole); - } - - /// Saves while auto closing any error dialog - bool save() - { - QTimer::singleShot(0, [] { autoCloseModalDialogs(); }); - return model->saveChanges(); - } - - /// Row of crud_item with the given name, -1 if not shown - int rowOfName(const QString &name) const - { - for (int r = 0; r < model->rowCount(); ++r) - if (cell(r, ColName, Qt::EditRole).toString() == name) - return r; - return -1; - } - - QStringList names() const - { - QStringList l; - for (int r = 0; r < model->rowCount(); ++r) - l << cell(r, ColName, Qt::EditRole).toString(); - return l; - } - - void reload() - { - bool reset = false; - QObject::connect(model.get(), &QAbstractItemModel::modelReset, model.get(), [&reset] { reset = true; }); - model->loadData(); - QDeadlineTimer deadline(10000); - while (!reset && !deadline.hasExpired()) - QCoreApplication::processEvents(QEventLoop::AllEvents, 50); - ASSERT_TRUE(reset); - } -}; - -} // namespace - -// ---- loading, headers, row number column - -TEST_F(CrudModelBehaviorTest, WholeTableLoadsRowsPlusEmptyNewRow) -{ - ASSERT_TRUE(showTable("crud_item")); - EXPECT_EQ(model->rowCount(), 6); - EXPECT_EQ(model->columnCount(), 7); - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(names(), (QStringList{"a", "b", "c", "d", "e", ""})); - for (int c = 0; c <= ColTag; ++c) - EXPECT_FALSE(cell(5, c).isValid()) << c; -} - -TEST_F(CrudModelBehaviorTest, EmptyTableHasOnlyTheNewRow) -{ - ASSERT_TRUE(showTable("crud_empty")); - EXPECT_EQ(model->rowCount(), 1); -} - -TEST_F(CrudModelBehaviorTest, QueryResultHasNoNewRow) -{ - showQuery("SELECT id, name FROM grid_parent"); - EXPECT_EQ(model->rowCount(), 3); -} - -TEST_F(CrudModelBehaviorTest, HeaderShowsNameAndType) -{ - ASSERT_TRUE(showTable("crud_item")); - EXPECT_EQ(model->headerData(0, Qt::Horizontal).toString(), "#"); - for (int c = 0; c <= ColTag; ++c) - { - auto text = model->headerData(c + 1, Qt::Horizontal, Qt::DisplayRole).toString(); - EXPECT_EQ(text.count('\n'), 1) << c; - EXPECT_FALSE(text.section('\n', 1).isEmpty()) << c; - } - EXPECT_EQ(columnName(ColId), "id"); - EXPECT_EQ(columnName(ColName), "name"); - EXPECT_EQ(columnName(ColTag), "tag"); - EXPECT_FALSE(model->headerData(1, Qt::Horizontal, Qt::EditRole).isValid()); -} - -TEST_F(CrudModelBehaviorTest, RowNumberColumnData) -{ - ASSERT_TRUE(showTable("crud_item")); - for (int r = 0; r < model->rowCount(); ++r) - { - auto i = model->index(r, 0); - EXPECT_EQ(model->data(i, Qt::DisplayRole).toInt(), r + 1); - EXPECT_EQ(model->data(i, CustomSortRole).toInt(), r + 1); - EXPECT_EQ(model->data(i, CustomDataTypeRole).toUInt(), static_cast(Pgsql::int4_oid)); - EXPECT_FALSE(model->data(i, Qt::EditRole).isValid()); - EXPECT_FALSE(model->data(i, Qt::ForegroundRole).isValid()); - } - EXPECT_FALSE(model->setData(model->index(0, 0), "5", Qt::EditRole)); - EXPECT_FALSE(model->hasPendingChanges()); -} - -// ---- data roles - -TEST_F(CrudModelBehaviorTest, DisplayAndEditRoles) -{ - ASSERT_TRUE(showTable("crud_item")); - EXPECT_EQ(cell(0, ColName).toString(), "a"); - EXPECT_EQ(cell(0, ColQty).toString(), "1"); - EXPECT_EQ(cell(0, ColQty, Qt::EditRole).toString(), "1"); - EXPECT_EQ(cell(0, ColBorn).toString(), "2020-01-02"); - EXPECT_EQ(cell(0, ColTag).toString(), "dflt"); - // NULL - EXPECT_FALSE(cell(1, ColBorn).isValid()); - EXPECT_FALSE(cell(1, ColBorn, Qt::EditRole).isValid()); - // Roles that are not handled - EXPECT_FALSE(cell(0, ColName, Qt::ToolTipRole).isValid()); - EXPECT_FALSE(cell(0, ColName, Qt::ForegroundRole).isValid()); -} - -TEST_F(CrudModelBehaviorTest, BoolDisplaysAsBool) -{ - ASSERT_TRUE(showTable("crud_item")); - auto t = cell(0, ColFlag); - EXPECT_EQ(t.typeId(), QMetaType::Bool); - EXPECT_TRUE(t.toBool()); - auto f = cell(1, ColFlag); - EXPECT_EQ(f.typeId(), QMetaType::Bool); - EXPECT_FALSE(f.toBool()); - EXPECT_FALSE(cell(2, ColFlag).isValid()); - EXPECT_EQ(cell(0, ColFlag, Qt::EditRole).toString(), "t"); -} - -TEST_F(CrudModelBehaviorTest, DisplayTruncatesButEditDoesNot) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(0, ColName, QString(300, 'x'))); - EXPECT_EQ(cell(0, ColName, Qt::DisplayRole).toString().size(), 256); - EXPECT_EQ(cell(0, ColName, Qt::EditRole).toString().size(), 300); -} - -TEST_F(CrudModelBehaviorTest, SortRoleGivesTypedValues) -{ - ASSERT_TRUE(showTable("crud_item")); - auto i = cell(0, ColQty, CustomSortRole); - EXPECT_EQ(i.typeId(), QMetaType::Int); - EXPECT_EQ(i.toInt(), 1); - EXPECT_EQ(cell(0, ColBorn, CustomSortRole).toDate(), QDate(2020, 1, 2)); - EXPECT_EQ(cell(0, ColBorn, CustomSortRole).typeId(), QMetaType::QDate); - auto b = cell(0, ColFlag, CustomSortRole); - EXPECT_EQ(b.typeId(), QMetaType::Bool); - EXPECT_TRUE(b.toBool()); - EXPECT_EQ(cell(0, ColName, CustomSortRole).toString(), "a"); - EXPECT_FALSE(cell(1, ColBorn, CustomSortRole).isValid()); - EXPECT_FALSE(cell(2, ColQty, CustomSortRole).isValid()); -} - -TEST_F(CrudModelBehaviorTest, DataTypeRole) -{ - ASSERT_TRUE(showTable("crud_item")); - EXPECT_EQ(cell(0, ColId, CustomDataTypeRole).toUInt(), static_cast(Pgsql::int4_oid)); - EXPECT_EQ(cell(0, ColName, CustomDataTypeRole).toUInt(), static_cast(Pgsql::text_oid)); - EXPECT_EQ(cell(0, ColFlag, CustomDataTypeRole).toUInt(), static_cast(Pgsql::bool_oid)); - EXPECT_EQ(cell(0, ColBorn, CustomDataTypeRole).toUInt(), static_cast(Pgsql::date_oid)); - // also for the new row - EXPECT_EQ(cell(5, ColQty, CustomDataTypeRole).toUInt(), static_cast(Pgsql::int4_oid)); -} - -TEST_F(CrudModelBehaviorTest, ReadOnlyColumnsAreGrayInQueryMode) -{ - showQuery("SELECT id, name || 'x' AS e FROM grid_parent"); - EXPECT_FALSE(cell(0, 0, Qt::ForegroundRole).isValid()); - auto brush = cell(0, 1, Qt::ForegroundRole); - ASSERT_TRUE(brush.isValid()); - EXPECT_EQ(brush.value().color(), QColor(Qt::gray)); -} - -// ---- flags - -TEST_F(CrudModelBehaviorTest, FlagsOfGeneratedAndIdentityColumns) -{ - ASSERT_TRUE(showTable("crud_gen")); - EXPECT_EQ(editableColumns(), (std::vector{false, true, false})); - auto f = model->flags(model->index(0, 0)); - EXPECT_TRUE(f & Qt::ItemIsSelectable); - EXPECT_TRUE(f & Qt::ItemIsEnabled); - EXPECT_FALSE(f & Qt::ItemIsEditable); -} - -TEST_F(CrudModelBehaviorTest, FlagsOfWholeTable) -{ - ASSERT_TRUE(showTable("crud_item")); - EXPECT_EQ(editableColumns(), (std::vector(6, true))); -} - -TEST_F(CrudModelBehaviorTest, ExpressionColumnIsNotEditable) -{ - showQuery("SELECT id, name || 'x' AS e FROM grid_parent"); - EXPECT_EQ(editableColumns(), (std::vector{true, false})); -} - -TEST_F(CrudModelBehaviorTest, TableWithoutPkIsNotEditable) -{ - ASSERT_TRUE(showTable("grid_nopk")); - EXPECT_EQ(editableColumns(), (std::vector{false, false})); -} - -// ---- setData - -TEST_F(CrudModelBehaviorTest, SetDataRejectsOtherRoles) -{ - ASSERT_TRUE(showTable("crud_item")); - EXPECT_FALSE(model->setData(idx(0, ColName), "x", Qt::DisplayRole)); - EXPECT_FALSE(model->hasPendingChanges()); -} - -TEST_F(CrudModelBehaviorTest, SetDataShowsPendingValueAndEmitsDataChanged) -{ - ASSERT_TRUE(showTable("crud_item")); - CountSpy spy(model.get(), &QAbstractItemModel::dataChanged); - ASSERT_TRUE(edit(0, ColName, "changed")); - EXPECT_EQ(spy.count(), 1); - EXPECT_TRUE(model->hasPendingChanges()); - EXPECT_EQ(cell(0, ColName).toString(), "changed"); - EXPECT_EQ(cell(1, ColName).toString(), "b"); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 1"), "a"); -} - -TEST_F(CrudModelBehaviorTest, EmptyStringBecomesNullAndQuotesBecomeEmptyString) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(0, ColTag, "")); - ASSERT_TRUE(edit(1, ColTag, "''")); - EXPECT_FALSE(cell(0, ColTag).isValid()); - EXPECT_TRUE(cell(1, ColTag).isValid()); - EXPECT_EQ(cell(1, ColTag).toString(), ""); - ASSERT_TRUE(save()); - - EXPECT_EQ(dbValue("SELECT tag FROM crud_item WHERE name = 'a'"), std::nullopt); - EXPECT_EQ(dbValue("SELECT tag FROM crud_item WHERE name = 'b'"), ""); - EXPECT_EQ(dbValue("SELECT tag FROM crud_item WHERE name = 'c'"), "dflt"); -} - -TEST_F(CrudModelBehaviorTest, RepeatedEditsOfSameCellKeepLastValue) -{ - ASSERT_TRUE(showTable("crud_item")); - PendingSpy spy(model.get()); - ASSERT_TRUE(edit(0, ColName, "one")); - ASSERT_TRUE(edit(0, ColName, "two")); - ASSERT_TRUE(edit(1, ColName, "three")); - EXPECT_EQ(spy.values, (std::vector{true})); - ASSERT_TRUE(save()); - EXPECT_EQ(spy.values, (std::vector{true, false})); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 1"), "two"); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 2"), "three"); -} - -// ---- saving updates - -TEST_F(CrudModelBehaviorTest, SavedUpdateIsShownAfterwards) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(2, ColQty, "42")); - ASSERT_TRUE(save()); - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(cell(2, ColQty).toString(), "42"); - EXPECT_EQ(cell(2, ColName).toString(), "c"); - EXPECT_EQ(cell(2, ColBorn).toString(), "2021-03-04"); - EXPECT_EQ(model->rowCount(), 6); - EXPECT_EQ(dbValue("SELECT qty FROM crud_item WHERE name = 'c'"), "42"); -} - -TEST_F(CrudModelBehaviorTest, SaveWithoutChangesIsOk) -{ - ASSERT_TRUE(showTable("crud_item")); - PendingSpy spy(model.get()); - EXPECT_TRUE(model->saveChanges()); - EXPECT_TRUE(spy.values.empty()); -} - -TEST_F(CrudModelBehaviorTest, RevertThenEditAgain) -{ - ASSERT_TRUE(showTable("crud_item")); - PendingSpy spy(model.get()); - ASSERT_TRUE(edit(0, ColName, "discarded")); - model->revertChanges(); - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(cell(0, ColName).toString(), "a"); - model->revertChanges(); // nothing to revert, no signal - ASSERT_TRUE(edit(0, ColName, "kept")); - EXPECT_EQ(cell(0, ColName).toString(), "kept"); - ASSERT_TRUE(save()); - EXPECT_EQ(spy.values, (std::vector{true, false, true, false})); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 1"), "kept"); -} - -TEST_F(CrudModelBehaviorTest, RevertDoesNotRemoveSavedValues) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(0, ColName, "saved")); - ASSERT_TRUE(save()); - ASSERT_TRUE(edit(0, ColName, "unsaved")); - model->revertChanges(); - EXPECT_EQ(cell(0, ColName).toString(), "saved"); -} - -TEST_F(CrudModelBehaviorTest, PartialFailureKeepsOnlyFailingRowPending) -{ - ASSERT_TRUE(showTable("crud_item")); - PendingSpy spy(model.get()); - ASSERT_TRUE(edit(0, ColName, "ok")); - ASSERT_TRUE(edit(1, ColName, "")); // NULL violates NOT NULL - ASSERT_TRUE(edit(2, ColName, "never saved")); - EXPECT_FALSE(save()); - - EXPECT_TRUE(model->hasPendingChanges()); - EXPECT_EQ(spy.values, (std::vector{true})); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 1"), "ok"); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 2"), "b"); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 3"), "c"); - EXPECT_EQ(cell(0, ColName).toString(), "ok"); - EXPECT_FALSE(cell(1, ColName).isValid()); // still shows the pending value - - // Fixing the failing row lets the rest be saved - ASSERT_TRUE(edit(1, ColName, "fixed")); - ASSERT_TRUE(save()); - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(spy.values, (std::vector{true, false})); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 2"), "fixed"); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 3"), "never saved"); -} - -TEST_F(CrudModelBehaviorTest, UpdateOfRowChangedElsewhereFails) -{ - ASSERT_TRUE(showTable("crud_item")); - conn().query("DELETE FROM crud_item WHERE id = 1"); - ASSERT_TRUE(edit(0, ColName, "gone")); - EXPECT_FALSE(save()); - EXPECT_TRUE(model->hasPendingChanges()); -} - -// ---- reload - -TEST_F(CrudModelBehaviorTest, ReloadClearsPendingChangesAndShowsDbState) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(0, ColName, "pending")); - conn().query("INSERT INTO crud_item (name) VALUES ('f')"); - PendingSpy spy(model.get()); - reload(); - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(spy.values, (std::vector{false})); - EXPECT_EQ(model->rowCount(), 7); - EXPECT_EQ(names(), (QStringList{"a", "b", "c", "d", "e", "f", ""})); -} - -TEST_F(CrudModelBehaviorTest, ReloadWithoutPendingChangesEmitsNoSignal) -{ - ASSERT_TRUE(showTable("crud_item")); - PendingSpy spy(model.get()); - reload(); - EXPECT_TRUE(spy.values.empty()); - EXPECT_EQ(model->rowCount(), 6); -} - -// ---- insert - -TEST_F(CrudModelBehaviorTest, InsertNewRow) -{ - ASSERT_TRUE(showTable("crud_item")); - PendingSpy spy(model.get()); - ASSERT_TRUE(edit(5, ColName, "new")); - ASSERT_TRUE(edit(5, ColQty, "7")); - EXPECT_EQ(spy.values, (std::vector{true})); - ASSERT_TRUE(save()); - - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(spy.values, (std::vector{true, false})); - EXPECT_EQ(model->rowCount(), 7); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "6"); - EXPECT_EQ(dbValue("SELECT qty FROM crud_item WHERE name = 'new'"), "7"); - - // RETURNING * brings the defaults into the model - EXPECT_EQ(cell(5, ColName).toString(), "new"); - EXPECT_EQ(cell(5, ColQty).toString(), "7"); - EXPECT_EQ(cell(5, ColTag).toString(), "dflt"); - EXPECT_EQ(cell(5, ColId).toString(), *dbValue("SELECT id FROM crud_item WHERE name = 'new'")); - EXPECT_FALSE(cell(5, ColBorn).isValid()); - - // and a fresh empty row is appended - EXPECT_EQ(names(), (QStringList{"a", "b", "c", "d", "e", "new", ""})); - EXPECT_FALSE(cell(6, ColId).isValid()); -} - -TEST_F(CrudModelBehaviorTest, InsertTwoRowsInARow) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(5, ColName, "n1")); - ASSERT_TRUE(save()); - ASSERT_TRUE(edit(6, ColName, "n2")); - ASSERT_TRUE(save()); - EXPECT_EQ(model->rowCount(), 8); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "7"); - EXPECT_EQ(names(), (QStringList{"a", "b", "c", "d", "e", "n1", "n2", ""})); -} - -TEST_F(CrudModelBehaviorTest, InsertIntoEmptyTable) -{ - ASSERT_TRUE(showTable("crud_empty")); - ASSERT_TRUE(edit(0, 1, "first")); - ASSERT_TRUE(save()); - EXPECT_EQ(model->rowCount(), 2); - EXPECT_EQ(dbValue("SELECT v FROM crud_empty"), "first"); - EXPECT_EQ(cell(0, 0).toString(), "1"); -} - -TEST_F(CrudModelBehaviorTest, InsertWithExplicitNull) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(5, ColName, "nulltag")); - ASSERT_TRUE(edit(5, ColTag, "")); - ASSERT_TRUE(save()); - EXPECT_EQ(dbValue("SELECT tag FROM crud_item WHERE name = 'nulltag'"), std::nullopt); - EXPECT_FALSE(cell(5, ColTag).isValid()); -} - -TEST_F(CrudModelBehaviorTest, FailedInsertKeepsRowPending) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(5, ColQty, "9")); // name is NOT NULL and missing - EXPECT_FALSE(save()); - EXPECT_TRUE(model->hasPendingChanges()); - EXPECT_EQ(model->rowCount(), 6); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "5"); - EXPECT_EQ(cell(5, ColQty).toString(), "9"); - - ASSERT_TRUE(edit(5, ColName, "now valid")); - ASSERT_TRUE(save()); - EXPECT_EQ(model->rowCount(), 7); - EXPECT_EQ(dbValue("SELECT qty FROM crud_item WHERE name = 'now valid'"), "9"); -} - -TEST_F(CrudModelBehaviorTest, EditingOnlyThenRevertingNewRowDoesNotInsert) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(edit(5, ColName, "temp")); - model->revertChanges(); - EXPECT_FALSE(cell(5, ColName).isValid()); - EXPECT_EQ(model->rowCount(), 6); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "5"); -} - -// ---- delete - -TEST_F(CrudModelBehaviorTest, RemoveSingleRow) -{ - ASSERT_TRUE(showTable("crud_item")); - CountSpy spy(model.get(), &QAbstractItemModel::rowsRemoved); - auto [ok, msg] = model->removeRows({IntegerRange(1, 1)}); - EXPECT_TRUE(ok) << msg.toStdString(); - EXPECT_EQ(spy.count(), 1); - EXPECT_EQ(names(), (QStringList{"a", "c", "d", "e", ""})); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "4"); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item WHERE name = 'b'"), "0"); - EXPECT_EQ(model->data(model->index(4, 0), Qt::DisplayRole).toInt(), 5); -} - -TEST_F(CrudModelBehaviorTest, RemoveContiguousRange) -{ - ASSERT_TRUE(showTable("crud_item")); - auto [ok, msg] = model->removeRows({IntegerRange(1, 3)}); - EXPECT_TRUE(ok) << msg.toStdString(); - EXPECT_EQ(names(), (QStringList{"a", "e", ""})); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "2"); -} - -TEST_F(CrudModelBehaviorTest, RemoveNonContiguousRanges) -{ - ASSERT_TRUE(showTable("crud_item")); - auto [ok, msg] = model->removeRows({IntegerRange(0, 1), IntegerRange(2, 2)}); - EXPECT_TRUE(ok) << msg.toStdString(); - EXPECT_EQ(names(), (QStringList{"b", "e", ""})); - EXPECT_EQ(dbValue("SELECT string_agg(name, ',' ORDER BY name) FROM crud_item"), "b,e"); -} - -TEST_F(CrudModelBehaviorTest, RemoveRowsOverloadForViews) -{ - ASSERT_TRUE(showTable("crud_item")); - EXPECT_TRUE(model->removeRows(0, 2)); - EXPECT_EQ(names(), (QStringList{"c", "d", "e", ""})); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "3"); -} - -TEST_F(CrudModelBehaviorTest, RemoveNothingIsOk) -{ - ASSERT_TRUE(showTable("crud_item")); - auto [ok, msg] = model->removeRows({}); - EXPECT_TRUE(ok); - EXPECT_EQ(model->rowCount(), 6); -} - -TEST_F(CrudModelBehaviorTest, RemoveDropsPendingEditsOfRemovedRows) -{ - ASSERT_TRUE(showTable("crud_item")); - PendingSpy spy(model.get()); - ASSERT_TRUE(edit(1, ColName, "edited then removed")); - auto [ok, msg] = model->removeRows({IntegerRange(1, 1)}); - EXPECT_TRUE(ok); - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(spy.values, (std::vector{true, false})); -} - -TEST_F(CrudModelBehaviorTest, PendingEditsOfOtherRowsSurviveRemoval) -{ - ASSERT_TRUE(showTable("crud_item")); - PendingSpy spy(model.get()); - ASSERT_TRUE(edit(3, ColName, "d edited")); - auto [ok, msg] = model->removeRows({IntegerRange(0, 3)}); - EXPECT_TRUE(ok); - EXPECT_TRUE(model->hasPendingChanges()); - EXPECT_EQ(spy.values, (std::vector{true})); - EXPECT_EQ(names(), (QStringList{"d edited", "e", ""})); - - ASSERT_TRUE(save()); - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 4"), "d edited"); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 5"), "e"); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "2"); -} - -TEST_F(CrudModelBehaviorTest, EditAfterRemovalUpdatesTheRightRow) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(std::get<0>(model->removeRows({IntegerRange(0, 3)}))); - ASSERT_TRUE(edit(1, ColName, "e edited")); - ASSERT_TRUE(save()); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 5"), "e edited"); - EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 4"), "d"); -} - -TEST_F(CrudModelBehaviorTest, InsertAfterRemoval) -{ - ASSERT_TRUE(showTable("crud_item")); - ASSERT_TRUE(std::get<0>(model->removeRows({IntegerRange(0, 1)}))); - ASSERT_TRUE(edit(model->rowCount() - 1, ColName, "after delete")); - ASSERT_TRUE(save()); - EXPECT_EQ(names(), (QStringList{"b", "c", "d", "e", "after delete", ""})); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "5"); -} - -TEST_F(CrudModelBehaviorTest, RemoveOutOfRangeFails) -{ - ASSERT_TRUE(showTable("crud_item")); - auto [ok, msg] = model->removeRows({IntegerRange(5, 5)}); - EXPECT_FALSE(ok); - EXPECT_FALSE(msg.isEmpty()); - EXPECT_EQ(model->rowCount(), 6); - EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "5"); -} - -TEST_F(CrudModelBehaviorTest, RemoveFromQueryResultFails) -{ - showQuery("SELECT id, name FROM grid_parent"); - auto [ok, msg] = model->removeRows({IntegerRange(0, 1)}); - EXPECT_FALSE(ok); - EXPECT_FALSE(msg.isEmpty()); - EXPECT_EQ(model->rowCount(), 3); - EXPECT_EQ(dbValue("SELECT count(*) FROM grid_parent"), "3"); -} - -TEST_F(CrudModelBehaviorTest, RemoveFromTableWithoutPkFails) -{ - ASSERT_TRUE(showTable("grid_nopk")); - auto [ok, msg] = model->removeRows({IntegerRange(0, 1)}); - EXPECT_FALSE(ok); - EXPECT_FALSE(msg.isEmpty()); - EXPECT_EQ(model->rowCount(), 3); - EXPECT_EQ(dbValue("SELECT count(*) FROM grid_nopk"), "2"); -} - -TEST_F(CrudModelBehaviorTest, RemoveRowReferencedByForeignKeyFails) -{ - conn().query("ALTER TABLE grid_child ADD FOREIGN KEY (parent_id) REFERENCES grid_parent (id)"); - ASSERT_TRUE(showTable("grid_parent")); - auto [ok, msg] = model->removeRows({IntegerRange(0, 1)}); - EXPECT_FALSE(ok); - EXPECT_EQ(model->rowCount(), 4); - EXPECT_EQ(dbValue("SELECT count(*) FROM grid_parent"), "3"); -} - -TEST_F(CrudModelBehaviorTest, RemoveFromCompositePkTable) -{ - ASSERT_TRUE(showTable("grid_composite")); - auto [ok, msg] = model->removeRows({IntegerRange(1, 1)}); - EXPECT_TRUE(ok) << msg.toStdString(); - EXPECT_EQ(dbValue("SELECT count(*) FROM grid_composite"), "2"); - EXPECT_EQ(dbValue("SELECT count(*) FROM grid_composite WHERE a = 1 AND b = 2"), "0"); -} diff --git a/tests/integration/tst_DataGridMapping.cpp b/tests/integration/tst_DataGridMapping.cpp deleted file mode 100644 index d1dc58a..0000000 --- a/tests/integration/tst_DataGridMapping.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include - -#include "catalog/PgClassContainer.h" -#include "catalog/PgDatabaseCatalog.h" - -#include -#include -#include -#include - -#include "DataGridTest.h" - - -TEST_F(DataGridTest, SingleTableLoadsRows) -{ - showQuery("SELECT id, name FROM grid_parent ORDER BY id"); - EXPECT_EQ(model->rowCount(), 3); - ASSERT_NE(widget->proxy(), nullptr); - EXPECT_EQ(widget->proxy()->rowCount(), 3); - EXPECT_FALSE(model->hasPendingChanges()); -} - -TEST_F(DataGridTest, RowNumberColumnIsNeverEditable) -{ - showQuery("SELECT id, name FROM grid_parent"); - EXPECT_FALSE(widget->proxy()->flags(widget->proxy()->index(0, 0)) & Qt::ItemIsEditable); -} - -TEST_F(DataGridTest, SingleTableAllColumnsEditable) -{ - showQuery("SELECT id, name FROM grid_parent"); - EXPECT_EQ(editableColumns(), (std::vector{true, true})); - EXPECT_EQ(columnName(0), "id"); - EXPECT_EQ(columnName(1), "name"); -} - -TEST_F(DataGridTest, SingleTableSubsetWithPkIsEditable) -{ - showQuery("SELECT name, id FROM grid_parent"); - EXPECT_EQ(editableColumns(), (std::vector{true, true})); - EXPECT_EQ(columnName(0), "name"); -} - -TEST_F(DataGridTest, JoinOfTwoTablesWithPksIsEditable) -{ - showQuery("SELECT p.id, p.name, c.id AS child_id, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id"); - EXPECT_EQ(editableColumns(), (std::vector{true, true, true, true})); - EXPECT_EQ(columnName(2), "child_id"); -} - -TEST_F(DataGridTest, JoinWithoutOnePkOnlyOtherTableEditable) -{ - showQuery("SELECT p.id, p.name, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id"); - EXPECT_EQ(editableColumns(), (std::vector{true, true, false})); -} - -TEST_F(DataGridTest, AliasedColumnStaysEditable) -{ - showQuery("SELECT id, name AS title FROM grid_parent"); - EXPECT_EQ(editableColumns(), (std::vector{true, true})); - EXPECT_EQ(columnName(1), "title"); -} - -TEST_F(DataGridTest, ExpressionColumnIsReadOnly) -{ - showQuery("SELECT id, name, upper(name) AS shout, id + 1 AS next_id FROM grid_parent"); - EXPECT_EQ(editableColumns(), (std::vector{true, true, false, false})); -} - -TEST_F(DataGridTest, TableWithoutPkIsReadOnly) -{ - showQuery("SELECT x, y FROM grid_nopk"); - EXPECT_EQ(editableColumns(), (std::vector{false, false})); -} - -TEST_F(DataGridTest, PkNotSelectedIsReadOnly) -{ - showQuery("SELECT name FROM grid_parent"); - EXPECT_EQ(editableColumns(), (std::vector{false})); -} - -TEST_F(DataGridTest, CompositePkFullyPresentIsEditable) -{ - showQuery("SELECT a, b, val FROM grid_composite"); - EXPECT_EQ(editableColumns(), (std::vector{true, true, true})); -} - -TEST_F(DataGridTest, CompositePkPartiallyPresentIsReadOnly) -{ - showQuery("SELECT a, val FROM grid_composite"); - EXPECT_EQ(editableColumns(), (std::vector{false, false})); -} - -TEST_F(DataGridTest, WholeTableViaSetConfigLoadsAsynchronously) -{ - db = openDb(); - auto cls = db->catalog()->classes()->getByName("grid_parent"); - ASSERT_NE(cls, nullptr); - model = std::make_unique(); - widget = std::make_unique(); - widget->setSourceModel(model.get()); - model->setConfig(db, *cls); - model->loadData(); - - QDeadlineTimer deadline(10000); - while (model->rowCount() < 4 && !deadline.hasExpired()) - QCoreApplication::processEvents(QEventLoop::AllEvents, 50); - - // the whole table view appends an empty row for inserting - ASSERT_EQ(model->rowCount(), 4); - EXPECT_EQ(editableColumns(), (std::vector{true, true})); -} diff --git a/tests/integration/tst_DataGridUpdate.cpp b/tests/integration/tst_DataGridUpdate.cpp deleted file mode 100644 index e225abc..0000000 --- a/tests/integration/tst_DataGridUpdate.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#include "DataGridTest.h" - -#include -#include -#include -#include -#include - -namespace { - -/// Failed saves show a modal message box, this closes it so the test can continue -void autoCloseModalDialogs(int triesLeft = 100) -{ - if (auto w = QApplication::activeModalWidget()) - w->close(); - else if (triesLeft > 0) - QTimer::singleShot(20, [triesLeft] { autoCloseModalDialogs(triesLeft - 1); }); -} - -/// Records the values of CrudModel::pendingChangesChanged -struct PendingSpy { - explicit PendingSpy(CrudModel *m) - { - QObject::connect(m, &CrudModel::pendingChangesChanged, m, [this](bool b) { values.push_back(b); }); - } - std::vector values; -}; - -class DataGridUpdateTest : public DataGridTest { -protected: - void edit(int proxyRow, int resultCol, const QString &value) - { - auto proxy = widget->proxy(); - ASSERT_TRUE(proxy->setData(proxy->index(proxyRow, resultCol + 1), value, Qt::EditRole)); - } - - QAction *action(const QString &text) - { - for (auto a : widget->findChildren()) - if (a->text() == text) - return a; - return nullptr; - } -}; - -} // namespace - -TEST_F(DataGridUpdateTest, EditSingleCellAndSave) -{ - showQuery("SELECT id, name FROM grid_parent"); - edit(0, 1, "renamed"); - EXPECT_TRUE(model->hasPendingChanges()); - action("Save")->trigger(); - - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "renamed"); - EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 2"), "beta"); -} - -TEST_F(DataGridUpdateTest, EditSeveralColumnsAndRows) -{ - showQuery("SELECT id, parent_id, note FROM grid_child"); - edit(0, 1, "3"); - edit(0, 2, "first"); - edit(2, 2, "last"); - action("Save")->trigger(); - - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(dbValue("SELECT parent_id FROM grid_child WHERE id = 10"), "3"); - EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "first"); - EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), "c2"); - EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 12"), "last"); -} - -TEST_F(DataGridUpdateTest, EmptyValueIsSavedAsNull) -{ - showQuery("SELECT id, note FROM grid_child"); - edit(1, 1, ""); - action("Save")->trigger(); - - EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), std::nullopt); - EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "c1"); -} - -TEST_F(DataGridUpdateTest, JoinUpdatesTheOwningTable) -{ - showQuery("SELECT p.id, p.name, c.id AS child_id, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id"); - edit(0, 1, "parent edited"); - edit(0, 3, "child edited"); - action("Save")->trigger(); - - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "parent edited"); - EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "child edited"); - EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 2"), "beta"); - EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), "c2"); -} - -TEST_F(DataGridUpdateTest, CompositePkUpdatesOnlyMatchingRow) -{ - showQuery("SELECT a, b, val FROM grid_composite"); - edit(1, 2, "changed"); - action("Save")->trigger(); - - EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 1 AND b = 2"), "changed"); - EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 1 AND b = 1"), "x"); - EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 2 AND b = 1"), "z"); -} - -TEST_F(DataGridUpdateTest, EditAfterSortingUpdatesTheShownRow) -{ - showQuery("SELECT id, name FROM grid_parent"); - widget->proxy()->sort(2, Qt::DescendingOrder); - auto proxy = widget->proxy(); - ASSERT_EQ(proxy->index(0, 1).data(Qt::DisplayRole).toString(), "3"); - ASSERT_EQ(proxy->index(0, 2).data(Qt::DisplayRole).toString(), "gamma"); - - edit(0, 1, "sorted edit"); - action("Save")->trigger(); - - EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 3"), "sorted edit"); - EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha"); -} - -TEST_F(DataGridUpdateTest, SaveAndRevertFollowPendingChanges) -{ - showQuery("SELECT id, name FROM grid_parent"); - auto save = action("Save"); - auto revert = action("Revert"); - ASSERT_NE(save, nullptr); - ASSERT_NE(revert, nullptr); - EXPECT_FALSE(save->isEnabled()); - EXPECT_FALSE(revert->isEnabled()); - - PendingSpy spy(model.get()); - edit(0, 1, "x"); - EXPECT_TRUE(save->isEnabled()); - EXPECT_TRUE(revert->isEnabled()); - EXPECT_EQ(spy.values, (std::vector{true})); - - save->trigger(); - EXPECT_FALSE(save->isEnabled()); - EXPECT_FALSE(revert->isEnabled()); - EXPECT_EQ(spy.values, (std::vector{true, false})); -} - -TEST_F(DataGridUpdateTest, RevertDiscardsEdits) -{ - showQuery("SELECT id, name FROM grid_parent"); - edit(0, 1, "discard me"); - EXPECT_EQ(widget->proxy()->index(0, 2).data(Qt::EditRole).toString(), "discard me"); - - PendingSpy spy(model.get()); - action("Revert")->trigger(); - - EXPECT_FALSE(model->hasPendingChanges()); - EXPECT_FALSE(action("Save")->isEnabled()); - EXPECT_EQ(spy.values, (std::vector{false})); - EXPECT_EQ(widget->proxy()->index(0, 2).data(Qt::EditRole).toString(), "alpha"); - EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha"); -} - -TEST_F(DataGridUpdateTest, FailedSaveKeepsRowPending) -{ - showQuery("SELECT id, name FROM grid_parent"); - edit(0, 1, ""); // NULL violates NOT NULL on name - QTimer::singleShot(0, [] { autoCloseModalDialogs(); }); - action("Save")->trigger(); - - EXPECT_TRUE(model->hasPendingChanges()); - EXPECT_TRUE(action("Save")->isEnabled()); - EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha"); -} - -TEST_F(DataGridUpdateTest, ToolbarHiddenForNonCrudModel) -{ - showQuery("SELECT id, name FROM grid_parent"); - auto toolbar = widget->findChild(); - ASSERT_NE(toolbar, nullptr); - EXPECT_FALSE(toolbar->isHidden()); - - QStandardItemModel other(1, 1); - widget->setSourceModel(&other); - EXPECT_TRUE(toolbar->isHidden()); - EXPECT_FALSE(action("Save")->isEnabled()); - - widget->setSourceModel(model.get()); - EXPECT_FALSE(toolbar->isHidden()); - widget->setSourceModel(nullptr); -} diff --git a/tests/pglabtests/CMakeLists.txt b/tests/pglabtests/CMakeLists.txt index d74d768..d9f30de 100644 --- a/tests/pglabtests/CMakeLists.txt +++ b/tests/pglabtests/CMakeLists.txt @@ -9,8 +9,6 @@ add_executable(pglabtests tst_escapeConnectionStringValue.cpp tst_expected.cpp tst_SqlLexer.cpp - tst_SqlContext.cpp - tst_SqlCompletionProvider.cpp tst_scopeguard.cpp tst_CsvWriter.cpp tst_PasswordManager.cpp @@ -20,13 +18,9 @@ add_executable(pglabtests tst_TypeMappings.cpp tst_Range.cpp ResultSourceMappingTest.cpp - tst_RecordPanel.cpp - tst_SslModeModel.cpp - tst_ConnectionConfigurationWidget.cpp ) target_link_libraries(pglabtests PRIVATE - pglabapp pglablib pgsql core @@ -38,8 +32,6 @@ target_link_libraries(pglabtests PRIVATE target_include_directories(pglabtests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_SOURCE_DIR}/pglab - ${CMAKE_SOURCE_DIR}/pglab/crud ) set_target_properties(pglabtests PROPERTIES diff --git a/tests/pglabtests/main.cpp b/tests/pglabtests/main.cpp index 5755f0d..291bdee 100644 --- a/tests/pglabtests/main.cpp +++ b/tests/pglabtests/main.cpp @@ -1,13 +1,7 @@ -#include - -#include -#include +#include int main(int argc, char *argv[]) { - if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) - qputenv("QT_QPA_PLATFORM", "offscreen"); - QApplication app(argc, argv); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/tests/pglabtests/tst_ConnectionConfigurationWidget.cpp b/tests/pglabtests/tst_ConnectionConfigurationWidget.cpp deleted file mode 100644 index 32eb0fd..0000000 --- a/tests/pglabtests/tst_ConnectionConfigurationWidget.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include - -#include "ConnectionConfig.h" -#include "ConnectionConfigurationWidget.h" -#include "ConnectionController.h" -#include "PrintTo_Qt.h" - -class ConnectionConfigurationWidgetTest : public ::testing::Test { -protected: - // SSL settings do not need the controller's database or connection tree initialized. - ConnectionController controller; -}; - -TEST_F(ConnectionConfigurationWidgetTest, SelectedSslModesRoundTripThroughTheWidget) -{ - ConnectionConfigurationWidget widget(&controller); - widget.setData(ConnectionConfig()); - auto combo = widget.findChild("cmbbxSsl"); - ASSERT_NE(combo, nullptr); - EXPECT_FALSE(combo->isEditable()); - - const QStringList modes = {"disable", "allow", "prefer", "require", "verify-ca", "verify-full"}; - for (const auto &mode : modes) { - SCOPED_TRACE(mode.toStdString()); - auto index = combo->findData(mode); - ASSERT_GT(index, 0); - combo->setCurrentIndex(index); - EXPECT_EQ(combo->currentText(), mode); - - auto saved = widget.data(); - EXPECT_EQ(saved.getParameter("sslmode"), mode); - ConnectionConfigurationWidget reopened(&controller); - reopened.setData(saved); - EXPECT_EQ(reopened.data().getParameter("sslmode"), mode); - EXPECT_EQ(reopened.findChild("cmbbxSsl")->currentText(), mode); - } - - combo->setCurrentIndex(combo->findData(QString())); - EXPECT_TRUE(widget.data().getParameter("sslmode").isEmpty()); - EXPECT_EQ(widget.data().getParameters().count("sslmode"), 0); -} - -TEST_F(ConnectionConfigurationWidgetTest, LegacyVerificationModesAreCanonicalized) -{ - ConnectionConfigurationWidget widget(&controller); - for (const auto &mode : {QStringLiteral("verify_ca"), QStringLiteral("verify_full")}) { - ConnectionConfig config; - config.setParameter("sslmode", mode); - widget.setData(config); - - auto canonical = QString(mode).replace('_', '-'); - EXPECT_EQ(widget.findChild("cmbbxSsl")->currentText(), canonical); - EXPECT_EQ(widget.data().getParameter("sslmode"), canonical); - EXPECT_EQ(config.getParameter("sslmode"), mode); - } -} - -TEST_F(ConnectionConfigurationWidgetTest, MissingOrUnknownModesResetToDefault) -{ - ConnectionConfigurationWidget widget(&controller); - for (const auto &mode : {QString(), QStringLiteral("unknown")}) { - ConnectionConfig config; - config.setParameter("sslmode", "require"); - widget.setData(config); - config.setParameter("sslmode", mode); - widget.setData(config); - - EXPECT_EQ(widget.findChild("cmbbxSsl")->currentText(), QStringLiteral("Default")); - EXPECT_TRUE(widget.data().getParameter("sslmode").isEmpty()); - } -} - -TEST_F(ConnectionConfigurationWidgetTest, DialogOnlySavesOnAccept) -{ - ConnectionConfig config; - config.setParameter("sslmode", "allow"); - for (bool accept : {false, true}) { - bool saved = false; - ConnectionConfigurationWidget::editExistingInWindow(&controller, config, - [&](ConnectionConfigurationWidget &widget) { - saved = true; - EXPECT_EQ(widget.data().getParameter("sslmode"), QStringLiteral("require")); - }); - - QDialog *dialog = nullptr; - for (auto window : QApplication::topLevelWidgets()) { - if (auto candidate = qobject_cast(window); - candidate && candidate->findChild()) { - dialog = candidate; - break; - } - } - ASSERT_NE(dialog, nullptr); - auto combo = dialog->findChild("cmbbxSsl"); - ASSERT_NE(combo, nullptr); - combo->setCurrentIndex(combo->findData(QStringLiteral("require"))); - auto buttons = dialog->findChild(); - ASSERT_NE(buttons, nullptr); - buttons->button(accept ? QDialogButtonBox::Ok : QDialogButtonBox::Cancel)->click(); - EXPECT_EQ(saved, accept); - EXPECT_EQ(config.getParameter("sslmode"), QStringLiteral("allow")); - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - } -} diff --git a/tests/pglabtests/tst_RecordPanel.cpp b/tests/pglabtests/tst_RecordPanel.cpp deleted file mode 100644 index 44d7ffc..0000000 --- a/tests/pglabtests/tst_RecordPanel.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include "CustomDataRole.h" -#include "Pgsql_oids.h" -#include "RecordPanel.h" - -namespace { - -/// Column 0 is the row number column, columns 1 to 3 are data columns -/// id (int4, editable), body (text, editable), ro (int4, read-only) -void fillModel(QStandardItemModel &model) -{ - model.setColumnCount(4); - model.setRowCount(1); - model.setHeaderData(0, Qt::Horizontal, QStringLiteral("#"), Qt::DisplayRole); - model.setHeaderData(1, Qt::Horizontal, QStringLiteral("id\nint4"), Qt::DisplayRole); - model.setHeaderData(2, Qt::Horizontal, QStringLiteral("body\ntext"), Qt::DisplayRole); - model.setHeaderData(3, Qt::Horizontal, QStringLiteral("ro\nint4"), Qt::DisplayRole); - model.setData(model.index(0, 1), Pgsql::int4_oid, CustomDataTypeRole); - model.setData(model.index(0, 2), Pgsql::text_oid, CustomDataTypeRole); - model.setData(model.index(0, 3), Pgsql::int4_oid, CustomDataTypeRole); - model.setData(model.index(0, 1), QStringLiteral("1")); - model.setData(model.index(0, 2), QStringLiteral("hello")); - model.setData(model.index(0, 3), QStringLiteral("7")); - model.itemFromIndex(model.index(0, 3))->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); -} - -} - -class RecordPanelTest : public ::testing::Test { -protected: - void SetUp() override - { - model.reset(new QStandardItemModel); - fillModel(*model); - panel.setModel(model.get()); - panel.setFirstDataColumn(1); - panel.setCurrentRow(0); - } - - std::unique_ptr model; - RecordPanel panel; -}; - -TEST_F(RecordPanelTest, OneFieldPerDataColumnSkippingRowNumber) -{ - ASSERT_NE(panel.widget(), nullptr); - EXPECT_EQ(panel.widget()->findChildren().size(), 6); - EXPECT_EQ(panel.widget()->findChildren().size(), 1); - EXPECT_EQ(panel.widget()->findChildren().size(), 2); -} - -TEST_F(RecordPanelTest, ShowsValuesOfCurrentRow) -{ - auto lines = panel.widget()->findChildren(); - auto texts = panel.widget()->findChildren(); - ASSERT_EQ(lines.size(), 2); - ASSERT_EQ(texts.size(), 1); - EXPECT_EQ(lines[0]->text(), QStringLiteral("1")); - EXPECT_EQ(texts[0]->toPlainText(), QStringLiteral("hello")); - EXPECT_EQ(lines[1]->text(), QStringLiteral("7")); -} - -TEST_F(RecordPanelTest, ReadOnlyWhenCellNotEditable) -{ - auto lines = panel.widget()->findChildren(); - ASSERT_EQ(lines.size(), 2); - EXPECT_FALSE(lines[0]->isReadOnly()); - EXPECT_TRUE(lines[1]->isReadOnly()); -} - -TEST_F(RecordPanelTest, NullShowsEmptyTextWithNullPlaceholder) -{ - model->setData(model->index(0, 1), QVariant(), Qt::EditRole); - panel.setCurrentRow(0); - - auto lines = panel.widget()->findChildren(); - ASSERT_EQ(lines.size(), 2); - EXPECT_TRUE(lines[0]->text().isEmpty()); - EXPECT_EQ(lines[0]->placeholderText(), QStringLiteral("null")); -} - -TEST_F(RecordPanelTest, EmptyStringShownAsQuotedEmpty) -{ - model->setData(model->index(0, 2), QString(""), Qt::EditRole); - panel.setCurrentRow(0); - - auto texts = panel.widget()->findChildren(); - ASSERT_EQ(texts.size(), 1); - EXPECT_EQ(texts[0]->toPlainText(), QStringLiteral("''")); -} - -TEST_F(RecordPanelTest, EditingFinishedWritesThroughModel) -{ - auto lines = panel.widget()->findChildren(); - ASSERT_EQ(lines.size(), 2); - lines[0]->setText(QStringLiteral("42")); - emit lines[0]->editingFinished(); - - EXPECT_EQ(model->index(0, 1).data(Qt::EditRole).toString(), QStringLiteral("42")); -} - -TEST_F(RecordPanelTest, DataChangedRefreshesEditor) -{ - model->setData(model->index(0, 1), QStringLiteral("5"), Qt::EditRole); - - auto lines = panel.widget()->findChildren(); - ASSERT_EQ(lines.size(), 2); - EXPECT_EQ(lines[0]->text(), QStringLiteral("5")); -} - -TEST_F(RecordPanelTest, NoCurrentRowShowsNothing) -{ - panel.setCurrentRow(-1); - EXPECT_EQ(panel.widget(), nullptr); -} - -TEST_F(RecordPanelTest, ModelResetClearsPanel) -{ - model->clear(); - EXPECT_EQ(panel.widget(), nullptr); -} - -TEST_F(RecordPanelTest, ExpandButtonGrowsTextBox) -{ - auto texts = panel.widget()->findChildren(); - auto buttons = panel.widget()->findChildren(); - ASSERT_EQ(texts.size(), 1); - ASSERT_EQ(buttons.size(), 1); - - int collapsed = texts[0]->maximumHeight(); - buttons[0]->setChecked(true); - int expanded = texts[0]->maximumHeight(); - EXPECT_GT(expanded, collapsed); - - buttons[0]->setChecked(false); - EXPECT_EQ(texts[0]->maximumHeight(), collapsed); -} diff --git a/tests/pglabtests/tst_SqlCompletionProvider.cpp b/tests/pglabtests/tst_SqlCompletionProvider.cpp deleted file mode 100644 index 8e7cfb7..0000000 --- a/tests/pglabtests/tst_SqlCompletionProvider.cpp +++ /dev/null @@ -1,237 +0,0 @@ -#include -#include "SqlCompletionProvider.h" -#include "PrintTo_Qt.h" - -using namespace testing; - -namespace { - -CompletionCatalog testCatalog() -{ - CompletionCatalog c; - c.schemas = { "public", "sales" }; - c.relations = { - { "public", "customers", 'r', { "id", "name", "select" } }, - { "public", "orders", 'r', { "id", "customer_id", "Total" } }, - { "public", "order_view", 'v', { "id" } }, - { "sales", "invoices", 'r', { "inv_id" } }, - { "public", "My Table", 'r', { "a" } }, - }; - return c; -} - -std::vector complete(QString text) -{ - int pos = text.indexOf('|'); - text.remove(pos, 1); - SqlCompletionProvider p(testCatalog()); - return p.complete(analyzeSqlContext(text, pos)); -} - -const CompletionItem* find(const std::vector &items, const QString &display, const QString &detail = QString()) -{ - for (auto &i : items) - if (i.display == display && (detail.isEmpty() || i.detail == detail)) - return &i; - return nullptr; -} - -} // namespace - -TEST(SqlCompletionProvider, relationsAndSchemasAfterFrom) -{ - auto items = complete("SELECT * FROM |"); - ASSERT_TRUE(find(items, "sales")); - EXPECT_EQ(find(items, "sales")->kind, CompletionKind::Schema); - ASSERT_TRUE(find(items, "customers")); - EXPECT_EQ(find(items, "customers")->insertText, "customers"); - EXPECT_FALSE(find(items, "invoices")); -} - -TEST(SqlCompletionProvider, relationNeedingQuotesIsQuoted) -{ - auto items = complete("SELECT * FROM my|"); - // prefix match is case-insensitive - ASSERT_TRUE(find(items, "My Table")); - EXPECT_EQ(find(items, "My Table")->insertText, "\"My Table\""); -} - -TEST(SqlCompletionProvider, relationsOfSchema) -{ - auto items = complete("SELECT * FROM sales.|"); - ASSERT_EQ(items.size(), 1u); - EXPECT_EQ(items[0].display, "invoices"); -} - -TEST(SqlCompletionProvider, prefixFilters) -{ - auto items = complete("SELECT * FROM ord|"); - EXPECT_TRUE(find(items, "orders")); - EXPECT_TRUE(find(items, "order_view")); - EXPECT_FALSE(find(items, "customers")); -} - -TEST(SqlCompletionProvider, reservedWordAndMixedCaseColumnsAreQuoted) -{ - auto items = complete("SELECT | FROM customers, orders o"); - ASSERT_TRUE(find(items, "select")); - EXPECT_EQ(find(items, "select")->insertText, "\"select\""); - ASSERT_TRUE(find(items, "Total")); - EXPECT_EQ(find(items, "Total")->insertText, "\"Total\""); - ASSERT_TRUE(find(items, "name")); - EXPECT_EQ(find(items, "name")->insertText, "name"); -} - -TEST(SqlCompletionProvider, ambiguousColumnsAreQualified) -{ - auto items = complete("SELECT | FROM customers, orders o"); - auto c = find(items, "id", "customers"); - auto o = find(items, "id", "o"); - ASSERT_TRUE(c); - ASSERT_TRUE(o); - EXPECT_TRUE(c->ambiguous); - EXPECT_TRUE(o->ambiguous); - EXPECT_EQ(c->insertText, "customers.id"); - EXPECT_EQ(o->insertText, "o.id"); - EXPECT_FALSE(find(items, "customer_id")->ambiguous); -} - -TEST(SqlCompletionProvider, qualifiedByAlias) -{ - auto items = complete("SELECT o.| FROM customers c, orders o"); - ASSERT_EQ(items.size(), 3u); - EXPECT_EQ(items[1].display, "customer_id"); - EXPECT_EQ(items[0].insertText, "id"); -} - -TEST(SqlCompletionProvider, qualifiedByTableName) -{ - auto items = complete("SELECT customers.na| FROM customers"); - ASSERT_EQ(items.size(), 1u); - EXPECT_EQ(items[0].display, "name"); -} - -TEST(SqlCompletionProvider, quotedPrefixIsCaseSensitive) -{ - auto items = complete("SELECT \"To| FROM orders"); - ASSERT_EQ(items.size(), 1u); - EXPECT_EQ(items[0].insertText, "\"Total\""); -} - -TEST(SqlCompletionProvider, unknownRelationGivesNoColumns) -{ - auto items = complete("SELECT | FROM nonexistent"); - EXPECT_TRUE(items.empty() || !find(items, "id")); -} - -TEST(SqlCompletionProvider, qualifierWithoutMatchIsSchema) -{ - auto items = complete("SELECT sales.| FROM customers"); - ASSERT_EQ(items.size(), 1u); - EXPECT_EQ(items[0].display, "invoices"); -} - -TEST(SqlCompletionProvider, insertColumnListOnlyTargetColumns) -{ - auto items = complete("INSERT INTO orders (|"); - ASSERT_EQ(items.size(), 4u); // 3 columns + the relation name - EXPECT_TRUE(find(items, "customer_id")); -} - -TEST(SqlCompletionProvider, updateSetColumns) -{ - auto items = complete("UPDATE customers SET na| = 1"); - ASSERT_TRUE(find(items, "name")); -} - -TEST(SqlCompletionProvider, functionsOnlyWithPrefix) -{ - CompletionCatalog c = testCatalog(); - c.functions = { { "pg_catalog", "count", 'a' }, { "pg_catalog", "coalesce", 'f' }, { "other", "cosmic", 'f' }, { "pg_catalog", "count", 'f' } }; - SqlCompletionProvider p(c); - auto run = [&](QString text) { - int pos = text.indexOf('|'); - text.remove(pos, 1); - return p.complete(analyzeSqlContext(text, pos)); - }; - auto items = run("SELECT co| FROM customers"); - ASSERT_TRUE(find(items, "count")); - EXPECT_EQ(find(items, "count")->insertText, "count("); - EXPECT_EQ(find(items, "count")->kind, CompletionKind::Function); - EXPECT_TRUE(find(items, "coalesce")); - EXPECT_FALSE(find(items, "cosmic")); - EXPECT_FALSE(find(run("SELECT | FROM customers"), "count")); - EXPECT_FALSE(find(run("INSERT INTO customers (co|"), "count")); -} - -namespace { - -std::vector completeJoin(QString text) -{ - CompletionCatalog c = testCatalog(); - c.relations.push_back({ "public", "order_lines", 'r', { "order_id", "line_no", "sku" } }); - c.relations.push_back({ "public", "employees", 'r', { "id", "boss_id" } }); - c.foreignKeys = { - { "public", "orders", "public", "customers", { "customer_id" }, { "id" } }, - { "public", "order_lines", "public", "orders", { "order_id", "line_no" }, { "id", "line_no" } }, - { "sales", "invoices", "public", "customers", { "inv_id" }, { "id" } }, - { "public", "employees", "public", "employees", { "boss_id" }, { "id" } }, - }; - int pos = text.indexOf('|'); - text.remove(pos, 1); - SqlCompletionProvider p(c); - return p.complete(analyzeSqlContext(text, pos)); -} - -} // namespace - -TEST(SqlCompletionProvider, joinFromChildToParent) -{ - auto items = completeJoin("SELECT * FROM orders o JOIN |"); - auto *item = find(items, "customers c"); - ASSERT_TRUE(item); - EXPECT_EQ(item->kind, CompletionKind::JoinTarget); - EXPECT_EQ(item->insertText, "customers c ON o.customer_id = c.id"); - // plain relations are still offered - bool plain = false; - for (auto &i : items) - plain |= i.kind == CompletionKind::Relation && i.display == "customers"; - EXPECT_TRUE(plain); -} - -TEST(SqlCompletionProvider, joinFromParentToChildMultiColumn) -{ - auto items = completeJoin("SELECT * FROM orders JOIN order_l|"); - auto *item = find(items, "order_lines ol"); - ASSERT_TRUE(item); - EXPECT_EQ(item->insertText, "order_lines ol ON orders.id = ol.order_id AND orders.line_no = ol.line_no"); -} - -TEST(SqlCompletionProvider, joinAliasAvoidsClash) -{ - auto items = completeJoin("SELECT * FROM orders c JOIN |"); - auto *item = find(items, "customers c2"); - ASSERT_TRUE(item); - EXPECT_EQ(item->insertText, "customers c2 ON c.customer_id = c2.id"); -} - -TEST(SqlCompletionProvider, joinOtherSchemaQualified) -{ - auto items = completeJoin("SELECT * FROM customers JOIN |"); - auto *item = find(items, "invoices i"); - ASSERT_TRUE(item); - EXPECT_EQ(item->insertText, "sales.invoices i ON customers.id = i.inv_id"); -} - -TEST(SqlCompletionProvider, joinSelfReference) -{ - auto items = completeJoin("SELECT * FROM employees e JOIN |"); - auto *item = find(items, "employees e2"); - ASSERT_TRUE(item); - // both directions give a different condition - int count = 0; - for (auto &i : items) - if (i.kind == CompletionKind::JoinTarget && i.display == "employees e2") - ++count; - EXPECT_EQ(count, 2); -} diff --git a/tests/pglabtests/tst_SqlContext.cpp b/tests/pglabtests/tst_SqlContext.cpp deleted file mode 100644 index dab58bc..0000000 --- a/tests/pglabtests/tst_SqlContext.cpp +++ /dev/null @@ -1,271 +0,0 @@ -#include -#include "SqlContext.h" -#include "PrintTo_Qt.h" - -using namespace testing; - -namespace { - -// '|' in the text marks the cursor -SqlContext analyze(QString text) -{ - int pos = text.indexOf('|'); - text.remove(pos, 1); - return analyzeSqlContext(text, pos); -} - -QStringList names(const SqlContext &ctx) -{ - QStringList l; - for (auto &r : ctx.relations) - l << (r.schema.isEmpty() ? r.name : r.schema + "." + r.name) + (r.alias.isEmpty() ? "" : "/" + r.alias); - return l; -} - -} // namespace - -TEST(SqlContext, emptyDocument) -{ - auto ctx = analyzeSqlContext("", 0); - EXPECT_EQ(ctx.kind, CompletionKind::None); -} - -TEST(SqlContext, selectListColumnWithFromAfterCursor) -{ - auto ctx = analyze("SELECT | FROM t"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(ctx.clause, SqlClause::Select); - EXPECT_EQ(names(ctx), QStringList({ "t" })); - EXPECT_TRUE(ctx.prefix.isEmpty()); -} - -TEST(SqlContext, prefixIsReplaceRange) -{ - auto ctx = analyze("SELECT na| FROM t"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(ctx.prefix, "na"); - EXPECT_EQ(ctx.prefixStart, 7); - EXPECT_EQ(ctx.prefixLen, 2); - EXPECT_EQ(names(ctx), QStringList({ "t" })); -} - -TEST(SqlContext, relationAfterFrom) -{ - auto ctx = analyze("SELECT * FROM us|"); - EXPECT_EQ(ctx.kind, CompletionKind::Relation); - EXPECT_EQ(ctx.prefix, "us"); - EXPECT_TRUE(ctx.relations.empty()); -} - -TEST(SqlContext, relationAfterSchemaDot) -{ - auto ctx = analyze("SELECT * FROM sch.|"); - EXPECT_EQ(ctx.kind, CompletionKind::Relation); - EXPECT_EQ(ctx.qualifier, "sch"); - EXPECT_TRUE(ctx.relations.empty()); -} - -TEST(SqlContext, noRelationAfterAlias) -{ - auto ctx = analyze("SELECT * FROM t a|"); - EXPECT_EQ(ctx.kind, CompletionKind::None); -} - -TEST(SqlContext, relationAfterComma) -{ - auto ctx = analyze("SELECT * FROM t, |"); - EXPECT_EQ(ctx.kind, CompletionKind::Relation); - EXPECT_EQ(names(ctx), QStringList({ "t" })); -} - -TEST(SqlContext, aliasQualifier) -{ - auto ctx = analyze("SELECT a.| FROM public.tbl AS a JOIN other o ON o.id = a.id"); - EXPECT_EQ(ctx.kind, CompletionKind::QualifiedColumn); - EXPECT_EQ(ctx.qualifier, "a"); - EXPECT_EQ(names(ctx), QStringList({ "public.tbl/a", "other/o" })); -} - -TEST(SqlContext, schemaQualifiedTableQualifier) -{ - auto ctx = analyze("SELECT s.t.c| FROM s.t"); - EXPECT_EQ(ctx.kind, CompletionKind::QualifiedColumn); - EXPECT_EQ(ctx.qualifierSchema, "s"); - EXPECT_EQ(ctx.qualifier, "t"); - EXPECT_EQ(ctx.prefix, "c"); -} - -TEST(SqlContext, onClause) -{ - auto ctx = analyze("SELECT * FROM a x JOIN b y ON x.id = |"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(ctx.clause, SqlClause::On); - EXPECT_EQ(names(ctx), QStringList({ "a/x", "b/y" })); -} - -TEST(SqlContext, relationAfterJoin) -{ - auto ctx = analyze("SELECT * FROM a x LEFT JOIN |"); - EXPECT_EQ(ctx.kind, CompletionKind::JoinTarget); - EXPECT_EQ(ctx.clause, SqlClause::Join); - EXPECT_EQ(names(ctx), QStringList({ "a/x" })); - - EXPECT_EQ(analyze("SELECT * FROM a x JOIN b|").kind, CompletionKind::JoinTarget); - EXPECT_EQ(analyze("SELECT * FROM a x JOIN sch.|").kind, CompletionKind::Relation); - EXPECT_EQ(analyze("SELECT * FROM a x JOIN b y |").kind, CompletionKind::None); -} - -TEST(SqlContext, whereGroupOrderHaving) -{ - EXPECT_EQ(analyze("SELECT * FROM t WHERE |").clause, SqlClause::Where); - EXPECT_EQ(analyze("SELECT * FROM t GROUP BY |").clause, SqlClause::GroupBy); - EXPECT_EQ(analyze("SELECT * FROM t ORDER BY |").clause, SqlClause::OrderBy); - EXPECT_EQ(analyze("SELECT * FROM t GROUP BY a HAVING |").clause, SqlClause::Having); -} - -TEST(SqlContext, quotedPrefix) -{ - auto ctx = analyze("SELECT \"My| FROM t"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_TRUE(ctx.prefixWasQuoted); - EXPECT_EQ(ctx.prefix, "My"); - EXPECT_EQ(ctx.prefixStart, 7); - EXPECT_EQ(ctx.prefixLen, 3); - EXPECT_EQ(names(ctx), QStringList({ "t" })); -} - -TEST(SqlContext, quotedRelationNamesKeepCase) -{ - auto ctx = analyze("SELECT | FROM \"My Tab\" AS \"X\", Other O"); - EXPECT_EQ(names(ctx), QStringList({ "My Tab/X", "other/o" })); - EXPECT_TRUE(ctx.relations[0].quoted); -} - -TEST(SqlContext, multipleStatements) -{ - auto ctx = analyze("SELECT * FROM a; SELECT | FROM b; SELECT 1 FROM c"); - EXPECT_EQ(names(ctx), QStringList({ "b" })); -} - -TEST(SqlContext, subQueryHasOwnScopeAndSeesParent) -{ - auto ctx = analyze("SELECT * FROM a WHERE a.id IN (SELECT | FROM b)"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(names(ctx), QStringList({ "b", "a" })); -} - -TEST(SqlContext, derivedTableDoesNotSeeParent) -{ - auto ctx = analyze("SELECT * FROM a, (SELECT | FROM b) d"); - EXPECT_EQ(names(ctx), QStringList({ "b" })); - auto ctx2 = analyze("SELECT | FROM a, (SELECT 1 FROM b) d"); - EXPECT_EQ(names(ctx2), QStringList({ "a", "d/d" })); - EXPECT_TRUE(ctx2.relations[1].derived); -} - -TEST(SqlContext, functionCallParenInheritsClause) -{ - auto ctx = analyze("SELECT count(|) FROM t"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(names(ctx), QStringList({ "t" })); -} - -TEST(SqlContext, cte) -{ - auto ctx = analyze("WITH x (a) AS (SELECT 1), y AS MATERIALIZED (SELECT 2) SELECT | FROM x JOIN t ON true"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(names(ctx), QStringList({ "x", "t", "x", "y" })); - EXPECT_TRUE(ctx.relations[2].derived); -} - -TEST(SqlContext, unionSegments) -{ - auto ctx = analyze("SELECT * FROM a UNION SELECT | FROM b"); - EXPECT_EQ(names(ctx), QStringList({ "b" })); -} - -TEST(SqlContext, noCompletionInCommentOrString) -{ - EXPECT_EQ(analyze("SELECT * FROM t -- co|").kind, CompletionKind::None); - EXPECT_EQ(analyze("SELECT * FROM t /* co| */").kind, CompletionKind::None); - EXPECT_EQ(analyze("SELECT 'ab|c' FROM t").kind, CompletionKind::None); -} - -TEST(SqlContext, incompleteSql) -{ - auto ctx = analyze("SELECT a.id, b.| FROM a, b WHERE"); - EXPECT_EQ(ctx.kind, CompletionKind::QualifiedColumn); - EXPECT_EQ(ctx.qualifier, "b"); - EXPECT_EQ(names(ctx), QStringList({ "a", "b" })); -} - -TEST(SqlContext, updateSetAndWhere) -{ - auto ctx = analyze("UPDATE sch.t AS x SET |"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(ctx.clause, SqlClause::Set); - EXPECT_EQ(names(ctx), QStringList({ "sch.t/x" })); - - auto ctx2 = analyze("UPDATE t SET a = 1, b = 2 WHERE |"); - EXPECT_EQ(ctx2.clause, SqlClause::Where); - EXPECT_EQ(names(ctx2), QStringList({ "t" })); -} - -TEST(SqlContext, updateRelation) -{ - auto ctx = analyze("UPDATE |"); - EXPECT_EQ(ctx.kind, CompletionKind::Relation); -} - -TEST(SqlContext, updateFrom) -{ - auto ctx = analyze("UPDATE t SET a = u.| FROM u WHERE t.id = u.id"); - EXPECT_EQ(ctx.kind, CompletionKind::QualifiedColumn); - EXPECT_EQ(names(ctx), QStringList({ "t", "u" })); -} - -TEST(SqlContext, deleteFrom) -{ - auto ctx = analyze("DELETE FROM |"); - EXPECT_EQ(ctx.kind, CompletionKind::Relation); - auto ctx2 = analyze("DELETE FROM t WHERE |"); - EXPECT_EQ(ctx2.kind, CompletionKind::Column); - EXPECT_EQ(names(ctx2), QStringList({ "t" })); -} - -TEST(SqlContext, insertIntoRelation) -{ - EXPECT_EQ(analyze("INSERT INTO |").kind, CompletionKind::Relation); - EXPECT_EQ(analyze("INSERT INTO sch.t|").kind, CompletionKind::Relation); -} - -TEST(SqlContext, insertColumnList) -{ - auto ctx = analyze("INSERT INTO s.t (|"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(ctx.clause, SqlClause::InsertInto); - EXPECT_EQ(names(ctx), QStringList({ "s.t" })); - - auto ctx2 = analyze("INSERT INTO t (a, b|) VALUES (1, 2)"); - EXPECT_EQ(ctx2.kind, CompletionKind::Column); - EXPECT_EQ(ctx2.prefix, "b"); - EXPECT_EQ(names(ctx2), QStringList({ "t" })); -} - -TEST(SqlContext, insertValuesAndSelect) -{ - auto ctx = analyze("INSERT INTO t (a) VALUES (|)"); - EXPECT_EQ(ctx.clause, SqlClause::Values); - auto ctx2 = analyze("INSERT INTO t (a) SELECT | FROM u"); - EXPECT_EQ(ctx2.kind, CompletionKind::Column); - EXPECT_EQ(names(ctx2), QStringList({ "u" })); -} - -TEST(SqlContext, returning) -{ - auto ctx = analyze("INSERT INTO t (a) VALUES (1) RETURNING |"); - EXPECT_EQ(ctx.kind, CompletionKind::Column); - EXPECT_EQ(ctx.clause, SqlClause::Returning); - EXPECT_EQ(names(ctx), QStringList({ "t" })); - EXPECT_EQ(analyze("DELETE FROM t RETURNING |").clause, SqlClause::Returning); -} diff --git a/tests/pglabtests/tst_SslModeModel.cpp b/tests/pglabtests/tst_SslModeModel.cpp deleted file mode 100644 index 3a29944..0000000 --- a/tests/pglabtests/tst_SslModeModel.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include - -#include - -#include "ConnectionConfig.h" -#include "PrintTo_Qt.h" -#include "SslModeModel.h" - -TEST(SslModeModelTest, SelectedModesKeepTheirTextAndCanonicalValue) -{ - SslModeModel model; - QComboBox combo; - combo.setModel(&model); - - ASSERT_FALSE(combo.isEditable()); - const QStringList modes = {"disable", "allow", "prefer", "require", "verify-ca", "verify-full"}; - ASSERT_EQ(combo.count(), modes.size() + 1); - - for (const auto &mode : modes) { - SCOPED_TRACE(mode.toStdString()); - auto index = combo.findData(mode); - ASSERT_GT(index, 0); - combo.setCurrentIndex(index); - EXPECT_EQ(combo.currentText(), mode); - EXPECT_EQ(combo.currentData().toString(), mode); - - ConnectionConfig config; - config.setParameter("sslmode", combo.currentData().toString()); - EXPECT_EQ(config.getParameter("sslmode"), mode); - EXPECT_TRUE(config.connectionString().contains("sslmode=" + mode)); - - QComboBox reopened; - reopened.setModel(&model); - reopened.setCurrentIndex(reopened.findData(config.getParameter("sslmode"))); - EXPECT_EQ(reopened.currentText(), mode); - EXPECT_EQ(reopened.currentData().toString(), mode); - } -} - -TEST(SslModeModelTest, DefaultClearsAnExplicitMode) -{ - SslModeModel model; - QComboBox combo; - combo.setModel(&model); - - EXPECT_EQ(combo.currentText(), QStringLiteral("Default")); - EXPECT_EQ(combo.currentData().toString(), QString()); - EXPECT_EQ(combo.findData(QString()), 0); - - ConnectionConfig config; - config.setParameter("sslmode", "require"); - combo.setCurrentIndex(combo.findData(QStringLiteral("require"))); - combo.setCurrentIndex(combo.findData(QString())); - config.setParameter("sslmode", combo.currentData().toString()); - - EXPECT_TRUE(config.getParameter("sslmode").isEmpty()); - EXPECT_EQ(config.getParameters().count("sslmode"), 0); - EXPECT_FALSE(config.connectionString().contains("sslmode=")); -} - -TEST(SslModeModelTest, InvalidIndexesAndChildRowsAreEmpty) -{ - SslModeModel model; - EXPECT_FALSE(model.data(QModelIndex()).isValid()); - EXPECT_FALSE(model.data(model.index(model.rowCount(), 0)).isValid()); - EXPECT_EQ(model.rowCount(model.index(0, 0)), 0); -}