Commit 0b9c112
Release dev → main: Redis provider, full provider docs, docs hygiene & schema-timeout fix (0.9.27) (#89)
* fix(sql): quote schema-qualified table names per-segment
The dialect-aware quoteIdentifier treated a schema-qualified table name as a
single identifier, so 'employees.department' became '"employees.department"' —
which Postgres reads as one relation literally named with a dot, failing with a
query error. Sidebar 'select top 100' on any schema-qualified table was broken.
Add quoteQualifiedName(): split on '.', quote each segment only-when-needed, and
rejoin. 'employees.department' stays unquoted; 'public.Order' becomes
'public."Order"'. Use it for table names in the query generators and the data
profiler (columns still use single-identifier quoting).
Verified end-to-end against a Neon Postgres database: the generated
'SELECT * FROM employees.department LIMIT 50;' runs, whereas the old quoted form
failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(package): bump version to 0.9.23
* chore(package): bump version to 0.9.24
* fix(db/postgres): prevent schema introspection timeout on large schemas getSchema() ran a single query that joins five information_schema-based CTEs (columns, PKs, foreign keys, indexes + table sizes). PostgreSQL 12+ inlines single-reference CTEs, and because the planner estimates rows=1 for information_schema views it chose nested-loop joins that re-execute the expensive fk/index CTEs many times. On schemas with 100+ tables this exploded to ~295s and always hit the connection's 60s statement_timeout, so the schema tree never loaded.
Two complementary fixes:
1. Mark all CTEs AS MATERIALIZED so each is computed once instead of being re-executed inside nested loops (~295s -> ~2.6s on a 122-table schema).
2. Split schema loading into two phases so a slow/failing stats query can never block the table list:
- getSchemaList() -> tables + columns + PKs (fast, ~50ms) [/api/db/schema/list]
- getSchemaRelations() -> foreign keys + indexes (heavy, ~2.3s) [/api/db/schema/relations]
The client renders the tree from the fast list immediately, then merges relations asynchronously. If relations fail/time out they are logged and skipped — the table list stays intact. getSchema() is kept (MATERIALIZED) for consumers that need the full schema (diff, AI context).
New optional provider methods getSchemaList/getSchemaRelations fall back to getSchema()/[] for providers that don't implement them.
* test(db/schema): cover two-phase schema introspection (list + relations)
Adds regression coverage for the schema-introspection timeout fix (0d60c47),
which split schema loading into a fast structural list and a heavy, best-effort
relations phase. None of the new code paths were previously tested.
Provider (tests/integration/db/postgres-provider.test.ts, +11):
- getSchemaList(): returns columns + PKs but intentionally empty
indexes/foreignKeys, isPrimary detection, non-public schema prefixing,
negative reltuples row_count clamped to 0, and null/empty columns.
- getSchemaRelations(): FK/index data keyed by display name, non-public
prefixing on both the table and the referenced table, public references kept
bare, empty/null fk-index tolerance, and null index columns coerced to [].
API routes (new files, 15 tests):
- /api/db/schema/list: uses getSchemaList() when present, falls back to
getSchema() otherwise, plus auth (401), empty-body (400), missing-type (400),
ConnectionError (503) and DatabaseError/generic (500).
- /api/db/schema/relations: uses getSchemaRelations() when present, returns []
fallback otherwise, plus the same auth/validation/error matrix.
Hook (tests/hooks/use-connection-manager.test.ts, +4):
- phase 1 renders the table list, phase 2 merges FKs/indexes by table name;
- relations failure does NOT wipe the table list and shows no error toast
(the core resilience contract of the fix);
- phase 1 failure short-circuits so the heavy relations endpoint is never hit;
- tables absent from the relations payload are left unchanged.
Full suite green: lint (0 errors), typecheck, 2050 unit/api/integration +
251 hooks + component groups, 0 failures.
* fix(db/postgres): hoist schema SQL to module scope + dedupe CTEs for Sonar gate
The SonarCloud PR quality gate failed on #71 with 53.3% coverage and 6.6%
duplication on new code. Both stem from how the schema introspection SQL was
written, not from missing tests.
Coverage: the getSchema/getSchemaList/getSchemaRelations SQL lived in multi-line
template literals *inside the method bodies*. bun's coverage instruments the
interior lines of such literals as 0-hit in any test process that imports the
module without exercising the method; the merged lcov (per-file isolated runs)
then reports those SQL lines as uncovered even though the methods are tested.
Verified empirically: a module-level `const` template is evaluated once at load
and reported covered everywhere, while an in-body one is not. Hoisting the three
queries to module scope takes postgres.ts new-code line coverage 28% -> 100%
(overall new-code coverage 53.9% -> ~99%).
Duplication: the hoisted queries shared the same CTEs (tables/columns/pk across
full+list, fk/index across full+relations). Extracted each CTE to a single
reusable fragment const and compose the three queries from them, removing the
duplicated blocks. The SQL text produced is unchanged.
Tests: the two route specs intentionally keep their mock.module setup inline
(a shared helper breaks bun's per-file mock scoping under the non-isolated
`bun run test`, clobbering @/lib/db mocks across files). Added an empty-object
({}) body case to each for the last uncovered branch. Test mock/setup
boilerplate is expected harness scaffolding, so tests are excluded from Sonar
copy-paste detection (sonar.cpd.exclusions).
No behavioural change: same SQL, same provider/route/hook logic. Verified:
lint (0 errors), typecheck, 2052 unit/api/integration + hooks + component
groups (0 failures), production build, full coverage regenerated.
* chore(package): bump version to 0.9.25
* refactor(api/schema): extract shared handler to remove route duplication
The Sonar PR gate still flagged 6.2% duplication on new code: the
/api/db/schema/list and /api/db/schema/relations routes shared ~39 identical
lines of body parsing, auth, connection resolution and error handling (the only
difference is which provider method they call).
Extract that boilerplate into handleSchemaRequest() in src/lib/api/schema-route.ts;
each route is now a thin wrapper that passes a `load` callback selecting the
provider method (with the getSchemaList->getSchema and getSchemaRelations->[]
fallbacks preserved). Removes both 39-line duplicated blocks.
Coverage holds at 100% on new code (the shared handler is fully exercised by the
existing route specs). Verified: typecheck, 2052 core tests + 0 failures,
coverage regenerated (schema-route.ts 32/32 lines).
* fix(db/postgres): join constraint_column_usage on constraint_schema for cross-schema FKs
Copilot review on #71 flagged a pre-existing bug in the FK introspection
(from 0d60c47, relocated verbatim into CTE_FK_INFO during the dedupe):
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema -- wrong
For a foreign key, constraint_column_usage reports the *referenced* (parent)
table's columns, so ccu.table_schema is the referenced schema while
tc.table_schema is the referencing schema. Joining them requires the two to be
equal, which silently drops every cross-schema foreign key and makes
referencedSchema always equal the referencing schema — defeating the non-public
referenced-table prefixing (e.g. billing.accounts).
Join on the constraint's own schema instead (ccu.constraint_schema =
tc.constraint_schema). Because the FK CTE is now single-sourced, this fixes both
getSchema() and getSchemaRelations().
Added a SQL-level regression guard: the existing specs mock the query result, so
they can't catch a bad join; the new test asserts the emitted SQL joins on
constraint_schema and not table_schema. Real cross-schema FK behaviour should be
confirmed against a live Postgres via E2E.
* fix(ci): restore missing test:ci script used by the npm publish gate
The npm-publish workflow's Validate job runs `bun run test:ci`, but the script
was dropped from package.json somewhere after v0.9.19 (it was added in #66).
The v0.9.25 tag push therefore failed with "Script not found test:ci", blocking
the npm publish (the GitHub release, tag and Docker images published fine).
Restore it to the per-file-isolated form the publish gate expects:
test:ci = bash tests/run-core.sh && bun run test:components
This mirrors ci.yml's reliable path and avoids bun's process-wide mock.module
load-order flakiness that the single-process `bun run test` is prone to.
* fix(schema-explorer): update select action label from 'Select Top 100' to 'Select Top 50'
* bump: upgrade version to 0.9.26
* fix(e2e): make admin-dashboard tests seed-state-independent
The 'default tab is overview' and 'can switch to operations tab' E2E tests
asserted on empty-state-only copy ('Command Center', the 'Select connection'
placeholder), which disappears once seed connections load the populated
dashboard. They passed in CI (no seed file) but failed locally with
SEED_CONFIG_PATH set.
Add stable data-testid hooks (admin-content-overview/-operations) to the
TabsContent wrappers and assert on testid visibility + tab aria-selected,
so the tests hold in both empty and populated states.
Verified: 32/32 full E2E pass with seeds loaded, 8/8 admin pass with seeds disabled.
* feat(redis): add Redis provider support with query format and error handling
- Introduced Redis connection configuration and query formats in CLAUDE.md and API_DOCS.md.
- Added support for both plain Redis commands and JSON command objects.
- Implemented schema introspection and health metrics retrieval for Redis.
- Enhanced Redis provider tests to cover error handling for malformed commands and Redis-side errors.
* bump: upgrade app version to 0.9.27
* docs(providers): add comprehensive Redis provider reference (#75)
* docs(providers): add comprehensive Redis provider reference
Add docs/providers/redis.md — the single reference point for the Redis
provider covering design rationale, architecture (Strategy Pattern + base
class), connection, query interface, schema mapping, monitoring, capabilities,
error handling, testing, usage, and known limitations.
Also serves as the template for the other provider docs under docs/providers/.
* docs(providers): address review — TYPE sampling, analyze line count, TLS wording
* docs(providers): add comprehensive PostgreSQL provider reference (#76)
* docs(providers): add comprehensive PostgreSQL provider reference
Add docs/providers/postgresql.md documenting the PostgreSQL provider — the
reference implementation for the SQL provider family. Covers the SQLBaseProvider
layer, connection pooling/SSL, automatic LIMIT injection, transactions and query
cancellation, the MATERIALIZED-CTE two-phase schema introspection (and its
performance/coverage rationale), pg_stat_* monitoring with graceful degradation,
maintenance, capabilities, error mapping, testing, and known limitations.
* docs(providers): rename to postgres.md to match the canonical type-id
Convention: doc filename mirrors the code type-id / source filename
(providers/sql/postgres.ts -> docs/providers/postgres.md) for a 1:1
code<->doc mapping. The official name "PostgreSQL" is retained in the
document title and prose.
* docs(providers): address review — queryTimeout source, SSL precedence, getHealth fallback, tx pooling, config validation
- queryTimeout is ProviderOptions.queryTimeout (not pool), applied as statement_timeout
- SSL step 2 also triggers on options.ssl === true, not just cloud hosts
- getHealth returns a placeholder row (no pg_stat_activity fallback — that's getSlowQueries)
- transaction client is checked out from the pool, not held "outside" it
- the two connection forms are not mutually exclusive; connection string wins when both given
* docs(providers): address review — test:ci runner & statement_timeout error class
- Testing: `bun run test` runs the core group in one process (mock.module flaky);
CI uses `test:ci` (run-core.sh per-file isolation) / `test:coverage`, not `bun run test`
- Error mapping: PG statement_timeout emits "canceling statement due to statement
timeout" → classified as QueryCancelledError (cancellation check precedes timeout),
not TimeoutError; TimeoutError is for generic/acquire timeouts
- getHealth: clarified as placeholder path (not pg_stat_activity fallback) in coverage notes
* docs: add provider tri-sync invariant to CLAUDE.md (#77)
Providers are the lifeblood of the project: for each provider, code, docs, and
tests form a 1:1 triad keyed by the canonical type-id, and a change to any one
side must sync the other two in the same PR. Documented as a callout under
Architecture → Key Patterns → Database Abstraction.
* docs(providers): add comprehensive MySQL provider reference (#78)
* docs(providers): add comprehensive MySQL provider reference
Add docs/providers/mysql.md documenting the MySQL provider, framed as a diff
against the PostgreSQL SQL reference. Covers the mysql2 pool config (only `max`
honored; no statement_timeout wiring), backtick quoting, binary→hex row
sanitization, N+1 single-database schema introspection (no two-phase split),
KILL QUERY cancellation, transactions, performance_schema monitoring with its
three distinct degradation modes, analyze/optimize/check/kill maintenance,
error mapping, testing, and known limitations.
* docs(providers): address review — connectionString bypass & TimeoutError nuance
- buildPoolConfig connectionString path returns {...baseConfig, uri} and skips
the discrete branch, so ssl/connection.ssl/auto-SSL and timezone are ignored
(must be encoded in the URI) — now documented in §4.2/§4.3
- error handling: mapDatabaseError DOES emit TimeoutError for any "timeout"/
"timed out" driver message (e.g. lock-wait timeout); the MySQL-specific gap is
only the lack of a server-side timeout derived from queryTimeout
* docs(providers): address review — rowCount for non-SELECT & MAX_EXECUTION_TIME
- rowCount is rows.length only for array (SELECT) results; non-SELECT returns a
ResultSetHeader and the provider reports rowCount: 0 (affected-rows not surfaced)
- future-work: MAX_EXECUTION_TIME is the per-statement limit; wait_timeout is
idle-connection only and wouldn't bound query execution — dropped it
* docs(providers): add comprehensive Oracle provider reference (#79)
* docs(providers): add comprehensive Oracle provider reference
Add docs/providers/oracle.md documenting the Oracle provider (oracledb Thin
mode), framed as a diff vs the PostgreSQL SQL reference. Covers EZConnect
service-name connect string, FETCH FIRST pagination, owner-scoped 5-query schema
introspection, connection.break() cancellation, transactions without an
auto-rollback timeout, the absence of an SSL config path (TLS via wallet/connect
string), privilege-guarded V$-view monitoring, DBMS_STATS/index-rebuild/kill
maintenance, overridden Oracle UI labels, ORA-* error mapping, testing, and
known limitations.
* docs(providers): address review — queryTimeout is a separate ProviderOptions field, not part of DEFAULT_POOL_CONFIG
* docs(providers): document Oracle data-type & feature gaps (LOB, NUMBER, EXPLAIN, callTimeout, privileges)
Add a §5.3 on CLOB/BLOB (returned as Lob stream objects, not configured via
fetchAsString/fetchAsBuffer) and NUMBER precision loss, and expand Known
Limitations with: LOB rendering gap, large-NUMBER precision, EXPLAIN advertised
but not implemented for Oracle (UI builder is pg/mysql only), connection.callTimeout
as the fix for no server-side timeout, ALTER SYSTEM / V$ privilege requirements,
and the module-global oracledb settings side effect.
* docs(providers): clarify Oracle connectionString is EZConnect/TNS, not oracle://
connectionString is passed straight to oracledb.createPool({ connectString }),
which expects EZConnect/TNS. The oracle:// scheme is only a UI paste-parser
convenience that decomposes into discrete fields and never reaches the driver.
* docs(providers): add comprehensive Microsoft SQL Server (mssql) reference (#80)
* docs(providers): add comprehensive Microsoft SQL Server (mssql) reference
Add docs/providers/mssql.md documenting the SQL Server provider (node-mssql),
framed as a diff vs the PostgreSQL SQL reference, and remove the stray
sqlserver.md placeholder (type-id naming: file = mssql, prose = "SQL Server").
Covers encrypt-by-default/Azure-aware TLS, TOP / OFFSET-FETCH pagination,
five-query cross-schema introspection, the fully-wired pool+requestTimeout
(server-side query timeout), rowsAffected-based rowCount, real blocked-session
detection and real index scan counts, mssql.Transaction (no auto-rollback
timeout), request.cancel() cancellation, DMV monitoring with privilege guards,
analyze/check/optimize/kill maintenance, overridden labels, SQL-Server error
mapping, and known gaps (connectionString ignored by buildConfig, EXPLAIN
advertised-not-implemented, non-Azure cert trust, unsanitized binary).
* docs(providers): address review — requestTimeout is driver-enforced, soften connectionString wording
- requestTimeout is enforced client-side by the mssql/Tedious driver, not a
server-enforced statement timeout (Postgres statement_timeout is the latter);
reworded §3.5 and §11 accordingly (+ fixed the anchor link)
- connectionString-only config: reworded from "silently connect to localhost"
to targeting an unintended server and likely failing on missing fields
* docs(providers): document SQL Server data-type & enterprise gaps
Add §5.3 (parameter type inference, BIGINT/DECIMAL/MONEY precision loss, binary
Buffer, single-recordset) and expand Known Limitations with numeric precision,
untyped parameter binding, missing Always On options (MultiSubnetFailover /
ApplicationIntent ReadOnly routing), and Azure SQL DMV/DBCC caveats.
* docs(providers): address review — grammar, cancel wording, index-scan claim, cancellation error class
- "does bound" -> "does impose a request timeout"
- cancelQuery: returns false if no tracked Request, else true if cancel() didn't
throw (doesn't confirm cancellation took effect)
- index scans: Postgres ALSO reports real counts (pg_stat_user_indexes.idx_scan);
SQL Server is unique only for blocked-session detection
- error table: cancellation-pattern messages map to QueryCancelledError before the timeout check
* docs(providers): add comprehensive SQLite reference (#81)
* docs(providers): add comprehensive SQLite reference (with deployment-constraint framing)
Add docs/providers/sqlite.md. Leads with the strategic framing the product needs:
SQLite-as-target is an embedded, server-local, bun:sqlite-only engine — fit for
self-hosted / local-dev / edge and zero-config trials, NOT a multi-tenant SaaS
target. Disentangles the two SQLites (storage better-sqlite3 vs target bun:sqlite).
Covers file-path resolution + traversal guard, connect PRAGMAs (WAL/FK), read/write
dispatch, the absence of transactions/cancellation/pooling, single-schema PRAGMA
introspection, minimal/estimated monitoring, vacuum/analyze/reindex/check
maintenance, real-engine (:memory:) testing, and known limitations.
* docs(providers): address review — stable anchor (drop emoji from heading) & accurate path-traversal note
- Removed the 1 parent 46fc7c2 commit 0b9c112
24 files changed
Lines changed: 3719 additions & 833 deletions
File tree
- .claude
- rules
- charts/libredb-studio
- docs
- archived
- editor
- providers
- tests/integration/db
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
164 | | - | |
| 164 | + | |
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
| 170 | + | |
| 171 | + | |
170 | 172 | | |
171 | 173 | | |
172 | 174 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| |||
0 commit comments