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.
Files changed (47) hide show
  1. {dbctl-0.6.2 → dbctl-0.6.3}/.dbctl/connections.yaml +53 -0
  2. {dbctl-0.6.2 → dbctl-0.6.3}/CHANGELOG.md +42 -0
  3. {dbctl-0.6.2 → dbctl-0.6.3}/PKG-INFO +7 -4
  4. {dbctl-0.6.2 → dbctl-0.6.3}/README.md +3 -2
  5. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/config.py +14 -2
  6. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/db.py +23 -6
  7. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/init.py +11 -1
  8. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/multi.py +2 -2
  9. {dbctl-0.6.2 → dbctl-0.6.3}/docs/connections.md +67 -1
  10. {dbctl-0.6.2 → dbctl-0.6.3}/pyproject.toml +4 -2
  11. {dbctl-0.6.2 → dbctl-0.6.3}/.dbctl/operations.yaml +0 -0
  12. {dbctl-0.6.2 → dbctl-0.6.3}/.github/workflows/ci.yml +0 -0
  13. {dbctl-0.6.2 → dbctl-0.6.3}/.github-local/ci.yml +0 -0
  14. {dbctl-0.6.2 → dbctl-0.6.3}/.gitignore +0 -0
  15. {dbctl-0.6.2 → dbctl-0.6.3}/Makefile +0 -0
  16. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/__init__.py +0 -0
  17. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/__main__.py +0 -0
  18. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/audit.py +0 -0
  19. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/cli.py +0 -0
  20. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/connections.py +0 -0
  21. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/execute.py +0 -0
  22. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/operations.py +0 -0
  23. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/reports.py +0 -0
  24. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/runtime.py +0 -0
  25. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/__init__.py +0 -0
  26. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/base.py +0 -0
  27. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/direct.py +0 -0
  28. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/k8s.py +0 -0
  29. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/ssh.py +0 -0
  30. {dbctl-0.6.2 → dbctl-0.6.3}/dbctl/tunnels/ssm.py +0 -0
  31. {dbctl-0.6.2 → dbctl-0.6.3}/docker-compose.yml +0 -0
  32. {dbctl-0.6.2 → dbctl-0.6.3}/docs/ACTION_OUTPUT.md +0 -0
  33. {dbctl-0.6.2 → dbctl-0.6.3}/docs/DESIGN.md +0 -0
  34. {dbctl-0.6.2 → dbctl-0.6.3}/docs/SESSION_STATE.md +0 -0
  35. {dbctl-0.6.2 → dbctl-0.6.3}/docs/logo.png +0 -0
  36. {dbctl-0.6.2 → dbctl-0.6.3}/docs/logo_small.png +0 -0
  37. {dbctl-0.6.2 → dbctl-0.6.3}/docs/operations.md +0 -0
  38. {dbctl-0.6.2 → dbctl-0.6.3}/docs/tutorial.md +0 -0
  39. {dbctl-0.6.2 → dbctl-0.6.3}/seed/mssql.sql +0 -0
  40. {dbctl-0.6.2 → dbctl-0.6.3}/seed/mysql.sql +0 -0
  41. {dbctl-0.6.2 → dbctl-0.6.3}/seed/postgres.sql +0 -0
  42. {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_bastion_tags.py +0 -0
  43. {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_connections_loader.py +0 -0
  44. {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_k8s_tunnel.py +0 -0
  45. {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_regressions.py +0 -0
  46. {dbctl-0.6.2 → dbctl-0.6.3}/tests/test_smoke.py +0 -0
  47. {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.2
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`, …), a healthcheck query,
89
- optional introspection (`info`) queries, and a `safety` policy.
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`, …), a healthcheck query,
57
- optional introspection (`info`) queries, and a `safety` policy.
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
- ["postgresql+psycopg", "mysql+pymysql", "mariadb+pymysql", "mssql+pyodbc"],
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, OnConflict
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=OnConflict.error, # replay is copy-with-transform; default to no skip
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.2"
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