Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b2ea9af
docs: record plugin completeness gaps
debba Aug 30, 2026
8dfc0a4
feat: support SQL Server connection strings
debba Aug 30, 2026
0fad03b
feat: add SQL Server BLOB export and preview
debba Aug 30, 2026
eaf4a6a
feat: apply plugin initialization settings
debba Aug 30, 2026
ddcbd98
feat: add SQL Server user management
debba Aug 30, 2026
3f70b91
feat: make unsupported RPC methods explicit
debba Aug 30, 2026
574d76d
feat: add registry-grade plugin manifest
debba Aug 30, 2026
4f15013
docs: add registry assets and user guide
debba Aug 30, 2026
0aedbe0
ci: align validation and release workflows
debba Aug 30, 2026
70c2b5a
fix(tooling): repair developer recipes
debba Aug 30, 2026
1718b31
chore(release): prepare 1.0.0 beta
debba Aug 30, 2026
ba1c968
docs(explain): freeze plugin parser contract
debba Aug 30, 2026
1af7488
feat(explain): add SQL Server parser package
debba Sep 2, 2026
e56e5e2
feat(explain): return raw SHOWPLAN XML
debba Sep 2, 2026
2fa0949
fix(sql): harden identifier construction
debba Sep 2, 2026
20102f5
fix(query): harden pagination semantics
debba Sep 2, 2026
52f321f
fix(types): harden SQL Server round trips
debba Sep 2, 2026
c98bd90
fix(errors): harden mapping and session recovery
debba Sep 2, 2026
6f0f55b
test(conformance): verify host response models
debba Sep 2, 2026
45a90b9
perf(runtime): bound pool and query memory
debba Sep 2, 2026
bcf9f0e
chore(release): close out release candidate
debba Sep 2, 2026
c84a811
ci: install pnpm with pnpm/action-setup instead of Corepack
debba Sep 3, 2026
3c5ef78
ci: validate the full manifest against the live registry schema
debba Sep 3, 2026
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
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/explain"
schedule:
interval: "weekly"
283 changes: 280 additions & 3 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,9 @@ on:
branches: [main]
pull_request:
branches: [main]
# Default types miss title-only edits. Label changes must also re-run the
# version suggestion because the prerelease channel comes from a label.
types: [opened, edited, reopened, synchronize, labeled, unlabeled]
schedule:
# Weekly cargo-audit sweep for advisories disclosed after dependencies land.
# Use an off-peak minute rather than :00 or :30.
Expand All@@ -24,16 +27,68 @@ jobs:
run: cargo build

- name: Run tests
# This crate has no library target, so --lib would fail. --bins keeps
# the live SQL Server test in its dedicated job below.
run: cargo test --bins
# This crate has no library target, so --lib would fail. Select the
# fixture-only conformance target while keeping live_db in its
# dedicated SQL Server job below.
run: cargo test --bins --test conformance

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

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

explain-package:
name: EXPLAIN parser package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

# Installs the pnpm version pinned by "packageManager" in
# explain/package.json. Corepack is not used because the copy bundled
# with Node 22.13 carries stale npm registry signing keys and fails
# with "Cannot find matching keyid" when it resolves pnpm.
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
package_json_file: explain/package.json

- name: Setup Node
uses: actions/setup-node@v5
with:
# pnpm 11.23 requires Node 22.13 or newer.
node-version: "22.13"

- name: Install dependencies
working-directory: explain
run: pnpm install --frozen-lockfile

- name: Typecheck
working-directory: explain
run: pnpm typecheck

- name: Test
working-directory: explain
run: pnpm test

- name: Build
working-directory: explain
run: pnpm build

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

# registry.tabularis.dev serves the driver-kind schema including the
# additive explain_parsers field from core PR #688, so the whole
# manifest is validated against the live validator.
- name: Validate manifest against the live registry schema
run: |
npx --yes @tabularium/cli validate .tabularium \
--registry https://registry.tabularis.dev --kind driver

live-db-integration:
name: Live SQL Server integration
runs-on: ubuntu-latest
Expand DownExpand Up@@ -73,6 +128,228 @@ jobs:
SQLSERVER_TEST_DATABASE: tabularis_test
run: cargo test --test live_db -- --test-threads=1

