dbctl 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. dbctl-0.1.1/.dbctl/connections.yaml +150 -0
  2. dbctl-0.1.1/.dbctl/operations.yaml +95 -0
  3. dbctl-0.1.1/.github/workflows/ci.yml +165 -0
  4. dbctl-0.1.1/.gitignore +35 -0
  5. dbctl-0.1.1/CHANGELOG.md +81 -0
  6. dbctl-0.1.1/PKG-INFO +323 -0
  7. dbctl-0.1.1/README.md +287 -0
  8. dbctl-0.1.1/dbctl/__init__.py +5 -0
  9. dbctl-0.1.1/dbctl/__main__.py +6 -0
  10. dbctl-0.1.1/dbctl/audit.py +74 -0
  11. dbctl-0.1.1/dbctl/cli.py +880 -0
  12. dbctl-0.1.1/dbctl/config.py +263 -0
  13. dbctl-0.1.1/dbctl/connections.py +52 -0
  14. dbctl-0.1.1/dbctl/db.py +117 -0
  15. dbctl-0.1.1/dbctl/execute.py +117 -0
  16. dbctl-0.1.1/dbctl/init.py +159 -0
  17. dbctl-0.1.1/dbctl/multi.py +53 -0
  18. dbctl-0.1.1/dbctl/operations.py +40 -0
  19. dbctl-0.1.1/dbctl/reports.py +136 -0
  20. dbctl-0.1.1/dbctl/runtime.py +110 -0
  21. dbctl-0.1.1/dbctl/tunnels/__init__.py +20 -0
  22. dbctl-0.1.1/dbctl/tunnels/base.py +71 -0
  23. dbctl-0.1.1/dbctl/tunnels/direct.py +28 -0
  24. dbctl-0.1.1/dbctl/tunnels/ssh.py +73 -0
  25. dbctl-0.1.1/dbctl/tunnels/ssm.py +142 -0
  26. dbctl-0.1.1/docker-compose.yml +72 -0
  27. dbctl-0.1.1/docs/ACTION_OUTPUT.md +99 -0
  28. dbctl-0.1.1/docs/DESIGN.md +231 -0
  29. dbctl-0.1.1/docs/SESSION_STATE.md +126 -0
  30. dbctl-0.1.1/docs/connections.md +233 -0
  31. dbctl-0.1.1/docs/operations.md +281 -0
  32. dbctl-0.1.1/docs/tutorial.md +690 -0
  33. dbctl-0.1.1/pyproject.toml +66 -0
  34. dbctl-0.1.1/seed/mssql.sql +66 -0
  35. dbctl-0.1.1/seed/mysql.sql +65 -0
  36. dbctl-0.1.1/seed/postgres.sql +66 -0
  37. dbctl-0.1.1/tests/test_bastion_tags.py +167 -0
  38. dbctl-0.1.1/tests/test_regressions.py +80 -0
  39. dbctl-0.1.1/tests/test_smoke.py +282 -0
  40. dbctl-0.1.1/uv.lock +981 -0
