c2r-collect 0.1.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.
- c2r_collect-0.1.0/MANIFEST.in +1 -0
- c2r_collect-0.1.0/PKG-INFO +34 -0
- c2r_collect-0.1.0/README.md +18 -0
- c2r_collect-0.1.0/pyproject.toml +34 -0
- c2r_collect-0.1.0/setup.cfg +4 -0
- c2r_collect-0.1.0/src/c2r_collect/__init__.py +13 -0
- c2r_collect-0.1.0/src/c2r_collect/__main__.py +6 -0
- c2r_collect-0.1.0/src/c2r_collect/cli.py +183 -0
- c2r_collect-0.1.0/src/c2r_collect/codebook.py +107 -0
- c2r_collect-0.1.0/src/c2r_collect/collect.py +307 -0
- c2r_collect-0.1.0/src/c2r_collect/config.py +149 -0
- c2r_collect-0.1.0/src/c2r_collect/counters.py +33 -0
- c2r_collect-0.1.0/src/c2r_collect/db.py +104 -0
- c2r_collect-0.1.0/src/c2r_collect/discover.py +28 -0
- c2r_collect-0.1.0/src/c2r_collect/make_input.py +146 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/dependencies.sql +5 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/dict_columns.sql +12 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/dict_constraints.sql +6 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/dict_manifest.sql +12 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/dict_tables.sql +5 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/env.sql +7 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/invalid_objects.sql +6 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/jobs.sql +5 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/procedures.sql +5 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/sources.sql +5 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/synonyms.sql +3 -0
- c2r_collect-0.1.0/src/c2r_collect/queries/triggers.sql +4 -0
- c2r_collect-0.1.0/src/c2r_collect/report.py +76 -0
- c2r_collect-0.1.0/src/c2r_collect/templates/codebook_README.md +11 -0
- c2r_collect-0.1.0/src/c2r_collect/templates/collect-report.md +30 -0
- c2r_collect-0.1.0/src/c2r_collect/templates/dict_README.md +13 -0
- c2r_collect-0.1.0/src/c2r_collect/transfer.py +122 -0
- c2r_collect-0.1.0/src/c2r_collect/writers.py +130 -0
- c2r_collect-0.1.0/src/c2r_collect.egg-info/PKG-INFO +34 -0
- c2r_collect-0.1.0/src/c2r_collect.egg-info/SOURCES.txt +50 -0
- c2r_collect-0.1.0/src/c2r_collect.egg-info/dependency_links.txt +1 -0
- c2r_collect-0.1.0/src/c2r_collect.egg-info/entry_points.txt +2 -0
- c2r_collect-0.1.0/src/c2r_collect.egg-info/requires.txt +12 -0
- c2r_collect-0.1.0/src/c2r_collect.egg-info/top_level.txt +1 -0
- c2r_collect-0.1.0/tests/conftest.py +79 -0
- c2r_collect-0.1.0/tests/test_cli.py +128 -0
- c2r_collect-0.1.0/tests/test_codebook.py +58 -0
- c2r_collect-0.1.0/tests/test_collect.py +128 -0
- c2r_collect-0.1.0/tests/test_config.py +97 -0
- c2r_collect-0.1.0/tests/test_counters.py +13 -0
- c2r_collect-0.1.0/tests/test_db_querystore.py +39 -0
- c2r_collect-0.1.0/tests/test_discover.py +20 -0
- c2r_collect-0.1.0/tests/test_make_input.py +124 -0
- c2r_collect-0.1.0/tests/test_package.py +5 -0
- c2r_collect-0.1.0/tests/test_report.py +44 -0
- c2r_collect-0.1.0/tests/test_transfer.py +104 -0
- c2r_collect-0.1.0/tests/test_writers.py +108 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include tests/conftest.py
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: c2r-collect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Oracle data dictionary + PL/SQL source collector that writes okf-loom input folders
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: oracledb>=2.0
|
|
9
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
10
|
+
Provides-Extra: s3
|
|
11
|
+
Requires-Dist: boto3>=1.34; extra == "s3"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
+
Requires-Dist: build; extra == "dev"
|
|
15
|
+
Requires-Dist: twine; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# c2r-collect
|
|
18
|
+
|
|
19
|
+
Pulls PL/SQL package sources, relationship CSVs, dictionary and codebook tables from an Oracle
|
|
20
|
+
schema into `raw/<date>/`, then builds an okf-loom input folder from that raw.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
pip install c2r-collect # add [s3] extra for upload/download
|
|
24
|
+
cp config.example.toml config.toml # edit; password via env C2R_DB_PASSWORD
|
|
25
|
+
c2r-collect collect # → <root>/raw/<today>/
|
|
26
|
+
c2r-collect upload --date YYYY-MM-DD
|
|
27
|
+
c2r-collect download --date YYYY-MM-DD
|
|
28
|
+
c2r-collect make-input --date YYYY-MM-DD [--prev YYYY-MM-DD] [--add add.txt]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Design and release notes live in the source repository (`docs/`), not in the package.
|
|
32
|
+
|
|
33
|
+
## First run checks
|
|
34
|
+
See `docs/release.md` (release steps, QA-DB run, VDI production run).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# c2r-collect
|
|
2
|
+
|
|
3
|
+
Pulls PL/SQL package sources, relationship CSVs, dictionary and codebook tables from an Oracle
|
|
4
|
+
schema into `raw/<date>/`, then builds an okf-loom input folder from that raw.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
pip install c2r-collect # add [s3] extra for upload/download
|
|
8
|
+
cp config.example.toml config.toml # edit; password via env C2R_DB_PASSWORD
|
|
9
|
+
c2r-collect collect # → <root>/raw/<today>/
|
|
10
|
+
c2r-collect upload --date YYYY-MM-DD
|
|
11
|
+
c2r-collect download --date YYYY-MM-DD
|
|
12
|
+
c2r-collect make-input --date YYYY-MM-DD [--prev YYYY-MM-DD] [--add add.txt]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Design and release notes live in the source repository (`docs/`), not in the package.
|
|
16
|
+
|
|
17
|
+
## First run checks
|
|
18
|
+
See `docs/release.md` (release steps, QA-DB run, VDI production run).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "c2r-collect"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Oracle data dictionary + PL/SQL source collector that writes okf-loom input folders"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"oracledb>=2.0",
|
|
14
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
s3 = ["boto3>=1.34"]
|
|
19
|
+
dev = ["pytest>=8", "build", "twine"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
c2r-collect = "c2r_collect.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
package-dir = { "" = "src" }
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.package-data]
|
|
31
|
+
c2r_collect = ["queries/*.sql", "templates/*.md"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""c2r-collect: Oracle data dictionary + PL/SQL package sources -> okf-loom input folders.
|
|
2
|
+
|
|
3
|
+
PURPOSE Replace manual DBA spool requests. Pull everything from one Oracle schema into a dated
|
|
4
|
+
raw/ folder, then derive the folder okf-loom (a separate, frozen tool) reads.
|
|
5
|
+
ENTRY console script `c2r-collect` -> c2r_collect.cli:main. Run `c2r-collect --help`.
|
|
6
|
+
PIPELINE collect (DB -> raw/<date>/) -> [upload -> download] -> make-input (raw -> input/<date>/)
|
|
7
|
+
-> okf-loom enrich/build (outside this package).
|
|
8
|
+
MODULES cli (args) . config (config.toml) . db (ONLY module touching Oracle) . queries/*.sql
|
|
9
|
+
. codebook (dynamic SQL) . writers (file contract) . counters . report . collect
|
|
10
|
+
. make_input . transfer (S3) . discover
|
|
11
|
+
EXIT 0 ok . 2 input/config error or partial success . 3 DB/required output failed . 4 transfer failed
|
|
12
|
+
"""
|
|
13
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Command line interface. Parses arguments and wires modules; holds no business logic.
|
|
2
|
+
|
|
3
|
+
PURPOSE One entry point for humans, schedulers and LLM agents (`c2r-collect <command>`).
|
|
4
|
+
INPUTS config file (--config PATH, default ./config.toml; may be given before or after the command)
|
|
5
|
+
env C2R_DB_PASSWORD (collect, discover-codebook; prompts if unset)
|
|
6
|
+
env AWS_* or EC2 instance role (upload, download; read by boto3, never by this code)
|
|
7
|
+
OUTPUTS see each run_* function. Console output is ASCII-only; the LAST line is either
|
|
8
|
+
`ok: <path> ...` or `error: <reason>`, so a caller can parse it.
|
|
9
|
+
EXIT 0 ok . 2 input/config error, or collect finished with non-required errors
|
|
10
|
+
. 3 DB connect/required output failed . 4 S3 transfer failed
|
|
11
|
+
NOT HERE any decision about what to collect or select (collect.py, make_input.py).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import datetime as dt
|
|
17
|
+
import os
|
|
18
|
+
import platform
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import traceback
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import List, Optional
|
|
24
|
+
|
|
25
|
+
from . import __version__
|
|
26
|
+
from .collect import EXIT_DB, EXIT_INPUT, run_collect
|
|
27
|
+
from .config import Config, ConfigError, load_config, resolve_password
|
|
28
|
+
from .db import DbError
|
|
29
|
+
from .discover import run_discover
|
|
30
|
+
from .make_input import run_make_input
|
|
31
|
+
from .transfer import make_client, run_download, run_upload
|
|
32
|
+
|
|
33
|
+
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
34
|
+
DEFAULT_CONFIG = "config.toml"
|
|
35
|
+
|
|
36
|
+
# ASCII only: Windows cp949 consoles (VDI) garble arrows and dashes.
|
|
37
|
+
EPILOG = """\
|
|
38
|
+
PIPELINE (one cycle; <root> = config [paths] root):
|
|
39
|
+
1. collect DB -> <root>/raw/<date>/ pulls ALL packages, CSVs, dictionary, codebook
|
|
40
|
+
2. upload <root>/raw/<date>/ -> s3://<bucket>/<prefix>/<date>/ (when collect ran on VDI)
|
|
41
|
+
3. download s3 -> <root>/raw/<date>/ (on the build PC)
|
|
42
|
+
4. make-input <root>/raw/<date>/ -> <root>/input/<date>/ okf-loom input
|
|
43
|
+
selects packages with a previous description (--prev) plus --add names
|
|
44
|
+
5. okf-loom enrich/build --input <root>/input/<date> (separate tool, not run here)
|
|
45
|
+
|
|
46
|
+
OUTPUTS:
|
|
47
|
+
raw/<date>/collect-report.md read this first after collect (errors, counts, '?' evidence)
|
|
48
|
+
raw/<date>/MANIFEST.sha256 integrity; verified by upload/download
|
|
49
|
+
input/<date>/make-input-report.md selected / added / vanished packages
|
|
50
|
+
|
|
51
|
+
ENVIRONMENT:
|
|
52
|
+
C2R_DB_PASSWORD DB password for collect and discover-codebook (prompted if unset)
|
|
53
|
+
AWS_* upload/download credentials (SSO portal keys); EC2 uses its instance role
|
|
54
|
+
|
|
55
|
+
EXIT CODES:
|
|
56
|
+
0 ok
|
|
57
|
+
2 input or config error; or collect finished but a non-required file failed (see report)
|
|
58
|
+
3 cannot connect, or a required output failed (raw/<date>.partial/ kept for inspection)
|
|
59
|
+
4 S3 transfer failed (credentials, AccessDenied, network, manifest mismatch)
|
|
60
|
+
|
|
61
|
+
LAST LINE of output is always one of:
|
|
62
|
+
ok: <path> ... exit 0
|
|
63
|
+
partial: <path> - N problem(s) ... exit 2 (collect wrote raw/<date>/ but some files are missing)
|
|
64
|
+
error: <reason> exit 2, 3 or 4
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _default_db_factory(cfg: Config, password: str):
|
|
69
|
+
from .db import OracleDb
|
|
70
|
+
return OracleDb(cfg.db.dsn, cfg.db.user, password)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _driver_version() -> str:
|
|
74
|
+
try:
|
|
75
|
+
import oracledb
|
|
76
|
+
return f"python-oracledb {oracledb.__version__}"
|
|
77
|
+
except Exception: # pragma: no cover
|
|
78
|
+
return "python-oracledb (not installed)"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
82
|
+
"""argparse parser. The epilog is the machine-oriented manual shown by --help."""
|
|
83
|
+
p = argparse.ArgumentParser(prog="c2r-collect", description="Oracle -> okf-loom input collector",
|
|
84
|
+
epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
85
|
+
p.add_argument("--version", action="version", version=f"c2r-collect {__version__}")
|
|
86
|
+
p.add_argument("--config", dest="config_top", metavar="PATH", default=None, help=f"path to config (default ./{DEFAULT_CONFIG})")
|
|
87
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
88
|
+
|
|
89
|
+
def sp(name, help_, dated=True):
|
|
90
|
+
s = sub.add_parser(name, help=help_)
|
|
91
|
+
s.add_argument("--config", dest="config_sub", default=None, help=argparse.SUPPRESS)
|
|
92
|
+
if dated:
|
|
93
|
+
s.add_argument("--date", default=dt.date.today().isoformat(), help="YYYY-MM-DD (default today)")
|
|
94
|
+
s.add_argument("--force", action="store_true", help="overwrite an existing output folder")
|
|
95
|
+
return s
|
|
96
|
+
|
|
97
|
+
sp("collect", "DB -> raw/<date>/")
|
|
98
|
+
mi = sp("make-input", "raw/<date>/ -> input/<date>/ for okf-loom")
|
|
99
|
+
mi.add_argument("--prev", help="previous input date whose enrichments/ to carry (default: latest before --date)")
|
|
100
|
+
mi.add_argument("--add", help="file path or http(s) URL listing extra package names")
|
|
101
|
+
sp("upload", "raw/<date>/ -> S3")
|
|
102
|
+
sp("download", "S3 -> raw/<date>/")
|
|
103
|
+
sp("discover-codebook", "list candidate codebook tables by name pattern", dated=False)
|
|
104
|
+
return p
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main(argv: Optional[List[str]] = None, db_factory=_default_db_factory, client_factory=make_client,
|
|
108
|
+
env=None) -> int:
|
|
109
|
+
try:
|
|
110
|
+
return _main(argv, db_factory, client_factory, env)
|
|
111
|
+
except SystemExit:
|
|
112
|
+
raise
|
|
113
|
+
except Exception as e: # keep the last-line contract even for bugs, disk full, locked files
|
|
114
|
+
traceback.print_exc(file=sys.stderr)
|
|
115
|
+
print(f"error: unexpected {type(e).__name__}: {str(e).splitlines()[0] if str(e) else ''}")
|
|
116
|
+
return EXIT_DB
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _main(argv, db_factory, client_factory, env) -> int:
|
|
120
|
+
"""Run one command and return its exit code (also used as the process exit status).
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
argv: arguments without the program name; None means sys.argv[1:].
|
|
124
|
+
db_factory: (Config, password) -> Db. Tests inject FakeDb here.
|
|
125
|
+
client_factory: (region) -> boto3-like S3 client. Tests inject a fake.
|
|
126
|
+
env: environment mapping; None means os.environ.
|
|
127
|
+
Returns:
|
|
128
|
+
0, 2, 3 or 4 (see module docstring). Never raises for expected failures.
|
|
129
|
+
"""
|
|
130
|
+
for stream in (sys.stdout, sys.stderr):
|
|
131
|
+
if hasattr(stream, "reconfigure"):
|
|
132
|
+
stream.reconfigure(errors="replace") # cp949 consoles must not crash on a stray character
|
|
133
|
+
args = build_parser().parse_args(argv)
|
|
134
|
+
env = os.environ if env is None else env
|
|
135
|
+
cfg_path = Path(args.config_sub or args.config_top or DEFAULT_CONFIG)
|
|
136
|
+
try:
|
|
137
|
+
cfg = load_config(cfg_path)
|
|
138
|
+
except ConfigError as e:
|
|
139
|
+
print(f"error: {e}")
|
|
140
|
+
return EXIT_INPUT
|
|
141
|
+
date = getattr(args, "date", None)
|
|
142
|
+
if date is not None and not DATE_RE.match(date):
|
|
143
|
+
print(f"error: --date must be YYYY-MM-DD, got {date!r}")
|
|
144
|
+
return EXIT_INPUT
|
|
145
|
+
|
|
146
|
+
if args.cmd in ("collect", "discover-codebook"):
|
|
147
|
+
try:
|
|
148
|
+
pw = resolve_password(env)
|
|
149
|
+
except ConfigError as e:
|
|
150
|
+
print(f"error: {e}")
|
|
151
|
+
return EXIT_INPUT
|
|
152
|
+
try:
|
|
153
|
+
db = db_factory(cfg, pw)
|
|
154
|
+
except DbError as e:
|
|
155
|
+
print(f"error: cannot connect: {e}")
|
|
156
|
+
return EXIT_DB
|
|
157
|
+
try:
|
|
158
|
+
if args.cmd == "collect":
|
|
159
|
+
return run_collect(cfg, db, date, force=args.force, host=platform.node(), driver=_driver_version())
|
|
160
|
+
return run_discover(cfg, db)
|
|
161
|
+
finally:
|
|
162
|
+
db.close()
|
|
163
|
+
|
|
164
|
+
if args.cmd == "make-input":
|
|
165
|
+
return run_make_input(cfg.root, date, cfg.scope.source_owner, args.prev, args.add, force=args.force)
|
|
166
|
+
|
|
167
|
+
if cfg.s3 is None:
|
|
168
|
+
print("error: [s3] section missing in config")
|
|
169
|
+
return EXIT_INPUT
|
|
170
|
+
try:
|
|
171
|
+
client = client_factory(cfg.s3.region)
|
|
172
|
+
except ImportError:
|
|
173
|
+
print("error: boto3 not installed - pip install 'c2r-collect[s3]'")
|
|
174
|
+
return EXIT_INPUT
|
|
175
|
+
if args.cmd == "upload":
|
|
176
|
+
return run_upload(cfg.root, date, cfg.s3, client)
|
|
177
|
+
if args.force:
|
|
178
|
+
print("note: --force is ignored for download (an existing raw/<date>/ is never overwritten)")
|
|
179
|
+
return run_download(cfg.root, date, cfg.s3, client)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__": # pragma: no cover
|
|
183
|
+
sys.exit(main())
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""SQL builders for codebook (parameter/code) tables and for discover-codebook.
|
|
2
|
+
|
|
3
|
+
PURPOSE Codebook queries need table names in the SQL text (Oracle cannot bind identifiers).
|
|
4
|
+
SECURITY every owner/table name passes check_identifier() before being inlined:
|
|
5
|
+
^[A-Z][A-Z0-9_$#]{0,127}$ after upper-casing. Anything else raises ValueError.
|
|
6
|
+
No other value is ever inlined; there are no user-supplied literals.
|
|
7
|
+
OUTPUTS SQL strings only. Execution happens in collect.py / discover.py via Db.query_sql.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Sequence
|
|
13
|
+
|
|
14
|
+
IDENT = re.compile(r"^[A-Z][A-Z0-9_$#]{0,127}$")
|
|
15
|
+
PATTERN = re.compile(r"^[A-Z0-9_$#%]{1,128}$")
|
|
16
|
+
|
|
17
|
+
# Generic, vendor-neutral defaults. Override per site in config [scope] discover_include / discover_exclude.
|
|
18
|
+
DEFAULT_DISCOVER_INCLUDE = ("%CODE%", "%PARAM%", "%LABEL%", "%LANG%", "%MESSAGE%", "%LIST%", "%TYPE%", "REF%", "%REF")
|
|
19
|
+
DEFAULT_DISCOVER_EXCLUDE = ("TMP%", "STG%", "TEST%", "%BKP%", "%BACKUP%", "%OLD")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_identifier(s: str) -> str:
|
|
23
|
+
"""Upper-case and validate an Oracle identifier before it is inlined into SQL.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: not ^[A-Z][A-Z0-9_$#]{0,127}$ (blocks injection such as "T; DROP").
|
|
27
|
+
"""
|
|
28
|
+
u = (s or "").upper()
|
|
29
|
+
if not IDENT.match(u):
|
|
30
|
+
raise ValueError(f"invalid Oracle identifier: {s!r}")
|
|
31
|
+
return u
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_pattern(s: str) -> str:
|
|
35
|
+
"""Validate a LIKE pattern from config before inlining (letters, digits, _ $ # % only)."""
|
|
36
|
+
u = (s or "").upper()
|
|
37
|
+
if not PATTERN.match(u):
|
|
38
|
+
raise ValueError(f"invalid LIKE pattern: {s!r}")
|
|
39
|
+
return u
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _names(tables: Sequence[str]) -> list:
|
|
43
|
+
names = [check_identifier(t) for t in tables]
|
|
44
|
+
if not names:
|
|
45
|
+
raise ValueError("codebook table list is empty")
|
|
46
|
+
return names
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _in_list(tables: Sequence[str]) -> str:
|
|
50
|
+
return ", ".join(f"'{n}'" for n in _names(tables))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _name_rows(tables: Sequence[str]) -> str:
|
|
54
|
+
return " UNION ALL ".join(f"SELECT '{n}' AS table_name FROM dual" for n in _names(tables))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def sql_exists(owner: str, tables: Sequence[str]) -> str:
|
|
58
|
+
o = check_identifier(owner)
|
|
59
|
+
return (
|
|
60
|
+
"SELECT g.table_name, "
|
|
61
|
+
"CASE WHEN t.table_name IS NULL THEN 'MISSING' ELSE 'EXISTS' END AS status, "
|
|
62
|
+
"t.num_rows AS stat_rows, t.last_analyzed, "
|
|
63
|
+
f"(SELECT COUNT(*) FROM all_tab_columns c WHERE c.owner = '{o}' AND c.table_name = g.table_name) AS col_cnt "
|
|
64
|
+
f"FROM ({_name_rows(tables)}) g "
|
|
65
|
+
f"LEFT JOIN all_tables t ON t.owner = '{o}' AND t.table_name = g.table_name "
|
|
66
|
+
"ORDER BY g.table_name"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def sql_rowcount_one(owner: str, table: str) -> str:
|
|
71
|
+
"""COUNT(*) for one table. One statement per table so a missing table loses only its own row."""
|
|
72
|
+
o, n = check_identifier(owner), check_identifier(table)
|
|
73
|
+
return f"SELECT '{n}' AS table_name, COUNT(*) AS real_rows FROM {o}.{n}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def sql_owners(tables: Sequence[str]) -> str:
|
|
77
|
+
return ("SELECT t.owner, t.table_name, t.num_rows AS stat_rows, t.last_analyzed "
|
|
78
|
+
f"FROM all_tables t WHERE t.table_name IN ({_in_list(tables)}) "
|
|
79
|
+
"ORDER BY t.table_name, t.owner")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def sql_columns(owner: str, tables: Sequence[str]) -> str:
|
|
83
|
+
o = check_identifier(owner)
|
|
84
|
+
return ("SELECT c.table_name, c.column_id, c.column_name, c.data_type, c.data_length, c.nullable, cc.comments "
|
|
85
|
+
"FROM all_tab_columns c "
|
|
86
|
+
"LEFT JOIN all_col_comments cc ON cc.owner = c.owner AND cc.table_name = c.table_name "
|
|
87
|
+
"AND cc.column_name = c.column_name "
|
|
88
|
+
f"WHERE c.owner = '{o}' AND c.table_name IN ({_in_list(tables)}) "
|
|
89
|
+
"ORDER BY c.table_name, c.column_id")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def sql_dump(owner: str, table: str) -> str:
|
|
93
|
+
return f"SELECT * FROM {check_identifier(owner)}.{check_identifier(table)}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def sql_discover(owner: str, include: Sequence[str] = DEFAULT_DISCOVER_INCLUDE,
|
|
97
|
+
exclude: Sequence[str] = DEFAULT_DISCOVER_EXCLUDE) -> str:
|
|
98
|
+
o = check_identifier(owner)
|
|
99
|
+
inc = [check_pattern(p) for p in include] or ["%"]
|
|
100
|
+
exc = [check_pattern(p) for p in exclude]
|
|
101
|
+
inc_sql = " OR ".join(f"UPPER(t.table_name) LIKE '{p}'" for p in inc)
|
|
102
|
+
exc_sql = " AND ".join(f"t.table_name NOT LIKE '{p}'" for p in exc) or "1 = 1"
|
|
103
|
+
return ("SELECT t.table_name, t.num_rows AS stat_rows, "
|
|
104
|
+
"(SELECT COUNT(*) FROM all_tab_columns c WHERE c.owner = t.owner AND c.table_name = t.table_name) AS col_cnt, "
|
|
105
|
+
"(SELECT tc.comments FROM all_tab_comments tc WHERE tc.owner = t.owner AND tc.table_name = t.table_name) AS table_comment "
|
|
106
|
+
f"FROM all_tables t WHERE t.owner = '{o}' AND ({inc_sql}) AND {exc_sql} "
|
|
107
|
+
"ORDER BY t.table_name")
|