Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches: [main]
pull_request:
branches: [main]
schedule:
# Weekly cargo-audit sweep to catch newly-disclosed CVEs in deps that
# haven't otherwise changed. Off-peak minute, not :00/:30.
- cron: "17 6 * * 1"

jobs:
test:
Expand All @@ -20,10 +24,89 @@ jobs:
run: cargo build

- name: Run tests
run: cargo test
# --lib --bins excludes tests/live_db.rs: that's a live-database
# integration test requiring a running PostgreSQL instance, covered
# by the dedicated live-db-integration job below.
run: cargo test --lib --bins

- name: Clippy
run: cargo clippy --all-targets -- -D warnings

- name: Check formatting
run: cargo fmt --all -- --check

validate-manifest:
name: Validate .tabularium manifest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Validate against the live registry schema
run: npx --yes @tabularium/cli validate .tabularium --registry https://registry.tabularis.dev --kind driver

markdownlint:
name: Markdown lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Run markdownlint
run: npx --yes markdownlint-cli "**/*.md"

audit:
name: Security audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
# RUSTSEC-2026-0235: vulnerable rkyv 0.7.46, pulled in transitively
# by rust_decimal's own optional "rkyv" feature declaration in its
# Cargo.toml — we never enable that feature (only "db-tokio-postgres"
# and "serde"), and confirmed no rkyv symbols are linked into the
# release binary (`nm -D target/release/postgresql-plugin | grep
# rkyv` — no output). cargo-audit scans the full Cargo.lock graph
# regardless of which optional features are active, so this is a
# lockfile-only entry with no reachable code path in what we ship.
# Re-check this ignore whenever rust_decimal is upgraded, in case a
# newer release changes what's declared as optional.
ignore: RUSTSEC-2026-0235

live-db-integration:
name: Live PostgreSQL integration
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
ports:
- 54320:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v7

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build
run: cargo build

- name: Run live-database integration test
env:
POSTGRES_PLUGIN_BIN: ${{ github.workspace }}/target/debug/postgresql-plugin
PGHOST: 127.0.0.1
PGPORT: 54320
PGUSER: postgres
PGPASSWORD: password
PGDATABASE: testdb
run: cargo test --test live_db -- --test-threads=1

26 changes: 26 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,32 @@ jobs:
Copy-Item ".tabularium" $stage
Compress-Archive -Path "$stage\*" -DestinationPath "postgresql-plugin-${{ matrix.platform-label }}.zip"

# linux-arm64 is cross-compiled via `cross` on an x86_64 runner — that
# binary is a foreign architecture and cannot execute here without
# QEMU emulation, so this leg is skipped rather than adding that
# complexity for one platform.
- name: Smoke test binary (unix)
if: runner.os != 'Windows' && !matrix.cross
run: |
response=$(echo '{"jsonrpc":"2.0","method":"initialize","id":1}' | ./staging/postgresql-plugin${{ matrix.binary-suffix }})
echo "$response"
echo "$response" | jq -e 'has("error") | not' > /dev/null || {
echo "::error::Binary did not return a valid initialize response"
exit 1
}