pr-title:
name: PR title (Conventional Commits)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

version-suggestion:
name: Version suggestion
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

# Classify the PR title's Conventional Commits type + breaking-change
# flag into a version-bump class. Requires the prerelease:* label to
# know which channel (alpha/beta/rc/stable) to suggest — see README's
# "Contributing: PR Titles & Versioning" for the full convention.
- name: Classify PR title and resolve prerelease channel
id: classify
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_LABELS: ${{ toJson(github.event.pull_request.labels) }}
run: |
PATTERN='^([a-zA-Z]+)(\(([^)]+)\))?(!)?: (.+)$'
if [[ "$PR_TITLE" =~ $PATTERN ]]; then
TYPE="${BASH_REMATCH[1]}"
BANG="${BASH_REMATCH[4]}"
else
echo "::error::PR title does not match Conventional Commits format (type: subject) — cannot classify."
exit 1
fi

BREAKING=false
[ -n "$BANG" ] && BREAKING=true
if echo "$PR_BODY" | grep -qiE "^BREAKING[ -]CHANGE:"; then
BREAKING=true
fi

case "$TYPE" in
feat) CLASS="minor" ;;
fix|refactor|perf) CLASS="patch" ;;
docs|style|chore|test|ci|build) CLASS="none" ;;
*) CLASS="none" ;;
esac
[ "$BREAKING" = true ] && CLASS="major"

CHANNEL=$(echo "$PR_LABELS" | jq -r '[.[] | select(.name | startswith("prerelease:")) | .name][0] // ""' | sed 's/^prerelease://')
if [ -z "$CHANNEL" ]; then
echo "::error::No prerelease:alpha|beta|rc|stable label found on this PR. Add one so the version suggestion knows which channel to target — see README's 'Contributing: PR Titles & Versioning'."
exit 1
fi
case "$CHANNEL" in
alpha|beta|rc|stable) ;;
*) echo "::error::Unrecognized prerelease label value '$CHANNEL' — expected alpha, beta, rc, or stable."; exit 1 ;;
esac

{
echo "type=$TYPE"
echo "breaking=$BREAKING"
echo "class=$CLASS"
echo "channel=$CHANNEL"
} >> "$GITHUB_OUTPUT"

- name: Resolve baseline version
id: baseline
run: |
git fetch origin main --tags --quiet
TAG=$(git -C . describe --tags --abbrev=0 origin/main 2>/dev/null || true)
if [ -n "$TAG" ]; then
BASELINE="${TAG#v}"
else
BASELINE=$(git show origin/main:.tabularium | jq -r .version)
fi
echo "version=$BASELINE" >> "$GITHUB_OUTPUT"

- name: Compute suggestion, manage comment
uses: actions/github-script@v9
with:
script: |
const classification = "${{ steps.classify.outputs.class }}";
const channel = "${{ steps.classify.outputs.channel }}";
const type = "${{ steps.classify.outputs.type }}";
const breaking = "${{ steps.classify.outputs.breaking }}" === "true";
const baselineStr = "${{ steps.baseline.outputs.version }}";
const marker = "<!-- version-suggestion-bot";

function parseVersion(v) {
const m = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z]+)\.(\d+))?$/);
if (!m) throw new Error(`Cannot parse version: ${v}`);
return {
major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]),
stage: m[4] || null, stageNum: m[5] ? Number(m[5]) : null,
};
}
function formatVersion(v) {
const base = `${v.major}.${v.minor}.${v.patch}`;
return v.stage ? `${base}-${v.stage}.${v.stageNum}` : base;
}
function bumpStable(v, cls) {
const out = { major: v.major, minor: v.minor, patch: v.patch, stage: null, stageNum: null };
if (cls === "major") { out.major += 1; out.minor = 0; out.patch = 0; }
else if (cls === "minor") { out.minor += 1; out.patch = 0; }
else if (cls === "patch") { out.patch += 1; }
return out;
}
function computeNextVersion(baselineStr, classification, channelLabel) {
const baseline = parseVersion(baselineStr);
if (channelLabel === "stable") {
if (baseline.stage) {
return formatVersion({ major: baseline.major, minor: baseline.minor, patch: baseline.patch, stage: null, stageNum: null });
}
return formatVersion(bumpStable(baseline, classification));
}
if (baseline.stage === channelLabel) {
return formatVersion({ ...baseline, stageNum: baseline.stageNum + 1 });
}
let base = { major: baseline.major, minor: baseline.minor, patch: baseline.patch };
if (!baseline.stage) {
const bumped = bumpStable(baseline, classification);
base = { major: bumped.major, minor: bumped.minor, patch: bumped.patch };
}
return formatVersion({ ...base, stage: channelLabel, stageNum: 1 });
}