@@ -0,0 +1,150 @@
1
+ # Sample dbctl config for the docker-compose test fleet in this repo.
2
+ # cp .dbctl/connections.yaml ~/.dbctl/connections.yaml
3
+ # cp .dbctl/operations.yaml ~/.dbctl/operations.yaml
4
+ # This file points at the three databases started by `docker compose up -d`.
5
+
6
+ connections:
7
+ pg:
8
+ description: "Postgres dev (docker compose)"
9
+ aliases: [postgres]
10
+ type: direct
11
+ driver: postgresql+psycopg
12
+ database: app
13
+ username: app_admin
14
+ password_env: DBCTL_PG_PASSWORD
15
+ direct: { host: 127.0.0.1, port: 5433 }
16
+ healthcheck: { query: "SELECT 1", timeout_seconds: 5 }
17
+ info:
18
+ - name: row_counts
19
+ description: "Top tables by row count"
20
+ query: |
21
+ SELECT relname AS table, n_live_tup AS rows
22
+ FROM pg_stat_user_tables
23
+ ORDER BY n_live_tup DESC LIMIT 10
24
+ - name: top_users
25
+ query: "SELECT name, quota_daily, is_active FROM users ORDER BY quota_daily DESC LIMIT 10"
26
+ safety:
27
+ confirm: true
28
+ read_only: false
29
+
30
+ my:
31
+ description: "MySQL dev (docker compose)"
32
+ aliases: [mysql]
33
+ type: direct
34
+ driver: mysql+pymysql
35
+ database: app
36
+ username: app_admin
37
+ password_env: DBCTL_MY_PASSWORD
38
+ direct: { host: 127.0.0.1, port: 3307 }
39
+ healthcheck: { query: "SELECT 1" }
40
+ info:
41
+ - name: top_users
42
+ query: "SELECT name, quota_daily, is_active FROM users ORDER BY quota_daily DESC LIMIT 10"
43
+ safety:
44
+ confirm: true
45
+ read_only: false
46
+
47
+ ms:
48
+ description: "SQL Server dev (docker compose)"
49
+ aliases: [mssql]
50
+ type: direct
51
+ driver: mssql+pyodbc
52
+ database: app
53
+ username: sa
54
+ password_env: DBCTL_MS_PASSWORD
55
+ # SQL Server needs a configured ODBC driver on the host. Use pyodbc's
56
+ # connection string by overriding connect_args in your own config:
57
+ # connect_args: { driver: "ODBC Driver 18 for SQL Server", TrustServerCertificate: "yes" }
58
+ direct: { host: 127.0.0.1, port: 1434 }
59
+ healthcheck: { query: "SELECT 1" }
60
+ info:
61
+ - name: top_users
62
+ query: "SELECT TOP 10 name, quota_daily, is_active FROM dbo.users ORDER BY quota_daily DESC"
63
+ safety:
64
+ confirm: true
65
+ read_only: true
66
+
67
+ # --------------------------------------------------------------------- #
68
+ # Reference templates for tunneled Postgres connections.
69
+ # These are NOT meant to connect to anything in the docker compose fleet;
70
+ # they are full-blown reference examples you can copy + edit in your own
71
+ # ~/.dbctl/connections.yaml. They are marked read_only so accidental runs
72
+ # are safe.
73
+ # --------------------------------------------------------------------- #
74
+
75
+ # AWS SSM port-forward through an EC2 bastion to a private RDS Postgres.
76
+ # Requires the `aws` CLI on PATH with an active SSO session in
77
+ # ~/.aws/cache/sso/*.json for the named `profile`.
78
+ pg-ssm:
79
+ description: "REFERENCE: Postgres via AWS SSM (edit before using)"
80
+ aliases: []
81
+ type: ssm
82
+ driver: postgresql+psycopg
83
+ database: app
84
+ username: app_admin
85
+ password_env: DBCTL_PG_SSM_PASSWORD
86
+ ssm:
87
+ region: eu-west-1
88
+ profile: prod # AWS SSO profile; tokens in ~/.aws/cache
89
+ # Resolve the bastion at tunnel-open time via aws ec2 describe-instances.
90
+ # Useful when the bastion is in an ASG / is replaced often — the tag
91
+ # set stays stable while the instance id rotates.
92
+ bastion_tags: { Name: bastion-prod, Env: prod }
93
+ # alt: hardcode the id and skip the ec2:DescribeInstances call:
94
+ # bastion_instance_id: i-0abcd1234ef
95
+ remote_host: mydb.xxxx.eu-west-1.rds.amazonaws.com
96
+ remote_port: 5432
97
+ local_port: 0 # 0 = dbctl picks a free local port
98
+ ssm_document: AWS-StartPortForwardingSessionToRemoteHost
99
+ healthcheck: { query: "SELECT 1" }
100
+ info:
101
+ - name: row_counts
102
+ description: "Top tables by row count"
103
+ query: |
104
+ SELECT relname AS table, n_live_tup AS rows
105
+ FROM pg_stat_user_tables
106
+ ORDER BY n_live_tup DESC LIMIT 10
107
+ - name: active_conns
108
+ description: "Active backend connections"
109
+ query: "SELECT count(*) FROM pg_stat_activity"
110
+ - name: db_size
111
+ description: "Database size in bytes"
112
+ query: "SELECT pg_database_size(current_database()) AS bytes"
113
+ safety:
114
+ confirm: true
115
+ read_only: true
116
+ allowed_operations: []
117
+
118
+ # Classic SSH port-forward through a bastion host to an internal Postgres.
119
+ # Requires the `ssh` CLI on PATH and a working key file at `identity`
120
+ # (tilde is expanded). The operator's ~/.ssh/config + agent keep working.
121
+ pg-ssh:
122
+ description: "REFERENCE: Postgres via SSH tunnel (edit before using)"
123
+ aliases: []
124
+ type: ssh
125
+ driver: postgresql+psycopg
126
+ database: app
127
+ username: app_admin
128
+ password_env: DBCTL_PG_SSH_PASSWORD
129
+ ssh:
130
+ host: bastion.example.com # reachable from your workstation
131
+ user: ec2-user
132
+ identity: ~/.ssh/id_rsa # ~ is expanded by dbctl
133
+ remote_host: db.internal # the database's own hostname, as the bastion sees it
134
+ remote_port: 5432
135
+ local_port: 0 # 0 = dbctl picks a free local port
136
+ port: 22 # bastion's SSH port
137
+ healthcheck: { query: "SELECT 1" }
138
+ info:
139
+ - name: row_counts
140
+ description: "Top tables by row count"
141
+ query: |
142
+ SELECT relname AS table, n_live_tup AS rows
143
+ FROM pg_stat_user_tables
144
+ ORDER BY n_live_tup DESC LIMIT 10
145
+ - name: active_conns
146
+ query: "SELECT count(*) FROM pg_stat_activity"
147
+ safety:
148
+ confirm: true
149
+ read_only: true
150
+ allowed_operations: []
@@ -0,0 +1,95 @@
1
+ # Sample dbctl operations.
2
+ # Each operation is a parameterised SQL block declaring its inputs; the CLI
3
+ # builds one Click option per parameter (positional + keyword).
4
+
5
+ operations:
6
+ add-user:
7
+ description: "Create or update an application user (Daily quota by default)"
8
+ scope: single
9
+ mode: execute
10
+ confirm: true
11
+ parameters:
12
+ - { name: name, type: string, required: true, position: 1, description: "Unique user name" }
13
+ - { name: quota, type: integer, required: true, position: 2, description: "Daily quota (used as daily limit)" }
14
+ - { name: type, type: string, default: "Daily", position: 3, description: "Account type" }
15
+ sql: |
16
+ INSERT INTO users (name, quota_daily, quota_yearly, type)
17
+ VALUES ($name, $quota, $quota * 365, $type)
18
+ ON CONFLICT (name) DO UPDATE
19
+ SET quota_daily = EXCLUDED.quota_daily,
20
+ quota_yearly = EXCLUDED.quota_yearly,
21
+ type = EXCLUDED.type,
22
+ updated_at = NOW()
23
+
24
+ list-users:
25
+ description: "List users (top N)"
26
+ scope: single
27
+ mode: fetch
28
+ output: table
29
+ parameters:
30
+ - { name: limit, type: integer, default: 10, position: 1 }
31
+ sql: |
32
+ SELECT name, quota_daily, quota_yearly, type, is_active
33
+ FROM users
34
+ ORDER BY quota_daily DESC
35
+ LIMIT $limit
36
+
37
+ find-user:
38
+ description: "Find a user by name prefix"
39
+ scope: single
40
+ mode: fetch
41
+ parameters:
42
+ - { name: prefix, type: string, required: true, position: 1 }
43
+ sql: |
44
+ SELECT name, quota_daily, is_active FROM users WHERE name ILIKE $prefix || '%'
45
+
46
+ report-logs:
47
+ description: "Summarise logs by level between two dates"
48
+ scope: single
49
+ mode: fetch
50
+ output: table
51
+ parameters:
52
+ - { name: since, type: string, required: true, position: 1, description: "ISO date (e.g. 2025-01-01)" }
53
+ - { name: until, type: string, required: true, position: 2, description: "ISO date" }
54
+ sql: |
55
+ SELECT level, COUNT(*) AS events, MIN(created_at) AS first_seen, MAX(created_at) AS last_seen
56
+ FROM logs
57
+ WHERE created_at >= $since::timestamp
58
+ AND created_at < $until::timestamp
59
+ GROUP BY level
60
+ ORDER BY events DESC
61
+
62
+ # --------------------------------------------------------------------------- #
63
+ # multi-database operations - invoked as `dbctl diff A B --op <name>`
64
+ # --------------------------------------------------------------------------- #
65
+ user-count:
66
+ description: "Compare user counts between two databases"
67
+ scope: multi
68
+ mode: diff
69
+ roles: [src, trg]
70
+ queries:
71
+ src: "SELECT 'users' AS t, COUNT(*) AS n FROM users"
72
+ trg: "SELECT 'users' AS t, COUNT(*) AS n FROM users"
73
+ diff:
74
+ key: [t]
75
+ show: [n]
76
+
77
+ compare-quotas:
78
+ description: "Side-by-side quota summary across two databases"
79
+ scope: multi
80
+ mode: diff
81
+ roles: [src, trg]
82
+ parameters:
83
+ - { name: period, type: string, default: "Daily", position: 1 }
84
+ queries:
85
+ src: |
86
+ SELECT period AS t, COUNT(*) AS n_rows, SUM(limit_value) AS total_limit
87
+ FROM quotas JOIN users ON quotas.user_id = users.id
88
+ WHERE period = $period GROUP BY period
89
+ trg: |
90
+ SELECT period AS t, COUNT(*) AS n_rows, SUM(limit_value) AS total_limit
91
+ FROM quotas JOIN users ON quotas.user_id = users.id
92
+ WHERE period = $period GROUP BY period
93
+ diff:
94
+ key: [t]
95
+ show: [n_rows, total_limit]
@@ -0,0 +1,165 @@
1
+ name: CI
2
+
3
+ # Run on every push to main and on PRs touching python / packaging /
4
+ # workflow files. Use path filters so doc-only edits don't burn CI minutes.
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ tags: ["v*"]
9
+ pull_request:
10
+ branches: [main]
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ # Cancel any in-progress run when a new commit lands on the same ref —
17
+ # saves CI minutes on rapid force-pushes and PR update churn.
18
+ concurrency:
19
+ group: ci-${{ github.ref }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ # ----------------------------------------------------------------------- #
24
+ # lint + typecheck — fast, no project deps installed
25
+ # ----------------------------------------------------------------------- #
26
+ lint:
27
+ runs-on: ubuntu-latest
28
+ timeout-minutes: 5
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - name: Install uv
32
+ uses: astral-sh/setup-uv@v3
33
+ with:
34
+ enable-cache: true
35
+ cache-dependency-glob: "uv.lock"
36
+ - name: Set up Python
37
+ run: uv python install 3.12
38
+ - name: Install dev deps
39
+ # --frozen keeps CI honest: if uv.lock is out of date the job
40
+ # fails rather than silently re-resolving.
41
+ run: uv sync --frozen --extra dev
42
+ - name: Ruff (annotations PR-native)
43
+ # --output-format=github emits ::error/::warning annotations that
44
+ # show up directly on PR file diffs and the run summary.
45
+ run: uv run ruff check --output-format=github dbctl tests
46
+ - name: Ruff format check
47
+ run: uv run ruff format --check dbctl tests
48
+ - name: Mypy
49
+ # Non-blocking: the codebase has pre-existing annotation debt that
50
+ # is being paid down incrementally. Remove `continue-on-error`
51
+ # once the count reaches zero.
52
+ run: uv run mypy dbctl
53
+ continue-on-error: true
54
+
55
+ # ----------------------------------------------------------------------- #
56
+ # test — matrix across supported Python versions + OSes
57
+ # ----------------------------------------------------------------------- #
58
+ test:
59
+ needs: lint
60
+ runs-on: ${{ matrix.os }}
61
+ timeout-minutes: 15
62
+ strategy:
63
+ fail-fast: false
64
+ matrix:
65
+ # requires-python = ">=3.11" in pyproject.toml
66
+ python-version: ["3.11", "3.12", "3.13"]
67
+ os: [ubuntu-latest, macos-latest]
68
+ exclude:
69
+ # Trim the matrix: macos + 3.11 + 3.13 stay, but skip 3.13 on
70
+ # mac to keep minutes in check (Linux covers the linux/3.13 case).
71
+ - os: macos-latest
72
+ python-version: "3.11"
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+ - name: Install uv
76
+ uses: astral-sh/setup-uv@v3
77
+ with:
78
+ enable-cache: true
79
+ cache-dependency-glob: "uv.lock"
80
+ - name: Set up Python ${{ matrix.python-version }}
81
+ run: uv python install ${{ matrix.python-version }}
82
+ - name: Install deps
83
+ # mssql extra needs ODBC at the system level (libodbc.so), which
84
+ # isn't on these runners by default; tests don't need it.
85
+ run: uv sync --frozen --extra dev --extra postgres --extra mysql
86
+ - name: Run pytest with coverage
87
+ # --junitxml so failed runs still produce a report artifact;
88
+ # --cov + --cov-report=xml for coverage upload.
89
+ run: |
90
+ uv run pytest tests/ \
91
+ --junitxml=reports/junit-${{ matrix.os }}-${{ matrix.python-version }}.xml \
92
+ --cov=dbctl \
93
+ --cov-report=xml:reports/coverage-${{ matrix.os }}-${{ matrix.python-version }}.xml \
94
+ --cov-report=term
95
+ - name: Upload test results
96
+ if: always() # upload even on failure so flaky-test data survives
97
+ uses: actions/upload-artifact@v4
98
+ with:
99
+ name: test-results-${{ matrix.os }}-${{ matrix.python-version }}
100
+ path: reports/
101
+ if-no-files-found: error
102
+ retention-days: 14
103
+ - name: Upload coverage to Codecov
104
+ # Codecov works tokenless on public repos via OIDC. On private
105
+ # repos add a CODECOV_TOKEN secret and uncomment `token:`.
106
+ if: always()
107
+ uses: codecov/codecov-action@v4
108
+ with:
109
+ files: reports/coverage-${{ matrix.os }}-${{ matrix.python-version }}.xml
110
+ disable_search: true
111
+ fail_ci_if_error: false
112
+ # token: ${{ secrets.CODECOV_TOKEN }}
113
+
114
+ # ----------------------------------------------------------------------- #
115
+ # build — wheel + sdist; upload artifact for inspection on every run
116
+ # ----------------------------------------------------------------------- #
117
+ build:
118
+ needs: test
119
+ runs-on: ubuntu-latest
120
+ timeout-minutes: 5
121
+ steps:
122
+ - uses: actions/checkout@v4
123
+ - name: Install uv
124
+ uses: astral-sh/setup-uv@v3
125
+ with:
126
+ enable-cache: true
127
+ cache-dependency-glob: "uv.lock"
128
+ - name: Set up Python
129
+ run: uv python install 3.12
130
+ - name: Build distributions
131
+ run: uv build
132
+ - name: Verify metadata
133
+ run: uv run --with twine twine check dist/*
134
+ - name: Upload artifacts
135
+ uses: actions/upload-artifact@v4
136
+ with:
137
+ name: dist
138
+ path: dist/
139
+ if-no-files-found: error
140
+
141
+ # ----------------------------------------------------------------------- #
142
+ # publish — only on a pushed v* tag, gated on test + build
143
+ # ----------------------------------------------------------------------- #
144
+ publish:
145
+ needs: [test, build]
146
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
147
+ runs-on: ubuntu-latest
148
+ timeout-minutes: 5
149
+ # Dedicated environment — configure "pypi" under repo settings →
150
+ # Environments with required reviewers/wait-for-approval before the
151
+ # job can publish. PyPI trusted publishing needs id-token: write.
152
+ environment:
153
+ name: pypi
154
+ url: https://pypi.org/p/dbctl
155
+ permissions:
156
+ contents: read
157
+ id-token: write # OIDC trusted publishing
158
+ steps:
159
+ - name: Download artifacts
160
+ uses: actions/download-artifact@v4
161
+ with:
162
+ name: dist
163
+ path: dist/
164
+ - name: Publish to PyPI
165
+ uses: pypa/gh-action-pypi-publish@release/v1
dbctl-0.1.1/.gitignore ADDED
@@ -0,0 +1,35 @@
1
+ # python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ *.egg
7
+ build/
8
+ dist/
9
+ .eggs/
10
+
11
+ # virtual envs
12
+ .venv/
13
+ venv/
14
+ env/
15
+
16
+ # test / lint caches
17
+ .pytest_cache/
18
+ .ruff_cache/
19
+ .mypy_cache/
20
+ .coverage
21
+ htmlcov/
22
+ .tox/
23
+
24
+ # editors / OS
25
+ .idea/
26
+ .vscode/
27
+ .DS_Store
28
+ *.swp
29
+ *.swo
30
+ *~
31
+
32
+ # local dbctl runtime state (sample configs live under .dbctl/ in the repo;
33
+ # ignore the per-user history/profile data that dbctl writes at runtime)
34
+ .dbctl/history.jsonl
35
+ .dbctl/profiles/
@@ -0,0 +1,81 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.1] — 2026-08-01
9
+
10
+ Retag of the initial public release (v0.1.0 publish failed; the trusted
11
+ publisher had not yet been registered on PyPI). No code changes — only
12
+ the version bump.
13
+
14
+ ## [0.1.0] — 2026-08-01
15
+
16
+ Initial public release.
17
+
18
+ ### Added
19
+
20
+ - **Connection registry** (`~/.dbctl/connections.yaml`) validated with pydantic
21
+ v2, supporting three tunnel types:
22
+ - `ssm` — AWS SSM port-forward through an EC2 bastion, shelled out to the
23
+ `aws` CLI. Supports `bastion_instance_id` or `bastion_tags` (resolved via
24
+ `aws ec2 describe-instances`).
25
+ - `ssh` — classic `ssh -N -L` port-forward, shelled out to the `ssh` CLI
26
+ with `ExitOnForwardFailure=yes` and `StrictHostKeyChecking=accept-new`.
27
+ - `direct` — no tunnel, connect to upstream host:port.
28
+ - **Operations registry** (`~/.dbctl/operations.yaml`) with two scopes:
29
+ - `single` — `dbctl <conn> <op> ...` with `execute` / `fetch` / `fetch_one` /
30
+ `script` / `upsert` modes.
31
+ - `multi` — `dbctl diff <op> <src> <trg> ...` with `diff` mode (side-by-side
32
+ join on a `key`); `compare` / `sync` modes reserved for v2.
33
+ - **Dynamic CLI** built from the registries: one Click subcommand per
34
+ connection, one per declared operation. Positional params via `position:`,
35
+ keyword params via `--flag`, all generated from the YAML declaration.
36
+ - **`$name` placeholders** rewritten to SQLAlchemy bind-params (`:name`) —
37
+ values are always parameterised, never string-interpolated.
38
+ - **Safety model**:
39
+ - `safety.confirm: true` makes DML **dry-run by default**; `--apply`
40
+ commits, `--yes` skips the prompt. Confirm happens *before* the
41
+ transaction opens so `N` leaves the DB untouched.
42
+ - `safety.read_only: true` blocks every DML op.
43
+ - `safety.allowed_operations: [...]` whitelists op names.
44
+ - **Audit log** at `~/.dbctl/history.jsonl` — one JSON event per run; secret
45
+ parameters redacted. `dbctl history list` / `dbctl <conn> history` /
46
+ `dbctl <conn> again` (re-run last).
47
+ - **Dashboard** — `dbctl` bare shows connections table; `dbctl <conn>` shows
48
+ a connection page with health, info queries, and available ops.
49
+ - **`dbctl doctor`** — healthcheck every connection.
50
+ - **`dbctl init`** — interactive wizard that writes/merges a new connection
51
+ and tests the tunnel + healthcheck before saving.
52
+ - **Shell completion** via `dbctl --install-completion bash|zsh|fish`.
53
+ - **`--profile <name>`** — swap config dir to `~/.dbctl/profiles/<name>/`.
54
+ - **Connection aliases** — `prod` resolves to `db1`, etc.
55
+ - **Bundled test fleet** — `docker-compose.yml` brings up postgres on
56
+ `:5433`, mysql on `:3307`, mssql on `:1434` with the same four-table
57
+ schema (`users`, `quotas`, `usage`, `logs`) and slightly different sample
58
+ data for diff testing.
59
+ - **15 unit tests** against in-memory SQLite covering placeholder rewriting,
60
+ parameter binding/coercion, mode routing, side-by-side diff, and audit
61
+ redaction.
62
+
63
+ ### Documentation
64
+
65
+ - `README.md` — usage, install, quick start, config layout, safety model.
66
+ - `docs/connections.md` — full `connections.yaml` reference with examples.
67
+ - `docs/operations.md` — full `operations.yaml` reference with the safety
68
+ check matrix.
69
+ - `docs/DESIGN.md` — architecture, layering, dynamic CLI, placeholder
70
+ semantics, confirm-before-commit invariant, exit codes.
71
+
72
+ ### Known limitations (v1)
73
+
74
+ - `mode: script` runs only the first statement in v1; multi-statement
75
+ scripts are v2.
76
+ - `mode: upsert` is reserved — the autoload/dialect-aware conflict logic is
77
+ v2.
78
+ - Operations are dialect-specific (no per-connection overrides).
79
+ - `${var}` identifier interpolation is intentionally absent (would be a SQL
80
+ injection vector with naive `str.replace`); use one op per table for now.
81
+ - Multi-DB tunnels open sequentially (parallel is v2).