- name: Smoke test binary (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$response = '{"jsonrpc":"2.0","method":"initialize","id":1}' | & ".\staging\postgresql-plugin${{ matrix.binary-suffix }}"
Write-Output $response
$parsed = $response | ConvertFrom-Json
if ($null -ne $parsed.error) {
Write-Error "Binary did not return a valid initialize response"
exit 1
}

- name: Stash artifact
uses: actions/upload-artifact@v7
with:
Expand Down
4 changes: 3 additions & 1 deletion .markdownlint.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"default": true,
"MD013": false,
"MD024": { "siblings_only": true },
"MD033": false,
"MD041": false
"MD041": false,
"MD060": false
}
2 changes: 2 additions & 0 deletions .markdownlintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
node_modules/
69 changes: 69 additions & 0 deletions .tabularium
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"$schema": "https://registry.tabularis.dev/manifest.schema.json?kind=driver",
"name": "postgresql",
"version": "0.1.0",
"description": "PostgreSQL plugin driver for Tabularis (parity implementation)",
"kind": "driver",
"engine": "postgresql",
"paradigms": ["relational"],
"default_port": 5432,
"default_username": "postgres",
"executable": "postgresql-plugin",
"capabilities": {
"schemas": true,
"views": true,
"materialized_views": true,
"routines": true,
"routine_management": true,
"triggers": true,
"file_based": false,
"folder_based": false,
"connection_string": true,
"connection_string_example": "postgres://user:pass@localhost:5432/db",
"identifier_quote": "\"",
"sql_dialect": "postgres",
"alter_primary_key": true,
"alter_column": true,
"create_foreign_keys": true,
"manage_tables": true,
"supports_ssl": true,
"explain": true,
"readonly": false,
"no_connection_required": false,
"serial_type": "SERIAL",
"auto_increment_keyword": "",
"inline_pk": false
},
"type_mappings": {
"DATETIME": "TIMESTAMP",
"JSON": "JSONB"
},
"data_types": [
{"name": "SMALLINT", "category": "numeric", "requires_length": false, "requires_precision": false},
{"name": "INTEGER", "category": "numeric", "requires_length": false, "requires_precision": false},
{"name": "BIGINT", "category": "numeric", "requires_length": false, "requires_precision": false},
{"name": "SERIAL", "category": "numeric", "requires_length": false, "requires_precision": false},
{"name": "BIGSERIAL", "category": "numeric", "requires_length": false, "requires_precision": false},
{"name": "REAL", "category": "numeric", "requires_length": false, "requires_precision": false},
{"name": "DOUBLE PRECISION", "category": "numeric", "requires_length": false, "requires_precision": false},
{"name": "NUMERIC", "category": "numeric", "requires_length": false, "requires_precision": true},
{"name": "DECIMAL", "category": "numeric", "requires_length": false, "requires_precision": true},
{"name": "MONEY", "category": "numeric", "requires_length": false, "requires_precision": false},
{"name": "CHAR", "category": "string", "requires_length": true, "requires_precision": false},
{"name": "VARCHAR", "category": "string", "requires_length": true, "requires_precision": false},
{"name": "TEXT", "category": "string", "requires_length": false, "requires_precision": false},
{"name": "DATE", "category": "date", "requires_length": false, "requires_precision": false},
{"name": "TIME", "category": "date", "requires_length": false, "requires_precision": false},
{"name": "TIMESTAMP", "category": "date", "requires_length": false, "requires_precision": false},
{"name": "TIMESTAMPTZ", "category": "date", "requires_length": false, "requires_precision": false},
{"name": "INTERVAL", "category": "date", "requires_length": false, "requires_precision": false},
{"name": "BOOLEAN", "category": "other", "requires_length": false, "requires_precision": false},
{"name": "UUID", "category": "other", "requires_length": false, "requires_precision": false},
{"name": "JSON", "category": "json", "requires_length": false, "requires_precision": false},
{"name": "JSONB", "category": "json", "requires_length": false, "requires_precision": false},
{"name": "BYTEA", "category": "binary", "requires_length": false, "requires_precision": false},
{"name": "INET", "category": "other", "requires_length": false, "requires_precision": false},
{"name": "CIDR", "category": "other", "requires_length": false, "requires_precision": false},
{"name": "MACADDR", "category": "other", "requires_length": false, "requires_precision": false}
]
}
126 changes: 123 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,116 @@

### Added

