limitsdb 0.5.0__tar.gz → 0.6.0__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.
- {limitsdb-0.5.0 → limitsdb-0.6.0}/PKG-INFO +146 -11
- {limitsdb-0.5.0 → limitsdb-0.6.0}/README.md +145 -10
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/cli/ldb_impl.py +90 -36
- limitsdb-0.6.0/limitsdb/core/ldb_ddl.py +132 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_logger.py +5 -5
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_params_config.py +23 -1
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_runner.py +223 -11
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/db/ldb_engines.py +53 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/db/oracle/ldb_engine_impl.py +402 -44
- {limitsdb-0.5.0 → limitsdb-0.6.0}/pyproject.toml +1 -1
- {limitsdb-0.5.0 → limitsdb-0.6.0}/LICENSE +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/LICENSE-DUAL.md +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/NOTICE +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/__init__.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/cli/__init__.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/cli/ldb_crypt.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/cli/ldb_init.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/cli/ldb_run.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/__init__.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_config_loader.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_crypto.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_errors.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_ilm_config.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_meta.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_status.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/core/ldb_utils.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/db/__init__.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/db/ldb_engine_loader.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/db/oracle/__init__.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/db/oracle/ldb_conf.sql +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/resources/__init__.py +0 -0
- {limitsdb-0.5.0 → limitsdb-0.6.0}/limitsdb/resources/ilm.example.yml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: limitsdb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Configuration-driven ILM (Information Lifecycle Management) for production and history databases
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -207,6 +207,52 @@ source .venv/bin/activate
|
|
|
207
207
|
pip install limitsdb
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
### Air-gapped or internet-restricted servers
|
|
211
|
+
|
|
212
|
+
For production database servers without internet access (common in enterprise Oracle
|
|
213
|
+
environments on Linux, AIX, or Solaris), download the packages on a machine that
|
|
214
|
+
does have access and transfer them to the target server.
|
|
215
|
+
|
|
216
|
+
**Step 1 — Download on a connected machine**
|
|
217
|
+
|
|
218
|
+
If the target server is Linux x86_64:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pip download limitsdb \
|
|
222
|
+
--dest ./limitsdb-bundle \
|
|
223
|
+
--platform linux_x86_64 \
|
|
224
|
+
--python-version 312 \
|
|
225
|
+
--only-binary=:all:
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
For the same platform as the current machine (Windows or macOS build host
|
|
229
|
+
copying to an identical Linux target, omit `--platform` and `--python-version`):
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pip download limitsdb --dest ./limitsdb-bundle
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Step 2 — Transfer to the target server**
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
scp -r ./limitsdb-bundle user@server:/opt/limitsdb-bundle
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Step 3 — Install on the target server (no internet required)**
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
python3 -m venv /opt/limitsdb-venv
|
|
245
|
+
source /opt/limitsdb-venv/bin/activate
|
|
246
|
+
pip install --no-index --find-links /opt/limitsdb-bundle limitsdb
|
|
247
|
+
ldb-run --help
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
> **Note for AIX and Solaris**: `oracledb` may not have a prebuilt wheel for your
|
|
251
|
+
> platform. If `pip download --only-binary=:all:` finds nothing, the package must
|
|
252
|
+
> be compiled from source on the target, which requires Python development headers
|
|
253
|
+
> and a C compiler (`gcc` or `xlc`). Contact your system administrator before
|
|
254
|
+
> proceeding.
|
|
255
|
+
|
|
210
256
|
For development setup (cloning the repo, running tests, CI) see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
211
257
|
|
|
212
258
|
## Quickstart
|
|
@@ -227,7 +273,7 @@ Spin up a minimal end-to-end run with the built-in scaffolding tools:
|
|
|
227
273
|
ldb-crypt --schema billing --profile dev
|
|
228
274
|
```
|
|
229
275
|
|
|
230
|
-
4. **
|
|
276
|
+
4. **Preview the ILM run** (applies the required DDL and grants, moves no data):
|
|
231
277
|
|
|
232
278
|
```bash
|
|
233
279
|
ldb-run --schema billing --profile dev --action SOURCE_ILM --mode PREVIEW
|
|
@@ -356,9 +402,32 @@ What it creates in both source and history environments:
|
|
|
356
402
|
- **Database link** — bidirectional links between source and history so the ILM
|
|
357
403
|
engine can reach both environments from either side.
|
|
358
404
|
|
|
359
|
-
`ldb-impl`
|
|
360
|
-
`secrets[.<PROFILE>].json
|
|
361
|
-
|
|
405
|
+
`ldb-impl` needs the runtime credentials (`source_password`, `history_password`)
|
|
406
|
+
encrypted in `secrets[.<PROFILE>].json`. Run `ldb-crypt` first.
|
|
407
|
+
|
|
408
|
+
### Without admin credentials
|
|
409
|
+
|
|
410
|
+
Admin credentials (`admin_source_*`, `admin_history_*`) are optional per
|
|
411
|
+
database. When they are absent for a database, `ldb-impl` creates nothing in it
|
|
412
|
+
and writes a SQL*Plus script for a DBA instead, one file per database:
|
|
413
|
+
`bootstrap_<schema>_<source|history>_<YYYYMMDD_HHMMSS>.sql` under
|
|
414
|
+
`ddl_pending_dir` (the same parameter as `ldb-run`: `--ddl-pending-dir`,
|
|
415
|
+
`LDB_DDL_PENDING_DIR`, default `ldb_ddl_pending`). A database whose admin credentials are present is still
|
|
416
|
+
provisioned directly. Generating scripts is not an error (exit code 0, with a
|
|
417
|
+
warning naming each file); if a script cannot be written, `ldb-impl` exits 1.
|
|
418
|
+
|
|
419
|
+
The script:
|
|
420
|
+
|
|
421
|
+
- stops at the first error (`whenever sqlerror exit failure`) and ends with a
|
|
422
|
+
`LimitsDb: OK` banner on success;
|
|
423
|
+
- contains no passwords: it prompts for them (hidden input);
|
|
424
|
+
- can be rerun: each object is created in a PL/SQL block that ignores only its
|
|
425
|
+
own "already exists" error, and an existing database link is skipped;
|
|
426
|
+
- creates the database link after reconnecting as the runtime user, who owns it
|
|
427
|
+
(it asks for that user's password).
|
|
428
|
+
|
|
429
|
+
Run it in SQL*Plus connected as a user with administrative privileges:
|
|
430
|
+
`sqlplus admin@<dsn> @bootstrap_<schema>_<env>_<timestamp>.sql`.
|
|
362
431
|
|
|
363
432
|
## Run ILM
|
|
364
433
|
|
|
@@ -372,9 +441,11 @@ ldb-run --schema <SCHEMA> [--profile <PROFILE>] --action SOURCE_ILM|HISTORY_ILM
|
|
|
372
441
|
Common overrides:
|
|
373
442
|
|
|
374
443
|
```
|
|
375
|
-
--mode VALIDATE|PLAN|PREVIEW|SCRIPT|EXECUTE # VALIDATE setup, PLAN DAG, PREVIEW
|
|
444
|
+
--mode VALIDATE|PLAN|PREVIEW|SCRIPT|EXECUTE # VALIDATE setup, PLAN DAG, PREVIEW run without moving data, SCRIPT emit SQL, EXECUTE run
|
|
376
445
|
--chunk-size <int> # Rows per chunk when processing large tables
|
|
377
446
|
--parallel-max <int> # Maximum number of tables processed in parallel
|
|
447
|
+
--ddl-pending-dir <path> # Where failed history DDL is written for a DBA (default: ldb_ddl_pending)
|
|
448
|
+
--history-baseline EMPTY|SOURCE # SCRIPT without a readable history: assume no history objects (default) or the same objects as source
|
|
378
449
|
--log-level DEBUG|INFO|WARNING|ERROR|CRITICAL # Adjust logger verbosity
|
|
379
450
|
--ilm-config-file <path> # Point to a specific ILM YAML file
|
|
380
451
|
--config-dir <root> # Override config root discovery
|
|
@@ -396,24 +467,84 @@ ldb-run --schema billing --profile prod --action SOURCE_ILM --mode EXECUTE --set
|
|
|
396
467
|
|
|
397
468
|
`ldb-run` uses admin credentials (`admin_source_password`,
|
|
398
469
|
`admin_history_password`) for two operations: granting table privileges to the
|
|
399
|
-
application role, and creating or altering history tables before
|
|
400
|
-
run
|
|
470
|
+
application role, and creating or altering history tables before a `PREVIEW` or
|
|
471
|
+
`EXECUTE` run (`PREVIEW` needs the objects in place to validate and time the
|
|
472
|
+
queries; it skips only the data movement). Both operations are idempotent.
|
|
401
473
|
|
|
402
474
|
If admin credentials are absent from `secrets[.<PROFILE>].json`, the privilege
|
|
403
475
|
grants are skipped silently — the assumption is that grants are already in
|
|
404
|
-
place.
|
|
405
|
-
|
|
476
|
+
place.
|
|
477
|
+
|
|
478
|
+
History table management follows the same principle:
|
|
479
|
+
|
|
480
|
+
- **Admin credentials present, tables already up to date**: the admin connection
|
|
481
|
+
is not opened. No DDL is executed.
|
|
482
|
+
- **Admin credentials present, DDL changes are required**: the admin connection
|
|
483
|
+
is opened and the changes are applied as before.
|
|
484
|
+
- **Admin credentials absent, tables already up to date**: `ldb-run` proceeds
|
|
485
|
+
without an admin connection. This is the normal case for environments where a
|
|
486
|
+
DBA pre-creates all history objects and admin credentials are not stored.
|
|
487
|
+
- **Admin credentials absent, history schema cannot be inspected** (for example
|
|
488
|
+
the runtime connection or dictionary query fails): `ldb-run` exits with code 1
|
|
489
|
+
and logs the cause. It does not run ILM against a history schema whose state is
|
|
490
|
+
unknown.
|
|
491
|
+
- **Admin credentials absent, DDL changes are required**: `ldb-run` applies the
|
|
492
|
+
DDL directly over the runtime history connection, with no prior privilege
|
|
493
|
+
check. It succeeds when the runtime user owns the objects or holds the needed
|
|
494
|
+
privileges (for example `ALTER`). Any DDL statement that fails, for whatever
|
|
495
|
+
reason, is recorded and the remaining statements are still attempted. If at least one
|
|
496
|
+
statement failed, `ldb-run` writes the failed statements, each preceded by its
|
|
497
|
+
error, to `history_ddl_<schema>_<YYYYMMDD_HHMMSS>.sql` under `ddl_pending_dir`
|
|
498
|
+
(default `ldb_ddl_pending`), logs an error naming the file, and exits with
|
|
499
|
+
code 1. The file is plain SQL for SQL*Plus (no PL/SQL): it stops at the first
|
|
500
|
+
failing statement with a non-zero exit code, and only a run that reaches the
|
|
501
|
+
final `LimitsDb: OK` banner applied everything. The recorded errors are
|
|
502
|
+
comments. A DBA reviews and runs the file, then `ldb-run` is rerun. Nothing is
|
|
503
|
+
written to stdout, and the directory is created only when a file is written.
|
|
504
|
+
If the file cannot be written (for example the directory is not writable),
|
|
505
|
+
`ldb-run` logs the error with the failed statements and exits with code 1.
|
|
506
|
+
|
|
507
|
+
### History DDL in `--mode SCRIPT`
|
|
508
|
+
|
|
509
|
+
`--mode SCRIPT` applies nothing to the databases. The ILM SQL blocks, which
|
|
510
|
+
run many times, go to stdout. The one-time statements go to files under
|
|
511
|
+
`ddl_pending_dir`, so they stay out of the script that is rerun:
|
|
512
|
+
|
|
513
|
+
- `history_ddl_<schema>_<YYYYMMDD_HHMMSS>.sql`: the history table DDL followed by
|
|
514
|
+
the grants on the history tables for the history role. Run it against the
|
|
515
|
+
history database.
|
|
516
|
+
- `source_grants_<schema>_<YYYYMMDD_HHMMSS>.sql` (`SOURCE_ILM` only): the grants
|
|
517
|
+
on the source tables for the source role. Run it against the source database.
|
|
518
|
+
|
|
519
|
+
Both are plain SQL for SQL*Plus that stop at the first failing statement; the
|
|
520
|
+
grants are unconditional and safe to run again. The history DDL is always
|
|
521
|
+
emitted, computed like this:
|
|
522
|
+
|
|
523
|
+
- **A history connection works**: the admin connection is used when admin
|
|
524
|
+
credentials exist, the runtime one otherwise. The current history structure is
|
|
525
|
+
read, and the file holds only the missing changes.
|
|
526
|
+
- **Neither works or there are no credentials**: `history_baseline` states what to
|
|
527
|
+
assume. `EMPTY` (default) assumes history has no objects, so every table, key
|
|
528
|
+
and index is created. `SOURCE` assumes history holds the same objects as
|
|
529
|
+
source, so only the history-specific differences are emitted (for example the
|
|
530
|
+
`LDB_PROCESS_DATE` column and the matching primary key).
|
|
531
|
+
|
|
532
|
+
Without admin credentials, `ldb-run` applies the history grants right after the
|
|
533
|
+
DDL over the runtime connection. Any grant the runtime user cannot apply goes to
|
|
534
|
+
the same pending file as the failed DDL, so a DBA can run it.
|
|
406
535
|
|
|
407
536
|
### Configuration parameters reference
|
|
408
537
|
|
|
409
538
|
| Parameter | Purpose | Default / values |
|
|
410
539
|
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
411
540
|
| `action` | ILM target (`SOURCE_ILM` moves/purges source, `HISTORY_ILM` cleans downstream history). | Default `SOURCE_ILM`; choices `SOURCE_ILM`, `HISTORY_ILM`. |
|
|
412
|
-
| `mode` | Execution mode: `VALIDATE` config/credentials, `PLAN` dependency order, `PREVIEW`
|
|
541
|
+
| `mode` | Execution mode: `VALIDATE` config/credentials, `PLAN` dependency order, `PREVIEW` run the real queries and apply DDL and grants but do not insert or delete data (a.k.a. legacy `DRY_RUN`), `SCRIPT` emit the history DDL and the ILM SQL without applying anything, `EXECUTE` run changes. | Default `PREVIEW`; choices `VALIDATE`, `PLAN`, `PREVIEW`, `SCRIPT`, `EXECUTE`. |
|
|
413
542
|
| `chunk_size` | Rows per chunk when processing large tables. | Default `100000`. |
|
|
414
543
|
| `use_added_columns` | Populate derived columns in history tables. | Default `True` (boolean toggle). |
|
|
415
544
|
| `add_ldb_columns` | Add LimitsDb execution-date columns in history tables. | Default `True` (boolean toggle). |
|
|
416
545
|
| `parallel_max` | Maximum number of parallel processes. | Default `10`. |
|
|
546
|
+
| `ddl_pending_dir` | Directory where `ldb-run` writes history DDL it could not apply without admin credentials, and the DDL and grants files of `--mode SCRIPT`. | Default `ldb_ddl_pending`. |
|
|
547
|
+
| `history_baseline` | State assumed for the history tables by `--mode SCRIPT` when it cannot read them over the runtime history connection: `EMPTY` (no objects) or `SOURCE` (same objects as source). | Default `EMPTY`; choices `EMPTY`, `SOURCE`. |
|
|
417
548
|
| `db_engine` | Database engine. | Default and only current choice: `"oracle"`. |
|
|
418
549
|
| `log_level` | Logging level. | Default `"INFO"`; choices `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"`. |
|
|
419
550
|
| `schema` | Configuration set name: identifies a system or application (e.g., `billing`). Not a database schema — it is the folder that groups all configuration files for one target. | Required on CLI; no persisted default. |
|
|
@@ -440,6 +571,8 @@ credentials in the current version.
|
|
|
440
571
|
| `use_added_columns` | — | — | `config[.<PROFILE>].yml: use_added_columns` |
|
|
441
572
|
| `add_ldb_columns` | — | — | `config[.<PROFILE>].yml: add_ldb_columns` |
|
|
442
573
|
| `parallel_max` | `--parallel-max` | `LDB_PARALLEL_MAX` | `config[.<PROFILE>].yml: parallel_max` |
|
|
574
|
+
| `ddl_pending_dir` | `--ddl-pending-dir` | `LDB_DDL_PENDING_DIR` | `config[.<PROFILE>].yml: ddl_pending_dir` |
|
|
575
|
+
| `history_baseline` | `--history-baseline` | `LDB_HISTORY_BASELINE` | `config[.<PROFILE>].yml: history_baseline` |
|
|
443
576
|
| `db_engine` | `--db-engine` | `LDB_DB_ENGINE` | `config[.<PROFILE>].yml: db_engine` |
|
|
444
577
|
| `log_level` | `--log-level` | `LDB_LOG_LEVEL` | `config[.<PROFILE>].yml: log_level` |
|
|
445
578
|
| `schema` | `--schema` | `LDB_SCHEMA` | CLI only (not stored). |
|
|
@@ -497,6 +630,8 @@ Every key is commented; the default value and short help appear inline. Example:
|
|
|
497
630
|
# use_added_columns: True # Populate derived columns in history tables
|
|
498
631
|
# add_ldb_columns: True # Add LimitsDb execution-date columns in history tables
|
|
499
632
|
# parallel_max: 10 # Maximum number of parallel processes
|
|
633
|
+
# ddl_pending_dir: "ldb_ddl_pending" # Where failed history DDL is written for a DBA
|
|
634
|
+
# history_baseline: "EMPTY" # SCRIPT without readable history: EMPTY | SOURCE
|
|
500
635
|
# db_engine: "oracle" # Database engine
|
|
501
636
|
# log_level: "INFO" # Logging level
|
|
502
637
|
```
|
|
@@ -61,6 +61,52 @@ source .venv/bin/activate
|
|
|
61
61
|
pip install limitsdb
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Air-gapped or internet-restricted servers
|
|
65
|
+
|
|
66
|
+
For production database servers without internet access (common in enterprise Oracle
|
|
67
|
+
environments on Linux, AIX, or Solaris), download the packages on a machine that
|
|
68
|
+
does have access and transfer them to the target server.
|
|
69
|
+
|
|
70
|
+
**Step 1 — Download on a connected machine**
|
|
71
|
+
|
|
72
|
+
If the target server is Linux x86_64:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip download limitsdb \
|
|
76
|
+
--dest ./limitsdb-bundle \
|
|
77
|
+
--platform linux_x86_64 \
|
|
78
|
+
--python-version 312 \
|
|
79
|
+
--only-binary=:all:
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For the same platform as the current machine (Windows or macOS build host
|
|
83
|
+
copying to an identical Linux target, omit `--platform` and `--python-version`):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip download limitsdb --dest ./limitsdb-bundle
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Step 2 — Transfer to the target server**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
scp -r ./limitsdb-bundle user@server:/opt/limitsdb-bundle
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Step 3 — Install on the target server (no internet required)**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python3 -m venv /opt/limitsdb-venv
|
|
99
|
+
source /opt/limitsdb-venv/bin/activate
|
|
100
|
+
pip install --no-index --find-links /opt/limitsdb-bundle limitsdb
|
|
101
|
+
ldb-run --help
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **Note for AIX and Solaris**: `oracledb` may not have a prebuilt wheel for your
|
|
105
|
+
> platform. If `pip download --only-binary=:all:` finds nothing, the package must
|
|
106
|
+
> be compiled from source on the target, which requires Python development headers
|
|
107
|
+
> and a C compiler (`gcc` or `xlc`). Contact your system administrator before
|
|
108
|
+
> proceeding.
|
|
109
|
+
|
|
64
110
|
For development setup (cloning the repo, running tests, CI) see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
65
111
|
|
|
66
112
|
## Quickstart
|
|
@@ -81,7 +127,7 @@ Spin up a minimal end-to-end run with the built-in scaffolding tools:
|
|
|
81
127
|
ldb-crypt --schema billing --profile dev
|
|
82
128
|
```
|
|
83
129
|
|
|
84
|
-
4. **
|
|
130
|
+
4. **Preview the ILM run** (applies the required DDL and grants, moves no data):
|
|
85
131
|
|
|
86
132
|
```bash
|
|
87
133
|
ldb-run --schema billing --profile dev --action SOURCE_ILM --mode PREVIEW
|
|
@@ -210,9 +256,32 @@ What it creates in both source and history environments:
|
|
|
210
256
|
- **Database link** — bidirectional links between source and history so the ILM
|
|
211
257
|
engine can reach both environments from either side.
|
|
212
258
|
|
|
213
|
-
`ldb-impl`
|
|
214
|
-
`secrets[.<PROFILE>].json
|
|
215
|
-
|
|
259
|
+
`ldb-impl` needs the runtime credentials (`source_password`, `history_password`)
|
|
260
|
+
encrypted in `secrets[.<PROFILE>].json`. Run `ldb-crypt` first.
|
|
261
|
+
|
|
262
|
+
### Without admin credentials
|
|
263
|
+
|
|
264
|
+
Admin credentials (`admin_source_*`, `admin_history_*`) are optional per
|
|
265
|
+
database. When they are absent for a database, `ldb-impl` creates nothing in it
|
|
266
|
+
and writes a SQL*Plus script for a DBA instead, one file per database:
|
|
267
|
+
`bootstrap_<schema>_<source|history>_<YYYYMMDD_HHMMSS>.sql` under
|
|
268
|
+
`ddl_pending_dir` (the same parameter as `ldb-run`: `--ddl-pending-dir`,
|
|
269
|
+
`LDB_DDL_PENDING_DIR`, default `ldb_ddl_pending`). A database whose admin credentials are present is still
|
|
270
|
+
provisioned directly. Generating scripts is not an error (exit code 0, with a
|
|
271
|
+
warning naming each file); if a script cannot be written, `ldb-impl` exits 1.
|
|
272
|
+
|
|
273
|
+
The script:
|
|
274
|
+
|
|
275
|
+
- stops at the first error (`whenever sqlerror exit failure`) and ends with a
|
|
276
|
+
`LimitsDb: OK` banner on success;
|
|
277
|
+
- contains no passwords: it prompts for them (hidden input);
|
|
278
|
+
- can be rerun: each object is created in a PL/SQL block that ignores only its
|
|
279
|
+
own "already exists" error, and an existing database link is skipped;
|
|
280
|
+
- creates the database link after reconnecting as the runtime user, who owns it
|
|
281
|
+
(it asks for that user's password).
|
|
282
|
+
|
|
283
|
+
Run it in SQL*Plus connected as a user with administrative privileges:
|
|
284
|
+
`sqlplus admin@<dsn> @bootstrap_<schema>_<env>_<timestamp>.sql`.
|
|
216
285
|
|
|
217
286
|
## Run ILM
|
|
218
287
|
|
|
@@ -226,9 +295,11 @@ ldb-run --schema <SCHEMA> [--profile <PROFILE>] --action SOURCE_ILM|HISTORY_ILM
|
|
|
226
295
|
Common overrides:
|
|
227
296
|
|
|
228
297
|
```
|
|
229
|
-
--mode VALIDATE|PLAN|PREVIEW|SCRIPT|EXECUTE # VALIDATE setup, PLAN DAG, PREVIEW
|
|
298
|
+
--mode VALIDATE|PLAN|PREVIEW|SCRIPT|EXECUTE # VALIDATE setup, PLAN DAG, PREVIEW run without moving data, SCRIPT emit SQL, EXECUTE run
|
|
230
299
|
--chunk-size <int> # Rows per chunk when processing large tables
|
|
231
300
|
--parallel-max <int> # Maximum number of tables processed in parallel
|
|
301
|
+
--ddl-pending-dir <path> # Where failed history DDL is written for a DBA (default: ldb_ddl_pending)
|
|
302
|
+
--history-baseline EMPTY|SOURCE # SCRIPT without a readable history: assume no history objects (default) or the same objects as source
|
|
232
303
|
--log-level DEBUG|INFO|WARNING|ERROR|CRITICAL # Adjust logger verbosity
|
|
233
304
|
--ilm-config-file <path> # Point to a specific ILM YAML file
|
|
234
305
|
--config-dir <root> # Override config root discovery
|
|
@@ -250,24 +321,84 @@ ldb-run --schema billing --profile prod --action SOURCE_ILM --mode EXECUTE --set
|
|
|
250
321
|
|
|
251
322
|
`ldb-run` uses admin credentials (`admin_source_password`,
|
|
252
323
|
`admin_history_password`) for two operations: granting table privileges to the
|
|
253
|
-
application role, and creating or altering history tables before
|
|
254
|
-
run
|
|
324
|
+
application role, and creating or altering history tables before a `PREVIEW` or
|
|
325
|
+
`EXECUTE` run (`PREVIEW` needs the objects in place to validate and time the
|
|
326
|
+
queries; it skips only the data movement). Both operations are idempotent.
|
|
255
327
|
|
|
256
328
|
If admin credentials are absent from `secrets[.<PROFILE>].json`, the privilege
|
|
257
329
|
grants are skipped silently — the assumption is that grants are already in
|
|
258
|
-
place.
|
|
259
|
-
|
|
330
|
+
place.
|
|
331
|
+
|
|
332
|
+
History table management follows the same principle:
|
|
333
|
+
|
|
334
|
+
- **Admin credentials present, tables already up to date**: the admin connection
|
|
335
|
+
is not opened. No DDL is executed.
|
|
336
|
+
- **Admin credentials present, DDL changes are required**: the admin connection
|
|
337
|
+
is opened and the changes are applied as before.
|
|
338
|
+
- **Admin credentials absent, tables already up to date**: `ldb-run` proceeds
|
|
339
|
+
without an admin connection. This is the normal case for environments where a
|
|
340
|
+
DBA pre-creates all history objects and admin credentials are not stored.
|
|
341
|
+
- **Admin credentials absent, history schema cannot be inspected** (for example
|
|
342
|
+
the runtime connection or dictionary query fails): `ldb-run` exits with code 1
|
|
343
|
+
and logs the cause. It does not run ILM against a history schema whose state is
|
|
344
|
+
unknown.
|
|
345
|
+
- **Admin credentials absent, DDL changes are required**: `ldb-run` applies the
|
|
346
|
+
DDL directly over the runtime history connection, with no prior privilege
|
|
347
|
+
check. It succeeds when the runtime user owns the objects or holds the needed
|
|
348
|
+
privileges (for example `ALTER`). Any DDL statement that fails, for whatever
|
|
349
|
+
reason, is recorded and the remaining statements are still attempted. If at least one
|
|
350
|
+
statement failed, `ldb-run` writes the failed statements, each preceded by its
|
|
351
|
+
error, to `history_ddl_<schema>_<YYYYMMDD_HHMMSS>.sql` under `ddl_pending_dir`
|
|
352
|
+
(default `ldb_ddl_pending`), logs an error naming the file, and exits with
|
|
353
|
+
code 1. The file is plain SQL for SQL*Plus (no PL/SQL): it stops at the first
|
|
354
|
+
failing statement with a non-zero exit code, and only a run that reaches the
|
|
355
|
+
final `LimitsDb: OK` banner applied everything. The recorded errors are
|
|
356
|
+
comments. A DBA reviews and runs the file, then `ldb-run` is rerun. Nothing is
|
|
357
|
+
written to stdout, and the directory is created only when a file is written.
|
|
358
|
+
If the file cannot be written (for example the directory is not writable),
|
|
359
|
+
`ldb-run` logs the error with the failed statements and exits with code 1.
|
|
360
|
+
|
|
361
|
+
### History DDL in `--mode SCRIPT`
|
|
362
|
+
|
|
363
|
+
`--mode SCRIPT` applies nothing to the databases. The ILM SQL blocks, which
|
|
364
|
+
run many times, go to stdout. The one-time statements go to files under
|
|
365
|
+
`ddl_pending_dir`, so they stay out of the script that is rerun:
|
|
366
|
+
|
|
367
|
+
- `history_ddl_<schema>_<YYYYMMDD_HHMMSS>.sql`: the history table DDL followed by
|
|
368
|
+
the grants on the history tables for the history role. Run it against the
|
|
369
|
+
history database.
|
|
370
|
+
- `source_grants_<schema>_<YYYYMMDD_HHMMSS>.sql` (`SOURCE_ILM` only): the grants
|
|
371
|
+
on the source tables for the source role. Run it against the source database.
|
|
372
|
+
|
|
373
|
+
Both are plain SQL for SQL*Plus that stop at the first failing statement; the
|
|
374
|
+
grants are unconditional and safe to run again. The history DDL is always
|
|
375
|
+
emitted, computed like this:
|
|
376
|
+
|
|
377
|
+
- **A history connection works**: the admin connection is used when admin
|
|
378
|
+
credentials exist, the runtime one otherwise. The current history structure is
|
|
379
|
+
read, and the file holds only the missing changes.
|
|
380
|
+
- **Neither works or there are no credentials**: `history_baseline` states what to
|
|
381
|
+
assume. `EMPTY` (default) assumes history has no objects, so every table, key
|
|
382
|
+
and index is created. `SOURCE` assumes history holds the same objects as
|
|
383
|
+
source, so only the history-specific differences are emitted (for example the
|
|
384
|
+
`LDB_PROCESS_DATE` column and the matching primary key).
|
|
385
|
+
|
|
386
|
+
Without admin credentials, `ldb-run` applies the history grants right after the
|
|
387
|
+
DDL over the runtime connection. Any grant the runtime user cannot apply goes to
|
|
388
|
+
the same pending file as the failed DDL, so a DBA can run it.
|
|
260
389
|
|
|
261
390
|
### Configuration parameters reference
|
|
262
391
|
|
|
263
392
|
| Parameter | Purpose | Default / values |
|
|
264
393
|
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
265
394
|
| `action` | ILM target (`SOURCE_ILM` moves/purges source, `HISTORY_ILM` cleans downstream history). | Default `SOURCE_ILM`; choices `SOURCE_ILM`, `HISTORY_ILM`. |
|
|
266
|
-
| `mode` | Execution mode: `VALIDATE` config/credentials, `PLAN` dependency order, `PREVIEW`
|
|
395
|
+
| `mode` | Execution mode: `VALIDATE` config/credentials, `PLAN` dependency order, `PREVIEW` run the real queries and apply DDL and grants but do not insert or delete data (a.k.a. legacy `DRY_RUN`), `SCRIPT` emit the history DDL and the ILM SQL without applying anything, `EXECUTE` run changes. | Default `PREVIEW`; choices `VALIDATE`, `PLAN`, `PREVIEW`, `SCRIPT`, `EXECUTE`. |
|
|
267
396
|
| `chunk_size` | Rows per chunk when processing large tables. | Default `100000`. |
|
|
268
397
|
| `use_added_columns` | Populate derived columns in history tables. | Default `True` (boolean toggle). |
|
|
269
398
|
| `add_ldb_columns` | Add LimitsDb execution-date columns in history tables. | Default `True` (boolean toggle). |
|
|
270
399
|
| `parallel_max` | Maximum number of parallel processes. | Default `10`. |
|
|
400
|
+
| `ddl_pending_dir` | Directory where `ldb-run` writes history DDL it could not apply without admin credentials, and the DDL and grants files of `--mode SCRIPT`. | Default `ldb_ddl_pending`. |
|
|
401
|
+
| `history_baseline` | State assumed for the history tables by `--mode SCRIPT` when it cannot read them over the runtime history connection: `EMPTY` (no objects) or `SOURCE` (same objects as source). | Default `EMPTY`; choices `EMPTY`, `SOURCE`. |
|
|
271
402
|
| `db_engine` | Database engine. | Default and only current choice: `"oracle"`. |
|
|
272
403
|
| `log_level` | Logging level. | Default `"INFO"`; choices `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"`. |
|
|
273
404
|
| `schema` | Configuration set name: identifies a system or application (e.g., `billing`). Not a database schema — it is the folder that groups all configuration files for one target. | Required on CLI; no persisted default. |
|
|
@@ -294,6 +425,8 @@ credentials in the current version.
|
|
|
294
425
|
| `use_added_columns` | — | — | `config[.<PROFILE>].yml: use_added_columns` |
|
|
295
426
|
| `add_ldb_columns` | — | — | `config[.<PROFILE>].yml: add_ldb_columns` |
|
|
296
427
|
| `parallel_max` | `--parallel-max` | `LDB_PARALLEL_MAX` | `config[.<PROFILE>].yml: parallel_max` |
|
|
428
|
+
| `ddl_pending_dir` | `--ddl-pending-dir` | `LDB_DDL_PENDING_DIR` | `config[.<PROFILE>].yml: ddl_pending_dir` |
|
|
429
|
+
| `history_baseline` | `--history-baseline` | `LDB_HISTORY_BASELINE` | `config[.<PROFILE>].yml: history_baseline` |
|
|
297
430
|
| `db_engine` | `--db-engine` | `LDB_DB_ENGINE` | `config[.<PROFILE>].yml: db_engine` |
|
|
298
431
|
| `log_level` | `--log-level` | `LDB_LOG_LEVEL` | `config[.<PROFILE>].yml: log_level` |
|
|
299
432
|
| `schema` | `--schema` | `LDB_SCHEMA` | CLI only (not stored). |
|
|
@@ -351,6 +484,8 @@ Every key is commented; the default value and short help appear inline. Example:
|
|
|
351
484
|
# use_added_columns: True # Populate derived columns in history tables
|
|
352
485
|
# add_ldb_columns: True # Add LimitsDb execution-date columns in history tables
|
|
353
486
|
# parallel_max: 10 # Maximum number of parallel processes
|
|
487
|
+
# ddl_pending_dir: "ldb_ddl_pending" # Where failed history DDL is written for a DBA
|
|
488
|
+
# history_baseline: "EMPTY" # SCRIPT without readable history: EMPTY | SOURCE
|
|
354
489
|
# db_engine: "oracle" # Database engine
|
|
355
490
|
# log_level: "INFO" # Logging level
|
|
356
491
|
```
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
6
9
|
|
|
7
10
|
from limitsdb.core.ldb_crypto import load_or_create_key
|
|
8
11
|
from limitsdb.core.ldb_errors import LimitsDbError
|
|
@@ -33,6 +36,9 @@ def _parse_args() -> argparse.Namespace:
|
|
|
33
36
|
parser.add_argument(
|
|
34
37
|
"--set", action="append", default=[], help="Overrides like key=value; supports dotted keys for nesting"
|
|
35
38
|
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--ddl-pending-dir", help="Directory for the bootstrap scripts written when admin credentials are absent"
|
|
41
|
+
)
|
|
36
42
|
parser.add_argument(
|
|
37
43
|
"--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Logging level"
|
|
38
44
|
)
|
|
@@ -101,10 +107,6 @@ def run_cli() -> None:
|
|
|
101
107
|
connections = config.connections
|
|
102
108
|
administration = config.administration
|
|
103
109
|
|
|
104
|
-
if not administration.source.username or not administration.source.password:
|
|
105
|
-
raise ValueError("admin_source_username and admin_source_password are required")
|
|
106
|
-
if not administration.history.username or not administration.history.password:
|
|
107
|
-
raise ValueError("admin_history_username and admin_history_password are required")
|
|
108
110
|
if not connections.source.username or not connections.source.password:
|
|
109
111
|
raise ValueError("source_username and source_password are required")
|
|
110
112
|
if not connections.history.username or not connections.history.password:
|
|
@@ -158,48 +160,100 @@ def run_cli() -> None:
|
|
|
158
160
|
)
|
|
159
161
|
|
|
160
162
|
summary: dict[str, list[str]] = {}
|
|
163
|
+
script_files: list[Path] = []
|
|
164
|
+
source_has_admin = bool(administration.source.username and administration.source.password)
|
|
165
|
+
history_has_admin = bool(administration.history.username and administration.history.password)
|
|
161
166
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
def write_script(env: str, **definitions: Any) -> None:
|
|
168
|
+
content = engine.generate_bootstrap_script(title=f"{env} database of schema {config.schema}", **definitions)
|
|
169
|
+
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
170
|
+
path = Path(config.execution.ddl_pending_dir) / f"bootstrap_{config.schema}_{env.lower()}_{stamp}.sql"
|
|
171
|
+
try:
|
|
172
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
path.write_text(content, encoding="utf-8")
|
|
174
|
+
except OSError as exc:
|
|
175
|
+
logger.error(
|
|
176
|
+
"Admin credentials for %s are absent and the script could not be written to %s: %s", env, path, exc
|
|
177
|
+
)
|
|
178
|
+
raise SystemExit(1) from exc
|
|
179
|
+
script_files.append(path)
|
|
173
180
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
if not source_has_admin:
|
|
182
|
+
write_script(
|
|
183
|
+
"SOURCE",
|
|
184
|
+
roles=source_roles,
|
|
185
|
+
users=[source_user],
|
|
186
|
+
tables=source_tables,
|
|
187
|
+
sequences=source_sequences,
|
|
188
|
+
supporting_owner=connections.source.username,
|
|
189
|
+
links=[source_db_link],
|
|
190
|
+
runtime_username=connections.source.username,
|
|
191
|
+
runtime_dsn=connections.source.dsn,
|
|
192
|
+
)
|
|
193
|
+
if not history_has_admin:
|
|
194
|
+
write_script(
|
|
195
|
+
"HISTORY",
|
|
196
|
+
roles=history_roles,
|
|
197
|
+
users=[history_user],
|
|
198
|
+
tables=history_tables,
|
|
199
|
+
sequences=history_sequences,
|
|
200
|
+
supporting_owner=connections.history.username,
|
|
201
|
+
links=[history_db_link],
|
|
202
|
+
runtime_username=connections.history.username,
|
|
203
|
+
runtime_dsn=connections.history.dsn,
|
|
182
204
|
)
|
|
183
|
-
finally:
|
|
184
|
-
engine.close_connection(history_admin_conn)
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
206
|
+
if source_has_admin:
|
|
207
|
+
source_admin_conn = engine.get_connection(config, admin=True, env="SOURCE")
|
|
208
|
+
try:
|
|
209
|
+
summary["source_roles"] = engine.ensure_roles(source_admin_conn, source_roles)
|
|
210
|
+
summary["source_users"] = engine.ensure_users(source_admin_conn, [source_user])
|
|
211
|
+
summary["source_tables"] = engine.ensure_tables(source_admin_conn, source_tables)
|
|
212
|
+
summary["source_sequences"] = engine.ensure_sequences(source_admin_conn, source_sequences)
|
|
213
|
+
summary["source_supporting_objects"] = engine.ensure_supporting_objects(
|
|
214
|
+
source_admin_conn, connections.source.username
|
|
215
|
+
)
|
|
216
|
+
finally:
|
|
217
|
+
engine.close_connection(source_admin_conn)
|
|
191
218
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
219
|
+
if history_has_admin:
|
|
220
|
+
history_admin_conn = engine.get_connection(config, admin=True, env="HISTORY")
|
|
221
|
+
try:
|
|
222
|
+
summary["history_roles"] = engine.ensure_roles(history_admin_conn, history_roles)
|
|
223
|
+
summary["history_users"] = engine.ensure_users(history_admin_conn, [history_user])
|
|
224
|
+
summary["history_tables"] = engine.ensure_tables(history_admin_conn, history_tables)
|
|
225
|
+
summary["history_sequences"] = engine.ensure_sequences(history_admin_conn, history_sequences)
|
|
226
|
+
summary["history_supporting_objects"] = engine.ensure_supporting_objects(
|
|
227
|
+
history_admin_conn, connections.history.username
|
|
228
|
+
)
|
|
229
|
+
finally:
|
|
230
|
+
engine.close_connection(history_admin_conn)
|
|
231
|
+
|
|
232
|
+
if source_has_admin:
|
|
233
|
+
source_user_conn = engine.get_connection(config, admin=False, env="SOURCE")
|
|
234
|
+
try:
|
|
235
|
+
summary["source_database_links"] = engine.ensure_database_links(source_user_conn, [source_db_link])
|
|
236
|
+
finally:
|
|
237
|
+
engine.close_connection(source_user_conn)
|
|
238
|
+
|
|
239
|
+
if history_has_admin:
|
|
240
|
+
history_user_conn = engine.get_connection(config, admin=False, env="HISTORY")
|
|
241
|
+
try:
|
|
242
|
+
summary["history_database_links"] = engine.ensure_database_links(history_user_conn, [history_db_link])
|
|
243
|
+
finally:
|
|
244
|
+
engine.close_connection(history_user_conn)
|
|
197
245
|
|
|
198
246
|
anything_created = any(summary.values())
|
|
199
247
|
for key, values in summary.items():
|
|
200
248
|
if values:
|
|
201
249
|
logger.info("Created %s: %s", key.replace("_", " "), ", ".join(values))
|
|
202
|
-
|
|
250
|
+
for script_file in script_files:
|
|
251
|
+
logger.warning(
|
|
252
|
+
"Admin credentials are absent for this database, so nothing was created there. "
|
|
253
|
+
"Ask a DBA to run %s in SQL*Plus.",
|
|
254
|
+
script_file,
|
|
255
|
+
)
|
|
256
|
+
if not anything_created and not script_files:
|
|
203
257
|
logger.info("All LimitsDb database objects are already present.")
|
|
204
258
|
|
|
205
259
|
|