dbctl 0.6.2__tar.gz → 0.6.3__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.
- {dbctl-0.6.2 → dbctl-0.6.3}/.dbctl/connections.yaml +53 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/CHANGELOG.md +42 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/PKG-INFO +7 -4
- {dbctl-0.6.2 → dbctl-0.6.3}/README.md +3 -2
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/config.py +14 -2
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/db.py +23 -6
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/init.py +11 -1
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/multi.py +2 -2
- {dbctl-0.6.2 → dbctl-0.6.3}/docs/connections.md +67 -1
- {dbctl-0.6.2 → dbctl-0.6.3}/pyproject.toml +4 -2
- {dbctl-0.6.2 → dbctl-0.6.3}/.dbctl/operations.yaml +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/.github/workflows/ci.yml +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/.github-local/ci.yml +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/.gitignore +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/Makefile +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/__init__.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/__main__.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/audit.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/cli.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/connections.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/execute.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/operations.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/reports.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/runtime.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/__init__.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/base.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/direct.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/k8s.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/ssh.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/ssm.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/docker-compose.yml +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/docs/ACTION_OUTPUT.md +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/docs/DESIGN.md +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/docs/SESSION_STATE.md +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/docs/logo.png +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/docs/logo_small.png +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/docs/operations.md +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/docs/tutorial.md +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/seed/mssql.sql +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/seed/mysql.sql +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/seed/postgres.sql +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_bastion_tags.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_connections_loader.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_k8s_tunnel.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_regressions.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_smoke.py +0 -0
- {dbctl-0.6.2 → dbctl-0.6.3}/uv.lock +0 -0
|
@@ -226,3 +226,56 @@ connections:
|
|
|
226
226
|
safety:
|
|
227
227
|
confirm: true
|
|
228
228
|
read_only: true
|
|
229
|
+
|
|
230
|
+
# --------------------------------------------------------------------- #
|
|
231
|
+
# Oracle, SQLite, DuckDB reference templates.
|
|
232
|
+
# --------------------------------------------------------------------- #
|
|
233
|
+
|
|
234
|
+
# Oracle Database — oracledb thin mode (pure Python, no Instant Client).
|
|
235
|
+
# Use `database:` for the service name (or SID). Healthcheck uses
|
|
236
|
+
# `SELECT 1 FROM DUAL` which is the Oracle convention.
|
|
237
|
+
prod-oracle:
|
|
238
|
+
description: "REFERENCE: Oracle via oracledb thin mode (edit before using)"
|
|
239
|
+
aliases: []
|
|
240
|
+
type: direct
|
|
241
|
+
driver: oracle+oracledb
|
|
242
|
+
database: ORCLPDB1
|
|
243
|
+
username: app_admin
|
|
244
|
+
password: "<set-me>"
|
|
245
|
+
direct: { host: db.internal, port: 1521 }
|
|
246
|
+
healthcheck: { query: "SELECT 1 FROM DUAL", timeout_seconds: 10 }
|
|
247
|
+
safety:
|
|
248
|
+
confirm: true
|
|
249
|
+
read_only: true
|
|
250
|
+
|
|
251
|
+
# Local SQLite — file-based, no host/port needed (but config schema
|
|
252
|
+
# requires a `direct:` block). Use `url:` mode for a cleaner config:
|
|
253
|
+
# url: "sqlite:////absolute/path/to/mydata.db"
|
|
254
|
+
local-sqlite:
|
|
255
|
+
description: "REFERENCE: Local SQLite (edit path before using)"
|
|
256
|
+
aliases: []
|
|
257
|
+
type: direct
|
|
258
|
+
driver: sqlite
|
|
259
|
+
database: /tmp/mydata.db
|
|
260
|
+
username: ""
|
|
261
|
+
password: ""
|
|
262
|
+
direct: { host: localhost, port: 0 }
|
|
263
|
+
healthcheck: { query: "SELECT 1" }
|
|
264
|
+
safety:
|
|
265
|
+
confirm: true
|
|
266
|
+
read_only: true
|
|
267
|
+
|
|
268
|
+
# Local DuckDB — file-based (or ":memory:" for in-memory analytics).
|
|
269
|
+
local-duckdb:
|
|
270
|
+
description: "REFERENCE: Local DuckDB (edit path before using)"
|
|
271
|
+
aliases: []
|
|
272
|
+
type: direct
|
|
273
|
+
driver: duckdb
|
|
274
|
+
database: /tmp/mydata.duckdb
|
|
275
|
+
username: ""
|
|
276
|
+
password: ""
|
|
277
|
+
direct: { host: localhost, port: 0 }
|
|
278
|
+
healthcheck: { query: "SELECT 1" }
|
|
279
|
+
safety:
|
|
280
|
+
confirm: true
|
|
281
|
+
read_only: true
|
|
@@ -5,6 +5,48 @@ All notable changes to this project will be documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.3] — 2026-08-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Oracle Database support** — `oracle+oracledb` driver (thin mode, pure
|
|
13
|
+
Python — no Oracle Instant Client needed). `oracledb>=2` added as a
|
|
14
|
+
core dependency. The init wizard offers it in the driver choice list;
|
|
15
|
+
`_default_port` returns 1521 for Oracle. Native-lib hint for Oracle
|
|
16
|
+
Instant Client (`libclntsh` / `libociei` / `libocci`) failure path
|
|
17
|
+
included in `dbctl.db._native_lib_hint`. Healthcheck query convention
|
|
18
|
+
is `SELECT 1 FROM DUAL`.
|
|
19
|
+
- **SQLite support** — `sqlite` driver (built into Python stdlib, no
|
|
20
|
+
extra dependency). File-based: `build_engine` skips host/port/
|
|
21
|
+
username/password injection (just `sqlite:///path`). Config validator
|
|
22
|
+
exempts file-based drivers (sqlite + duckdb) from the credential
|
|
23
|
+
requirement — `username` and `password` are not needed.
|
|
24
|
+
- **DuckDB support** — `duckdb` driver. `duckdb>=1` added as a core
|
|
25
|
+
dependency. Same file-based handling as SQLite (`duckdb:///path` or
|
|
26
|
+
`duckdb:///:memory:`).
|
|
27
|
+
- **`replay_spec.on_conflict`** field — the conflict-handling strategy
|
|
28
|
+
for replay mode. Defaults to `skip` (additive — replay new/changed
|
|
29
|
+
rows without breaking existing entries), unlike `copy_spec` which
|
|
30
|
+
defaults to `error`. Use `truncate` for a full refresh.
|
|
31
|
+
- **`logo_small.png`** added to top of every `docs/*.md` file; README
|
|
32
|
+
gets a centred logo_small footer (big logo stays at top).
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **`replay-users` crashed on existing rows** — hardcoded
|
|
37
|
+
`on_conflict=error` meant any PK collision in the target aborted the
|
|
38
|
+
replay. Now uses `replay_spec.on_conflict` (default `skip`), so
|
|
39
|
+
`INSERT IGNORE` / `ON CONFLICT DO NOTHING` handles duplicates cleanly
|
|
40
|
+
and the report shows inserted vs skipped counts correctly.
|
|
41
|
+
- **File-based drivers (sqlite/duckdb) rejected by config validator**
|
|
42
|
+
— required username + password even though the drivers ignore them.
|
|
43
|
+
The validator now exempts `sqlite*` and `duckdb*` from the credential
|
|
44
|
+
requirement.
|
|
45
|
+
- **File-based drivers: `build_engine` injected host:port into the URL**
|
|
46
|
+
— SQLAlchemy rejected `sqlite://:***@localhost:0//path` with an
|
|
47
|
+
ArgumentError. Now skips host/port/username/password for file-based
|
|
48
|
+
drivers; the URL is just `sqlite:///path`.
|
|
49
|
+
|
|
8
50
|
## [0.6.2] — 2026-08-03
|
|
9
51
|
|
|
10
52
|
### Added
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbctl
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: Generic CLI to monitor, control, and administer multiple databases via SSM, SSH, or direct connection.
|
|
5
5
|
Author: dbctl contributors
|
|
6
6
|
License: MIT
|
|
7
|
-
Keywords: cli,database,mssql,mysql,postgres,ssh,ssm,tunnel
|
|
7
|
+
Keywords: cli,database,duckdb,mssql,mysql,oracle,postgres,sqlite,ssh,ssm,tunnel
|
|
8
8
|
Classifier: Development Status :: 3 - Alpha
|
|
9
9
|
Classifier: Environment :: Console
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
@@ -16,6 +16,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Topic :: Database
|
|
17
17
|
Requires-Python: >=3.12
|
|
18
18
|
Requires-Dist: click>=8.1
|
|
19
|
+
Requires-Dist: duckdb>=1
|
|
20
|
+
Requires-Dist: oracledb>=2
|
|
19
21
|
Requires-Dist: psycopg[binary]>=3.1
|
|
20
22
|
Requires-Dist: pydantic>=2
|
|
21
23
|
Requires-Dist: pymysql>=1.1
|
|
@@ -85,8 +87,9 @@ your shell history. (For ad-hoc exploration open the tunnel with
|
|
|
85
87
|
| `direct` | No tunnel — connect to the upstream host:port directly | none |
|
|
86
88
|
|
|
87
89
|
Each connection declares its SQLAlchemy URL scheme (`postgresql+psycopg`,
|
|
88
|
-
`mysql+pymysql`, `mariadb+pymysql`, `mssql+pyodbc`,
|
|
89
|
-
|
|
90
|
+
`mysql+pymysql`, `mariadb+pymysql`, `mssql+pyodbc`, `oracle+oracledb`,
|
|
91
|
+
`sqlite`, `duckdb`, …), a healthcheck query, optional introspection
|
|
92
|
+
(`info`) queries, and a `safety` policy.
|
|
90
93
|
|
|
91
94
|
Operations are declared separately in `operations.yaml`. Each operation is a
|
|
92
95
|
parameterised SQL block (using `$name` placeholders) with declared parameters;
|
|
@@ -53,8 +53,9 @@ your shell history. (For ad-hoc exploration open the tunnel with
|
|
|
53
53
|
| `direct` | No tunnel — connect to the upstream host:port directly | none |
|
|
54
54
|
|
|
55
55
|
Each connection declares its SQLAlchemy URL scheme (`postgresql+psycopg`,
|
|
56
|
-
`mysql+pymysql`, `mariadb+pymysql`, `mssql+pyodbc`,
|
|
57
|
-
|
|
56
|
+
`mysql+pymysql`, `mariadb+pymysql`, `mssql+pyodbc`, `oracle+oracledb`,
|
|
57
|
+
`sqlite`, `duckdb`, …), a healthcheck query, optional introspection
|
|
58
|
+
(`info`) queries, and a `safety` policy.
|
|
58
59
|
|
|
59
60
|
Operations are declared separately in `operations.yaml`. Each operation is a
|
|
60
61
|
parameterised SQL block (using `$name` placeholders) with declared parameters;
|
|
@@ -247,16 +247,22 @@ class Connection(BaseModel):
|
|
|
247
247
|
if not self.database:
|
|
248
248
|
raise ValueError("'database' is required (or use 'url:' for a full connection string)")
|
|
249
249
|
|
|
250
|
+
# File-based drivers (sqlite, duckdb) have no auth — skip the
|
|
251
|
+
# credential requirement entirely. The username/password fields
|
|
252
|
+
# are accepted (and ignored by the driver) for config-schema
|
|
253
|
+
# compatibility, but none is required.
|
|
254
|
+
_file_based = self.driver.startswith(("sqlite", "duckdb"))
|
|
255
|
+
|
|
250
256
|
sources = [bool(self.password), bool(self.password_env), self.prompt]
|
|
251
257
|
if sum(sources) > 1:
|
|
252
258
|
raise ValueError("'password', 'password_env' and 'prompt' are mutually exclusive")
|
|
253
|
-
if not any(sources) and not self.windows_sso:
|
|
259
|
+
if not _file_based and not any(sources) and not self.windows_sso:
|
|
254
260
|
raise ValueError("set 'password', 'password_env', 'prompt: true', or 'windows_sso: true'")
|
|
255
261
|
if self.windows_sso and any(sources):
|
|
256
262
|
raise ValueError("'windows_sso' is mutually exclusive with password/password_env/prompt")
|
|
257
263
|
if self.windows_sso and not self.driver.startswith("mssql"):
|
|
258
264
|
raise ValueError("'windows_sso' is only supported with mssql+pyodbc driver")
|
|
259
|
-
if not self.windows_sso and not self.username:
|
|
265
|
+
if not _file_based and not self.windows_sso and not self.username:
|
|
260
266
|
raise ValueError("'username' is required (or set 'windows_sso: true' for mssql SSO)")
|
|
261
267
|
return self
|
|
262
268
|
|
|
@@ -339,11 +345,17 @@ class ReplaySpec(BaseModel):
|
|
|
339
345
|
``package.module:callable`` / ``package.module.callable`` resolving to a
|
|
340
346
|
``Callable[[dict], dict]``. The callable runs in-process; it must not
|
|
341
347
|
reach across connections.
|
|
348
|
+
|
|
349
|
+
`on_conflict` defaults to ``skip`` (unlike ``copy`` which defaults to
|
|
350
|
+
``error``) — a replay is typically additive (replay new/changed rows
|
|
351
|
+
from a source log into a target without breaking existing entries).
|
|
352
|
+
Use ``truncate`` if you want a full refresh instead.
|
|
342
353
|
"""
|
|
343
354
|
|
|
344
355
|
model_config = ConfigDict(extra="forbid")
|
|
345
356
|
batch_size: int = 10000
|
|
346
357
|
tables: list[str] | None = None # None = introspect src
|
|
358
|
+
on_conflict: OnConflict = OnConflict.skip # default skip (additive)
|
|
347
359
|
where: dict[str, str] = Field(default_factory=dict)
|
|
348
360
|
transform: str = "identity"
|
|
349
361
|
|
|
@@ -80,14 +80,12 @@ def _connect_args(conn: Connection, timeout: float) -> dict:
|
|
|
80
80
|
"""Driver-specific connect-time knobs (mainly connect_timeout)."""
|
|
81
81
|
args: dict = {}
|
|
82
82
|
driver = _driver_name(conn)
|
|
83
|
-
if driver.startswith(("postgresql", "mysql", "mariadb")):
|
|
83
|
+
if driver.startswith(("postgresql", "mysql", "mariadb", "oracle")):
|
|
84
84
|
args["connect_timeout"] = int(max(1, timeout))
|
|
85
|
+
# sqlite + duckdb are file-based — no connect_timeout; SQLAlchemy
|
|
86
|
+
# ignores it anyway, but we skip it so we don't pass an unknown kwarg
|
|
87
|
+
# to the underlying C library.
|
|
85
88
|
if conn.windows_sso:
|
|
86
|
-
# pyodbc: Trusted_Connection=yes tells the ODBC driver to use the
|
|
87
|
-
# current Windows user's credentials (Kerberos / NTLM). The ODBC
|
|
88
|
-
# Driver 17+ also supports Authentication=ActiveDirectoryIntegrated
|
|
89
|
-
# for Azure AD SSO — use that by setting it explicitly via
|
|
90
|
-
# connect_args in your config if needed.
|
|
91
89
|
args["Trusted_Connection"] = "yes"
|
|
92
90
|
return args
|
|
93
91
|
|
|
@@ -99,6 +97,11 @@ def build_engine(conn: Connection, tunnel: Tunnel, *, echo: bool = False) -> Eng
|
|
|
99
97
|
tunnel's local bind is NOT injected — the URL's own host:port wins. This
|
|
100
98
|
is intentional: a user who provides a full URL is taking responsibility
|
|
101
99
|
for the entire connection string.
|
|
100
|
+
|
|
101
|
+
File-based drivers (``sqlite``, ``duckdb``) never get host/port/username/
|
|
102
|
+
password injected — the URL is just ``driver:///path/to/file``. The
|
|
103
|
+
tunnel's local bind is irrelevant for file-based DBs (the file is
|
|
104
|
+
local), and injecting `host:port` makes SQLAlchemy reject the URL.
|
|
102
105
|
"""
|
|
103
106
|
driver = _driver_name(conn)
|
|
104
107
|
_check_driver_available(driver)
|
|
@@ -107,6 +110,11 @@ def build_engine(conn: Connection, tunnel: Tunnel, *, echo: bool = False) -> Eng
|
|
|
107
110
|
|
|
108
111
|
if conn.url:
|
|
109
112
|
url = make_url(conn.url)
|
|
113
|
+
elif driver.startswith(("sqlite", "duckdb")):
|
|
114
|
+
# File-based: URL is just "sqlite:///path" or "duckdb:///path".
|
|
115
|
+
# No host/port/username/password — those are meaningless for a
|
|
116
|
+
# local file. The database field IS the file path.
|
|
117
|
+
url = URL.create(driver, database=conn.database)
|
|
110
118
|
else:
|
|
111
119
|
password = resolve_password(conn)
|
|
112
120
|
url = URL.create(
|
|
@@ -138,6 +146,9 @@ def _check_driver_available(driver: str) -> None:
|
|
|
138
146
|
"mysql+pymysql": "pymysql",
|
|
139
147
|
"mariadb+pymysql": "pymysql",
|
|
140
148
|
"mssql+pyodbc": "pyodbc",
|
|
149
|
+
"oracle+oracledb": "oracledb",
|
|
150
|
+
"sqlite": "sqlite3", # stdlib — always available
|
|
151
|
+
"duckdb": "duckdb",
|
|
141
152
|
}
|
|
142
153
|
pkg = module_map.get(driver)
|
|
143
154
|
if pkg is None:
|
|
@@ -183,6 +194,12 @@ def _native_lib_hint(driver: str, library: str) -> str:
|
|
|
183
194
|
"(Debian/Ubuntu: `sudo apt install libpq5`, "
|
|
184
195
|
"RHEL/Fedora: `sudo dnf install libpq`, macOS: `brew install libpq`)."
|
|
185
196
|
)
|
|
197
|
+
if "libociei" in (library or "") or "libclntsh" in (library or "") or "libocci" in (library or ""):
|
|
198
|
+
return (
|
|
199
|
+
"missing Oracle Instant Client libs (libclntsh / libociei / libocci); "
|
|
200
|
+
"install Oracle Instant Client (download from oracle.com, or use "
|
|
201
|
+
"`pip install oracledb` with the default Thin mode which needs no native libs)."
|
|
202
|
+
)
|
|
186
203
|
return ""
|
|
187
204
|
|
|
188
205
|
|
|
@@ -67,7 +67,15 @@ def run_wizard(*, profile: str | None) -> None:
|
|
|
67
67
|
driver = click.prompt(
|
|
68
68
|
"driver (sqlalchemy url scheme)",
|
|
69
69
|
type=click.Choice(
|
|
70
|
-
[
|
|
70
|
+
[
|
|
71
|
+
"postgresql+psycopg",
|
|
72
|
+
"mysql+pymysql",
|
|
73
|
+
"mariadb+pymysql",
|
|
74
|
+
"mssql+pyodbc",
|
|
75
|
+
"oracle+oracledb",
|
|
76
|
+
"sqlite",
|
|
77
|
+
"duckdb",
|
|
78
|
+
],
|
|
71
79
|
case_sensitive=False,
|
|
72
80
|
),
|
|
73
81
|
default="postgresql+psycopg",
|
|
@@ -214,6 +222,8 @@ def _default_port(driver: str) -> int:
|
|
|
214
222
|
return 3306
|
|
215
223
|
if driver.startswith("mssql"):
|
|
216
224
|
return 1433
|
|
225
|
+
if driver.startswith("oracle"):
|
|
226
|
+
return 1521
|
|
217
227
|
return 5432
|
|
218
228
|
|
|
219
229
|
|
|
@@ -672,14 +672,14 @@ def run_replay(
|
|
|
672
672
|
each row before it lands in the insert batch. ``"identity"`` makes the
|
|
673
673
|
replay equivalent to a plain copy.
|
|
674
674
|
"""
|
|
675
|
-
from dbctl.config import CopySpec
|
|
675
|
+
from dbctl.config import CopySpec
|
|
676
676
|
|
|
677
677
|
# Adapt ReplaySpec → CopySpec so we reuse the copy machinery verbatim.
|
|
678
678
|
copy_spec = CopySpec(
|
|
679
679
|
batch_size=spec.batch_size,
|
|
680
680
|
tables=spec.tables,
|
|
681
681
|
where=spec.where,
|
|
682
|
-
on_conflict=
|
|
682
|
+
on_conflict=spec.on_conflict, # replay defaults to skip (additive)
|
|
683
683
|
)
|
|
684
684
|
transform = _resolve_transform(spec.transform)
|
|
685
685
|
return run_copy(
|
|
@@ -39,7 +39,7 @@ overlap with a clear message.
|
|
|
39
39
|
| `description` | string | no | shown in the dashboard and `dbctl connections list`. |
|
|
40
40
|
| `aliases` | list of strings | no | alternates that resolve back to this connection (e.g. `prod` → `db1`). |
|
|
41
41
|
| `type` | `ssm` \| `ssh` \| `k8s` \| `direct` | **yes** | selects the tunnel implementation. |
|
|
42
|
-
| `driver` | string | **yes** | SQLAlchemy URL scheme. Supported: `postgresql+psycopg`, `mysql+pymysql`, `mariadb+pymysql`, `mssql+pyodbc`. Any other SQLAlchemy scheme works as long as its driver is importable. |
|
|
42
|
+
| `driver` | string | **yes** | SQLAlchemy URL scheme. Supported: `postgresql+psycopg`, `mysql+pymysql`, `mariadb+pymysql`, `mssql+pyodbc`, `oracle+oracledb`, `sqlite`, `duckdb`. Any other SQLAlchemy scheme works as long as its driver is importable. |
|
|
43
43
|
| `database` | string | **yes** | database / catalog name passed to SQLAlchemy. |
|
|
44
44
|
| `username` | string | **yes** (unless `windows_sso`) | DB user. |
|
|
45
45
|
| `password` | string | see rule | plaintext DB password (local dev only — don't commit real secrets to YAML). Mutually exclusive with `password_env`, `prompt`, and `windows_sso`. |
|
|
@@ -320,6 +320,72 @@ connections:
|
|
|
320
320
|
safety: { confirm: false, read_only: false }
|
|
321
321
|
```
|
|
322
322
|
|
|
323
|
+
### Oracle Database (thin mode — no native client needed)
|
|
324
|
+
|
|
325
|
+
```yaml
|
|
326
|
+
connections:
|
|
327
|
+
prod-oracle:
|
|
328
|
+
description: "Production Oracle (oracledb thin mode)"
|
|
329
|
+
type: direct
|
|
330
|
+
driver: oracle+oracledb
|
|
331
|
+
database: ORCLPDB1 # service name (or SID)
|
|
332
|
+
username: app_admin
|
|
333
|
+
password_env: DBCTL_ORACLE_PASSWORD
|
|
334
|
+
direct: { host: db.internal, port: 1521 }
|
|
335
|
+
healthcheck: { query: "SELECT 1 FROM DUAL", timeout_seconds: 10 }
|
|
336
|
+
safety:
|
|
337
|
+
confirm: true
|
|
338
|
+
read_only: false
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
> `oracledb` defaults to **thin mode** (pure Python, no Oracle Instant
|
|
342
|
+
> Client needed). If you need thick mode (native Oracle client libs), set
|
|
343
|
+
> it via the `url:` field with `thick_mode=true` in the query string.
|
|
344
|
+
|
|
345
|
+
### Local SQLite database (file-based)
|
|
346
|
+
|
|
347
|
+
```yaml
|
|
348
|
+
connections:
|
|
349
|
+
local-sqlite:
|
|
350
|
+
description: "Local SQLite database"
|
|
351
|
+
type: direct
|
|
352
|
+
driver: sqlite
|
|
353
|
+
database: /path/to/mydata.db # absolute path to the .db file
|
|
354
|
+
username: "" # sqlite ignores these but config requires one
|
|
355
|
+
password: "" # sqlite ignores
|
|
356
|
+
direct: { host: localhost, port: 0 } # ignored by sqlite; required by config schema
|
|
357
|
+
healthcheck: { query: "SELECT 1" }
|
|
358
|
+
safety:
|
|
359
|
+
confirm: true
|
|
360
|
+
read_only: false
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
> SQLite and DuckDB are file-based — the `host` / `port` fields are
|
|
364
|
+
> ignored by the driver but `type: direct` still requires a `direct:`
|
|
365
|
+
> block. Use `url:` mode if you prefer:
|
|
366
|
+
> ```yaml
|
|
367
|
+
> url: "sqlite:////absolute/path/to/mydata.db"
|
|
368
|
+
> ```
|
|
369
|
+
> (Note the four slashes for absolute paths in SQLAlchemy's sqlite scheme.)
|
|
370
|
+
|
|
371
|
+
### Local DuckDB database (file-based)
|
|
372
|
+
|
|
373
|
+
```yaml
|
|
374
|
+
connections:
|
|
375
|
+
local-duckdb:
|
|
376
|
+
description: "Local DuckDB database"
|
|
377
|
+
type: direct
|
|
378
|
+
driver: duckdb
|
|
379
|
+
database: /path/to/mydata.duckdb # or ":memory:" for in-memory
|
|
380
|
+
username: "" # duckdb ignores
|
|
381
|
+
password: "" # duckdb ignores
|
|
382
|
+
direct: { host: localhost, port: 0 }
|
|
383
|
+
healthcheck: { query: "SELECT 1" }
|
|
384
|
+
safety:
|
|
385
|
+
confirm: true
|
|
386
|
+
read_only: false
|
|
387
|
+
```
|
|
388
|
+
|
|
323
389
|
### CloudNativePG cluster via kubectl port-forward
|
|
324
390
|
|
|
325
391
|
```yaml
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbctl"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.3"
|
|
8
8
|
description = "Generic CLI to monitor, control, and administer multiple databases via SSM, SSH, or direct connection."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
12
|
authors = [{ name = "dbctl contributors" }]
|
|
13
|
-
keywords = ["cli", "database", "postgres", "mysql", "mssql", "ssm", "ssh", "tunnel"]
|
|
13
|
+
keywords = ["cli", "database", "postgres", "mysql", "mssql", "oracle", "sqlite", "duckdb", "ssm", "ssh", "tunnel"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 3 - Alpha",
|
|
16
16
|
"Environment :: Console",
|
|
@@ -31,6 +31,8 @@ dependencies = [
|
|
|
31
31
|
"psycopg[binary]>=3.1",
|
|
32
32
|
"pymysql>=1.1",
|
|
33
33
|
"pyodbc>=5",
|
|
34
|
+
"oracledb>=2",
|
|
35
|
+
"duckdb>=1",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.optional-dependencies]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|