- CI hardening — deliberately set a higher bar than the sibling plugin
repos and the org's own documented requirements (no sibling runs
`cargo audit`; only 2 of 11 Rust siblings gate on clippy/fmt at all):
- `.tabularium` manifest validation against the live registry schema via
`@tabularium/cli validate`, catching a malformed manifest automatically
(we got `name`/`id` wrong once by hand earlier this session).
- `markdownlint-cli` as an enforced CI job, not a manually-run habit.
- A release-binary smoke test: pipe a trivial `initialize` JSON-RPC
request into each freshly-built platform binary and assert a valid
(non-error) response before it ships in a zip. Skipped for `linux-arm64`
only, since that leg is cross-compiled and the binary can't execute on
the x86_64 build runner without QEMU emulation.
- `cargo audit` (via `rustsec/audit-check`) for supply-chain
vulnerabilities, on every push/PR and a weekly schedule (catches CVEs
disclosed after merge against unchanged dependencies).
- `tests/live_db.rs`: a self-contained live-`postgres:16`-container
integration test (first top-level `tests/` dir in this repo — existing
tests are all pure unit tests via the `.rules/rust.md` #4/#5
sibling-file convention). Covers connect, a basic query, an insert, and
the `startup_script`/`connection_string` handlers found completely
uncovered during the security-audit pass — closes the actual biggest
gap in this repo's CI: nothing previously verified the binary against
a real database automatically. Deliberately NOT the cross-repo 82-test
parity suite (that stays a manual/periodic check against `tabularis`,
per the "Repo Extraction" open question in
`docs/planning/02-phase-1-plugin-build.md`).
- Two further hardening ideas — `dependency-review-action` on PRs and
SBOM generation via `cargo-cyclonedx` — were considered and
deliberately deferred rather than implemented now; see
`docs/planning/ci-hardening-deferred.md` for the rationale.

### Fixed

- Three of the new CI jobs above failed on their first real run and were
fixed:
- `Test` job: the existing `cargo test` (no target filter) tried to run
`tests/live_db.rs` too, which panics immediately without a running
PostgreSQL instance. Scoped to `cargo test --lib --bins`, leaving the
live-DB test to its own dedicated `live-db-integration` job.
- `Markdown lint` job: `docs/planning/.markdownlint.json`'s scoped
override (`MD024`/`MD060`) only applied when markdownlint was invoked
from within that directory — the CI step's root-level `**/*.md` glob
never picked it up. Merged the scoped overrides into the single root
`.markdownlint.json` instead of maintaining two config files. Also
added `.markdownlintignore` (`target/`) since the glob was
incidentally linting vendored third-party docs copied into build
output by a dependency's build script.
- `Security audit` job: `cargo audit` correctly found a real advisory,
RUSTSEC-2026-0235 (vulnerable `rkyv` 0.7.46) — but it's pulled in only
because `rust_decimal` lists it as an optional dependency behind a
feature (`rkyv`) we never enable; confirmed no `rkyv` symbols are
linked into the release binary. Added a documented `ignore:` entry for
that specific advisory ID, since `cargo audit` scans the full
`Cargo.lock` graph regardless of which optional features are active.

