Files
qbit-sql-workbench/docs/ddl/postgres.sql
james.maesandClaude Opus 5.5 ddd85ebe0d feat(#3): expose workbench data as standard QQQ tables secured by record locks
Notebooks, cells, versions, pinned results, shares and the execution
audit are now ordinary QQQ tables in the SQL Workbench app (nav children,
record views with child lists, edit, export, audits, developer mode)
instead of hidden admin-only tables.

- Record security locks (core-enforced, so every entry point): owner OR
  share audience for notebooks and their children (through joins), the
  audit by user; admins hold sqlWorkbenchAllAccess. Key types
  sqlWorkbenchUser / sqlWorkbenchAudience, applied to sessions by
  SqlWorkbenchSecurity.applyToSession (no keys without workbench access).
- New sqlWorkbenchNotebookShare (USER / GROUP / ALL); publishing writes an
  ALL share and visibility follows the shares.
- Per-table read/insert/edit/delete permissions, listed per role by the
  config (user / share / admin).
- Table customizers hold the notebook invariants for the notebook UI and
  the tables alike: owner from the session, cell validation and order,
  one version per change, share keys, append-only versions and audit
  (admins included), pinned results only from pinning, cascades.
- NotebookService and history run as the caller through the same actions;
  explicit owner/share checks stay as defense in depth.
- Record actions: Share Notebook, Duplicate Notebook, Restore Version.
- DDL: share table plus an idempotent backfill of library shares.
- Tests: TableSecurityMatrixTest (role x table x action over the v1 table
  API, core actions, export, PVS, record actions, notebook endpoints) and
  a PostgreSQL lock test. Docs: README, DESIGN, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:08:10 -05:00

136 lines
5.9 KiB
SQL

-- SQL Workbench QBit (com.kingsrook.qbits:qbit-sql-workbench) - PostgreSQL DDL
--
-- Table and column names are the snake_case forms of the QQQ names, which is
-- what a host gets when its tableMetaDataCustomizer applies
-- QInstanceEnricher.inferBackendName to the table name and
-- QInstanceEnricher.setInferredFieldBackendNames to the fields (see README,
-- "Storage"). With a tableNamePrefix, prefix the table names the same way.
--
-- Liquibase: paste into a formatted-SQL changeset, e.g.
-- -- liquibase formatted sql
-- -- changeset you:NNNN-1
-- Idempotent (IF NOT EXISTS), so it is safe on re-run.
CREATE TABLE IF NOT EXISTS sql_workbench_notebook (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
owner_user_id VARCHAR(255) NOT NULL,
owner_name VARCHAR(255),
visibility VARCHAR(16) NOT NULL DEFAULT 'PERSONAL',
default_backend_name VARCHAR(100),
parameters_json TEXT,
current_version INTEGER,
create_date TIMESTAMP WITHOUT TIME ZONE,
modify_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_sql_workbench_notebook PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS ix_sql_workbench_notebook_owner ON sql_workbench_notebook (owner_user_id);
CREATE INDEX IF NOT EXISTS ix_sql_workbench_notebook_visibility ON sql_workbench_notebook (visibility);
CREATE TABLE IF NOT EXISTS sql_workbench_cell (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
notebook_id INTEGER NOT NULL,
cell_key VARCHAR(64) NOT NULL,
sort_order INTEGER,
cell_type VARCHAR(16) NOT NULL,
title VARCHAR(255),
content TEXT,
backend_name VARCHAR(100),
create_date TIMESTAMP WITHOUT TIME ZONE,
modify_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_sql_workbench_cell PRIMARY KEY (id),
CONSTRAINT uq_sql_workbench_cell_key UNIQUE (notebook_id, cell_key),
CONSTRAINT fk_sql_workbench_cell_notebook FOREIGN KEY (notebook_id) REFERENCES sql_workbench_notebook (id)
);
CREATE TABLE IF NOT EXISTS sql_workbench_notebook_version (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
notebook_id INTEGER NOT NULL,
version_number INTEGER NOT NULL,
content_json TEXT,
saved_by_user_id VARCHAR(255),
saved_by_name VARCHAR(255),
comment VARCHAR(255),
create_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_sql_workbench_notebook_version PRIMARY KEY (id),
CONSTRAINT uq_sql_workbench_notebook_version UNIQUE (notebook_id, version_number),
CONSTRAINT fk_sql_workbench_notebook_version_notebook FOREIGN KEY (notebook_id) REFERENCES sql_workbench_notebook (id)
);
CREATE TABLE IF NOT EXISTS sql_workbench_result_snapshot (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
notebook_id INTEGER NOT NULL,
cell_key VARCHAR(64),
execution_uuid VARCHAR(36),
label VARCHAR(255),
backend_name VARCHAR(100),
sql_text TEXT,
result_json TEXT,
row_count INTEGER,
truncated BOOLEAN,
created_by_user_id VARCHAR(255),
create_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_sql_workbench_result_snapshot PRIMARY KEY (id),
CONSTRAINT fk_sql_workbench_result_snapshot_notebook FOREIGN KEY (notebook_id) REFERENCES sql_workbench_notebook (id)
);
-- Shares (1.0.0-RC.2): who, besides the owner, may read a notebook.
-- audience_key is "user:<id>", "group:<name>" or "all" (the shared library);
-- the record security locks match it against the session's audience keys.
CREATE TABLE IF NOT EXISTS sql_workbench_notebook_share (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
notebook_id INTEGER NOT NULL,
audience_type VARCHAR(16) NOT NULL,
audience_id VARCHAR(255),
audience_key VARCHAR(300) NOT NULL,
created_by_user_id VARCHAR(255),
created_by_name VARCHAR(255),
create_date TIMESTAMP WITHOUT TIME ZONE,
modify_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_sql_workbench_notebook_share PRIMARY KEY (id),
CONSTRAINT uq_sql_workbench_notebook_share UNIQUE (notebook_id, audience_key),
CONSTRAINT fk_sql_workbench_notebook_share_notebook FOREIGN KEY (notebook_id) REFERENCES sql_workbench_notebook (id)
);
CREATE INDEX IF NOT EXISTS ix_sql_workbench_notebook_share_audience ON sql_workbench_notebook_share (audience_key, notebook_id);
-- Upgrading from 1.0.0-RC.1: notebooks published to the library
-- (visibility SHARED) get their "all" share row. Idempotent.
INSERT INTO sql_workbench_notebook_share (notebook_id, audience_type, audience_id, audience_key, created_by_user_id, created_by_name, create_date, modify_date)
SELECT n.id, 'ALL', NULL, 'all', n.owner_user_id, n.owner_name, COALESCE(n.modify_date, CURRENT_TIMESTAMP), COALESCE(n.modify_date, CURRENT_TIMESTAMP)
FROM sql_workbench_notebook n
WHERE n.visibility = 'SHARED'
AND NOT EXISTS (SELECT 1 FROM sql_workbench_notebook_share s WHERE s.notebook_id = n.id AND s.audience_key = 'all');
-- Audit: append-only. Not foreign-keyed to notebooks, so deleting a
-- notebook never deletes its audit trail.
CREATE TABLE IF NOT EXISTS sql_workbench_execution (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
execution_uuid VARCHAR(36) NOT NULL,
user_id VARCHAR(255),
user_name VARCHAR(255),
backend_name VARCHAR(100),
mode VARCHAR(24),
status VARCHAR(16),
sql_text TEXT,
sql_hash VARCHAR(64),
parameters_json TEXT,
statement_count INTEGER,
statement_kinds VARCHAR(255),
notebook_id INTEGER,
cell_key VARCHAR(64),
start_time TIMESTAMP WITHOUT TIME ZONE,
end_time TIMESTAMP WITHOUT TIME ZONE,
duration_ms INTEGER,
rows_returned INTEGER,
rows_affected INTEGER,
truncated BOOLEAN,
error_message TEXT,
client_ip VARCHAR(64),
create_date TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_sql_workbench_execution PRIMARY KEY (id),
CONSTRAINT uq_sql_workbench_execution_uuid UNIQUE (execution_uuid)
);
CREATE INDEX IF NOT EXISTS ix_sql_workbench_execution_user ON sql_workbench_execution (user_id, id);
CREATE INDEX IF NOT EXISTS ix_sql_workbench_execution_start ON sql_workbench_execution (start_time);