QBit SQL Workbench

A super-user SQL notebook for any QQQ application.

You write SQL and markdown in notebook cells, run them against any RDBMS backend of the instance (Postgres, MySQL, SQLite, H2), and see typed, paged, exportable results, quick charts, and visual EXPLAIN plans. The notebook UI lives in the Next UI as a SQL_WORKBENCH widget on the qbit's app page.

  • Read-only by default. A default session runs one statement inside a database READ ONLY transaction, and a statement classifier refuses anything that isn't a read.
  • Gated write mode. Writes need a separate permission and two steps. First a dry run executes in a transaction that is rolled back, so you see the affected-row counts. You then confirm with a single-use token bound to that exact SQL. A row guard, a hard cap, a drift check, and statement timeouts apply.
  • Everything is audited. The sqlWorkbenchExecution table records who ran what, when, on which backend, in which mode, how long it took, how many rows it returned or affected, and any error. SQL text and results never go to the logs.
  • Notebooks. Notebooks are personal by default and can be published to a shared library. They have named :parameters (always bound, never spliced), version history, and pinned result snapshots.

Raw SQL bypasses QQQ. It ignores record security locks, table and field permissions, hidden fields, and record locks. It also skips QQQ audits, record-change listeners, automations, ESB events, and search indexing. That is why the workbench is for super-users only. Use QQQ processes for business changes.

The full design (architecture, data model, security model, limits, API contract, UX, test strategy) is in docs/DESIGN.md.

Requirements

  • Java 21 and QQQ 4.1 (built against 4.1.0-RC.1).
  • qqq-backend-module-rdbms for the targets. qqq-middleware-javalin for the REST routes; it is optional, and without it the engine still works but no routes are registered.
  • A Next UI release that renders the SQL_WORKBENCH widget.

Install

<dependency>
   <groupId>com.kingsrook.qbits</groupId>
   <artifactId>qbit-sql-workbench</artifactId>
   <version>1.0.0-RC.1</version>
</dependency>
SqlWorkbenchQBitConfig config = new SqlWorkbenchQBitConfig()
   .withBackendName("postgres")                              // where the qbit's 5 tables live
   .withTableMetaDataCustomizer(new SnakeCaseCustomizer())   // optional: map names to snake_case
   .withReadOnlyCredentials("postgres", new SqlWorkbenchCredentials()
      .withUsername("${env.SQLWB_RO_USER}")                  // optional, recommended: a SELECT-only role
      .withPassword("${env.SQLWB_RO_PASSWORD}"));

new SqlWorkbenchQBitProducer().withQBitConfig(config).produce(qInstance);

The producer adds the following to the instance:

  • Tables: sqlWorkbenchNotebook, sqlWorkbenchCell, sqlWorkbenchNotebookVersion, sqlWorkbenchResultSnapshot, sqlWorkbenchExecution.
  • A possible value source over the notebook table.
  • The sqlWorkbenchWidget widget (type SQL_WORKBENCH).
  • The sqlWorkbench app, which contains the widget plus the notebook and audit tables. It has no processes.
  • The /qqq/v1/sql-workbench/* routes, registered through QJavalinMetaData, as qqq-esb does.

To move the app under another app (for example an Admin app), re-parent SqlWorkbenchQBitProducer.APP_NAME after producing.

Storage

Create the five tables on backendName. docs/ddl/postgres.sql has idempotent PostgreSQL DDL that you can paste into a Liquibase formatted-SQL changeset.

The DDL uses snake_case names: sql_workbench_notebook, owner_user_id, and so on. Match them with a table customizer:

public class SnakeCaseCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
{
   public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table)
   {
      table.setBackendDetails(new RDBMSTableBackendDetails().withTableName(QInstanceEnricher.inferBackendName(table.getName())));
      QInstanceEnricher.setInferredFieldBackendNames(table);
      return (table);
   }
}

Permissions

Everything is denied by default.

Permission Grants
sqlWorkbench.hasAccess The app, widget, tables, and every endpoint: read-only SQL, EXPLAIN, schema, notebooks, history
sqlWorkbench.write Write mode (preview and commit) and EXPLAIN ANALYZE
sqlWorkbench.share Publishing your notebooks to the shared library

The names are configurable (accessPermissionBaseName, writePermissionName, sharePermissionName).

write and share are plain session permissions. QQQ 4.1 does not list them as available permissions, so grant them where you grant roles, for example in an authentication customizer.

writeModeEnabled=false turns write mode off for everyone.

Safety model

Read-only sessions:

  • Postgres: setReadOnly(true), SET TRANSACTION READ ONLY, SET LOCAL statement_timeout and lock_timeout.
  • MySQL: setReadOnly(true), which sets the session transaction characteristic; it is reverted afterwards.
  • SQLite: PRAGMA query_only, reset afterwards.
  • H2: the classifier only. Use read-only credentials, or don't target H2 in production.

Credentials:

  • With readOnlyCredentials, read-only sessions connect as a dedicated role through their own pool. A READ ONLY transaction does not stop side-effecting functions such as pg_terminate_backend(), but a least-privilege role does.
  • Credentials are never returned or logged, and the config's toString redacts them.

Classifier:

  • It is conservative. Anything that isn't recognized as a read is treated as a write.
  • A "read" that mentions a mutating keyword is also a write: data-modifying CTEs, SELECT ... INTO, FOR UPDATE, nextval().
  • Transaction and session control is refused in every mode: BEGIN, COMMIT, SET, RESET, USE, PRAGMA x=y, ATTACH, DO, and others.

Clean connections:

  • Every session ends with a rollback (or a commit for a confirmed write) and restores autocommit and read-only.
  • If restoring fails, the connection is aborted rather than returned to the pool.
  • Audit writes happen after the session's connection is released.

Limits (all configurable):

Limit Default
Read-only statement timeout 30 s
Write statement timeout 60 s
Rows kept per result (paged from a server-side cache) 10,000
Result size 20 MB
Row guard (needs acknowledgement above this) 1,000 rows
Hard cap on affected rows 100,000
Concurrent runs per user 2
SQL length 100,000 characters

Cancellation, single-statement read-only runs, and per-user result caches also apply.

Audit:

  • A RUNNING row is written before the SQL runs. If that write fails, the SQL does not run.
  • Rejected attempts are audited too.
  • The audit table cannot be inserted into, edited, or deleted from through QQQ.

REST API

Base path: /qqq/v1/sql-workbench. The contract is in docs/DESIGN.md §5.

Area Endpoints
Info GET /capabilities, GET /schema/{backend}
Run POST /execute, POST /explain
Results GET /executions/{uuid}/rows, GET /executions/{uuid}/export?format=csv|xlsx|json, POST /executions/{uuid}/cancel
History GET /history
Notebooks /notebooks (CRUD, publish, duplicate, versions, restore, snapshots)

POST and PUT bodies must be application/json.

Build

./mvnw compile
./mvnw test                      # unit tests (H2, SQLite, Javalin over HTTP)
CI=true ./mvnw verify            # plus Postgres integration tests (Docker) and the JaCoCo gate
./mvnw verify -Dpostgres.test.image=postgres:17-alpine

Integration tests are skipped without Docker. With CI=true, a missing Docker fails the build instead.

License

Apache License 2.0. See LICENSE.

S
Description
QQQ QBit: super-user SQL notebook (read-only by default, gated audited write mode) over any QQQ RDBMS backend
Readme
2.4 MiB
0 Stars 3 Watchers 0 Forks
Languages
Java 100%