const prNumber = context.payload.pull_request.number;

// Find our most recent, not-yet-minimized comment on this PR.
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
const ours = comments.filter(c => c.body.includes(marker));
const previous = ours.length ? ours[ours.length - 1] : null;

let previousClassification = null;
if (previous) {
const m = previous.body.match(/classification=([\w-]+:[\w-]+:[\w-]+)/);
previousClassification = m ? m[1] : null;
}
const currentClassification = `${type}:${classification}:${channel}`;

if (classification === "none") {
if (previous && previousClassification !== currentClassification) {
// Was suggesting something (or saying "none" for a different
// reason/channel), now saying "none" for this reason — say so
// once, then stop.
await minimizePrevious();
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `No release needed for this PR (\`${type}\`).\n\n${marker} classification=${currentClassification} -->`,
});
}
// Otherwise: never suggested anything, or already said "none" — stay silent.
return;
}

if (previous && previousClassification === currentClassification) {
// Meaningful classification hasn't changed since the last comment.
return;
}

async function minimizePrevious() {
if (!previous) return;
// REST comment objects expose node_id directly — no separate
// lookup needed to get the GraphQL node id.
await github.graphql(
`mutation($id: ID!) { minimizeComment(input: { subjectId: $id, classifier: OUTDATED }) { clientMutationId } }`,
{ id: previous.node_id }
);
}

const suggested = computeNextVersion(baselineStr, classification, channel);
const tag = `v${suggested}`;

await minimizePrevious();

const breakingNote = breaking ? " (breaking change)" : "";
const body = [
`### Version suggestion`,
``,
`Based on this PR's title (\`${type}\`${breakingNote}) and the \`prerelease:${channel}\` label:`,
``,
`| | |`,
`|---|---|`,
`| Current | \`${baselineStr}\` |`,
`| Suggested next tag | \`${tag}\` |`,
``,
`This is informational only — no tag or release is created automatically yet.`,
``,
`${marker} classification=${currentClassification} -->`,
].join("\n");

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});

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
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/publish-explain.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
name: Publish EXPLAIN parser

on:
push:
tags:
- "explain-v*"

permissions:
contents: read
id-token: write

jobs:
publish:
name: Publish npm package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

# See ci.yml: pnpm comes from the pinned "packageManager" field, not
# from the Corepack copy bundled with Node.
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
package_json_file: explain/package.json

- name: Setup Node
uses: actions/setup-node@v5
with:
# pnpm 11.23 requires Node 22.13 or newer.
node-version: "22.13"
registry-url: "https://registry.npmjs.org"

- name: Check tag matches package and plugin versions
run: |
TAG_VERSION="${GITHUB_REF_NAME#explain-v}"
PACKAGE_VERSION=$(jq -r .version explain/package.json)
PLUGIN_VERSION=$(jq -r .version .tabularium)
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match explain/package.json ($PACKAGE_VERSION)"
exit 1
fi
if [ "$PACKAGE_VERSION" != "$PLUGIN_VERSION" ]; then
echo "::error::EXPLAIN package version ($PACKAGE_VERSION) does not match plugin version ($PLUGIN_VERSION)"
exit 1
fi

- name: Install, typecheck, test and build
working-directory: explain
run: |
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm build

- name: Publish
working-directory: explain
run: pnpm publish --access public --provenance --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Loading
Loading