- `main.rs` rewritten to a worker-pool architecture (4 workers + a single
writer task + a dedicated pool-cleanup task, coordinated via a
`tokio::sync::watch` shutdown signal on stdin EOF), matching the
sqlserver/dynamodb sibling plugins. A slow query on one connection no
longer blocks a concurrent `ping` or metadata call on another; the host
already tolerates out-of-order responses (it correlates by JSON-RPC `id`
via a `HashMap`, not arrival order), so this required no protocol change.
- Periodic idle-pool eviction: every 10 minutes, `client::cleanup_idle_pools()`
drops cached connection pools that currently have no checked-out
connections, so a long-running session that has connected to many
distinct targets doesn't pin idle TCP connections and pool memory for
the plugin's lifetime. Matches the sqlserver/dynamodb sibling plugins'
pattern exactly (`pool.status().size > pool.status().available` as the
keep predicate). Found missing during the same security-audit pass that
flagged "pool cleanup on shutdown" — investigation showed the host never
sends the plugin a `shutdown` RPC call at all (it kills the process
outright), so that specific checklist wording described something
unreachable; comparing sibling plugins surfaced this as the real,
exercisable gap instead. Added test-first (TDD): a unit test asserting
an idle pool gets evicted, written and confirmed RED (`cleanup_idle_pools`
didn't exist) before the function was implemented to GREEN.
- `save_blob_to_file` now validates `file_path` (empty, existing-directory,
or missing-parent-directory) before spending a DB round-trip on a write
that would fail anyway — a clearly attributed `-32602` error instead of
a bare OS error number surfacing after the query already ran. Not a
security boundary (the path comes from the frontend's native save
dialog), just a fast-fail. The builtin driver's identical gap is
untouched; this fix is plugin-only. Found during the security-audit pass.
- `startup_script` support: SQL supplied on the connection now runs on every
new pooled connection via a `deadpool-postgres` `post_create` hook, with a
preflight validation pass so a broken script fails fast with a clearly
attributed `Startup script failed: ...` error instead of a misleading
connection error. Matches the builtin driver's
`run_postgres_startup_script` behavior (`src-tauri/src/pool_manager.rs`).
Found missing during a security-audit pass — no parity test exercises
this field, so the 82/82 parity suite didn't catch the gap.
- `connection_string` support: when present, it's parsed via
`tokio_postgres::Config::from_str` and takes precedence over the discrete
host/port/database/username/password fields, matching the README's
documented behavior. Previously the field was parsed into
`ConnectionParams` but silently never consumed by `build_pool()`. Also
found during the security-audit pass.
- Plugin source (`Cargo.toml`, `Cargo.lock`, `.tabularium`, `src/`) imported
from `TabularisDB/tabularis`'s `plugins/postgres-plugin/` at commit
`ad765f3a` (82/82 parity tests green per that commit). This is a parallel
copy — the in-tree source has not been removed, and the two copies are
kept in sync manually pending a later decision to deprecate the in-tree
copy.
- `docs/planning/`: the 8 design documents that shaped this migration
(phase docs, both migration-plan variants, and the feature-gap audit
feeding Phase 2), copied from `tabularis`'s `.github/planning/`.
- `src/lib.rs` and `src/bin/test_plugin.rs`: extracted the plugin's module
tree into a library crate so the justfile's `repl` recipe (a local
JSON-RPC REPL) has a real binary to run, matching the oracle/dynamodb
sibling plugins' structure.
- Repo scaffolding: `LICENSE` (Apache-2.0), `.gitignore`, `.editorconfig`,
`CODEOWNERS`, `rust-toolchain.toml` (pinning `rustfmt`/`clippy`),
`.github/dependabot.yml`, `justfile` (build/test/lint/fmt/dev-install/
Expand All @@ -15,6 +125,16 @@
- `README.md` and `CLAUDE.md` describing the plugin's purpose, architecture,
and current migration status.

The plugin source itself (`Cargo.toml`, `src/`, `.tabularium`) has not been
migrated from `TabularisDB/tabularis`'s `plugins/postgres-plugin/` yet — this
release holds only the repo-level basics staged ahead of that migration.
### Changed

- `.tabularium`'s `name` field changed from `postgres-plugin` to
`postgresql` (and the redundant `id` field dropped) to match this repo's
own install-path/executable naming and the sibling-plugin convention of a
bare engine-name slug. This field is a permanent registry slug once
published, so it was fixed before any release.
- README's connection config table: `ssl_ca`/`ssl_cert`/`ssl_key` were
documented as a single group ("If using `verify-ca`/`verify-full`"), but
only `ssl_ca` (custom CA pinning) is actually implemented — matches the
builtin PostgreSQL driver, which also has no client-certificate support
(unlike its MySQL driver). Documentation corrected to describe only what
the plugin (and builtin) actually do.
Loading
Loading