limitsdb 0.5.0__py3-none-any.whl
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/__init__.py +0 -0
- limitsdb/cli/__init__.py +0 -0
- limitsdb/cli/ldb_crypt.py +44 -0
- limitsdb/cli/ldb_impl.py +207 -0
- limitsdb/cli/ldb_init.py +60 -0
- limitsdb/cli/ldb_run.py +53 -0
- limitsdb/core/__init__.py +0 -0
- limitsdb/core/ldb_config_loader.py +198 -0
- limitsdb/core/ldb_crypto.py +91 -0
- limitsdb/core/ldb_errors.py +30 -0
- limitsdb/core/ldb_ilm_config.py +228 -0
- limitsdb/core/ldb_logger.py +57 -0
- limitsdb/core/ldb_meta.py +31 -0
- limitsdb/core/ldb_params_config.py +459 -0
- limitsdb/core/ldb_runner.py +810 -0
- limitsdb/core/ldb_status.py +8 -0
- limitsdb/core/ldb_utils.py +332 -0
- limitsdb/db/__init__.py +0 -0
- limitsdb/db/ldb_engine_loader.py +16 -0
- limitsdb/db/ldb_engines.py +347 -0
- limitsdb/db/oracle/__init__.py +0 -0
- limitsdb/db/oracle/ldb_conf.sql +151 -0
- limitsdb/db/oracle/ldb_engine_impl.py +1505 -0
- limitsdb/resources/__init__.py +0 -0
- limitsdb/resources/ilm.example.yml +71 -0
- limitsdb-0.5.0.dist-info/METADATA +586 -0
- limitsdb-0.5.0.dist-info/RECORD +32 -0
- limitsdb-0.5.0.dist-info/WHEEL +4 -0
- limitsdb-0.5.0.dist-info/entry_points.txt +6 -0
- limitsdb-0.5.0.dist-info/licenses/LICENSE +118 -0
- limitsdb-0.5.0.dist-info/licenses/LICENSE-DUAL.md +59 -0
- limitsdb-0.5.0.dist-info/licenses/NOTICE +12 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
from concurrent.futures import Future, ProcessPoolExecutor, as_completed
|
|
2
|
+
from operator import attrgetter
|
|
3
|
+
from typing import Any, Literal, TypedDict, cast
|
|
4
|
+
|
|
5
|
+
from limitsdb.core.ldb_errors import ConfigurationError
|
|
6
|
+
from limitsdb.core.ldb_ilm_config import IlmRule, load_rows_from_yaml
|
|
7
|
+
from limitsdb.core.ldb_logger import configure_logger, get_logger, reconfigure_logger
|
|
8
|
+
from limitsdb.core.ldb_params_config import Config
|
|
9
|
+
from limitsdb.core.ldb_status import Status
|
|
10
|
+
from limitsdb.core.ldb_utils import get_effective_credentials, max_ignore_none, nvl
|
|
11
|
+
from limitsdb.db.ldb_engine_loader import get_db_engine
|
|
12
|
+
from limitsdb.db.ldb_engines import ColumnDefinition, DatabaseEngine, TableDefinition
|
|
13
|
+
|
|
14
|
+
logger = get_logger("runner")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _OtherColumn(TypedDict):
|
|
18
|
+
name: str
|
|
19
|
+
expr: str
|
|
20
|
+
metadata: ColumnDefinition
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProcessedTableConfig(TypedDict, total=False):
|
|
24
|
+
"""Mutable derived state for one table during planning and execution."""
|
|
25
|
+
|
|
26
|
+
conds: list[IlmRule]
|
|
27
|
+
referencing_tables: list[tuple[str, str]]
|
|
28
|
+
table_columns: list[str]
|
|
29
|
+
columns_metadata: dict[str, ColumnDefinition]
|
|
30
|
+
other_columns: list[_OtherColumn]
|
|
31
|
+
months_keep_history_max: int | None
|
|
32
|
+
skip: bool
|
|
33
|
+
query_expr: str
|
|
34
|
+
sql_block: str
|
|
35
|
+
metadata: Any
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
TablesConfig = dict[tuple[str, str], ProcessedTableConfig]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
WorkerResult = tuple[str, str, str, int, int, int | None, str | None]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _exception_details(exc: Exception) -> tuple[int | None, str]:
|
|
45
|
+
"""Extract stable error details without assuming an Oracle exception shape."""
|
|
46
|
+
detail: Any = exc.args[0] if len(exc.args) == 1 else exc
|
|
47
|
+
code = getattr(detail, "code", None)
|
|
48
|
+
message = getattr(detail, "message", None)
|
|
49
|
+
if not isinstance(code, int):
|
|
50
|
+
code = None
|
|
51
|
+
if not isinstance(message, str) or not message:
|
|
52
|
+
message = str(exc) or type(exc).__name__
|
|
53
|
+
return code, message
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _credentials_present(config: Config, *, admin: bool, env: Literal["SOURCE", "HISTORY"] | None) -> bool:
|
|
57
|
+
try:
|
|
58
|
+
get_effective_credentials(config, admin=admin, env=env)
|
|
59
|
+
return True
|
|
60
|
+
except ValueError:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _open_connection(
|
|
65
|
+
config: Config, engine: DatabaseEngine, *, admin: bool, env: Literal["SOURCE", "HISTORY"] | None, description: str
|
|
66
|
+
) -> Any | None:
|
|
67
|
+
try:
|
|
68
|
+
user, _, dsn = get_effective_credentials(config, admin=admin, env=env)
|
|
69
|
+
except ValueError as exc:
|
|
70
|
+
logger.error("%s: %s", description, exc)
|
|
71
|
+
return None
|
|
72
|
+
conn: Any | None = None
|
|
73
|
+
try:
|
|
74
|
+
conn = engine.get_connection(config, admin=admin, env=env)
|
|
75
|
+
engine.get_system_date(conn)
|
|
76
|
+
logger.info("%s: connected to %s as %s", description, dsn, user)
|
|
77
|
+
return conn
|
|
78
|
+
except Exception:
|
|
79
|
+
logger.error("%s: failed to connect to %s as %s", description, dsn, user, exc_info=True)
|
|
80
|
+
if conn:
|
|
81
|
+
engine.close_connection(conn)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _load_offline_rows(config: Config) -> list[IlmRule]:
|
|
86
|
+
if config.context.ilm_config_file:
|
|
87
|
+
return load_rows_from_yaml(config.context.ilm_config_file)
|
|
88
|
+
schema = config.context.schema
|
|
89
|
+
profile = config.context.profile
|
|
90
|
+
name = f"ilm.{profile}.yml" if profile else "ilm.yml"
|
|
91
|
+
raise ConfigurationError(
|
|
92
|
+
f"ILM configuration file '{name}' not found for schema '{schema}'. Provide the file or set LDB_ILM_CONFIG_FILE."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_dependency_graph(rows: list[IlmRule]) -> dict[tuple[str, str], set[tuple[str, str]]]:
|
|
97
|
+
graph: dict[tuple[str, str], set[tuple[str, str]]] = {}
|
|
98
|
+
for row in rows:
|
|
99
|
+
key = (row["source_owner"], row["table_name"])
|
|
100
|
+
graph.setdefault(key, set())
|
|
101
|
+
for row in rows:
|
|
102
|
+
key = (row["source_owner"], row["table_name"])
|
|
103
|
+
refs = str(row.get("referencing_tables") or "").split(",")
|
|
104
|
+
for raw_ref in refs:
|
|
105
|
+
ref = raw_ref.strip()
|
|
106
|
+
if not ref:
|
|
107
|
+
continue
|
|
108
|
+
if "." in ref:
|
|
109
|
+
owner_part, remainder = ref.split(".", 1)
|
|
110
|
+
else:
|
|
111
|
+
owner_part, remainder = row["source_owner"], ref
|
|
112
|
+
parts = remainder.strip().split(" ", 1)
|
|
113
|
+
if len(parts) != 2:
|
|
114
|
+
raise ValueError(f"Invalid referencing_tables entry '{ref}' for {owner_part}")
|
|
115
|
+
table_part = parts[0]
|
|
116
|
+
ref_key = (owner_part.strip(), table_part.strip())
|
|
117
|
+
if ref_key not in graph:
|
|
118
|
+
raise ValueError(f"Referenced table {owner_part}.{table_part} not present in ILM configuration")
|
|
119
|
+
graph[ref_key].add(key)
|
|
120
|
+
return graph
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _compute_plan_layers(graph: dict[tuple[str, str], set[tuple[str, str]]]) -> list[list[tuple[str, str]]]:
|
|
124
|
+
remaining = set(graph.keys())
|
|
125
|
+
layers: list[list[tuple[str, str]]] = []
|
|
126
|
+
while remaining:
|
|
127
|
+
ready = sorted([key for key in remaining if not graph[key].intersection(remaining)])
|
|
128
|
+
if not ready:
|
|
129
|
+
cycle = ", ".join(f"{owner}.{table}" for owner, table in sorted(remaining))
|
|
130
|
+
raise ValueError(f"Cyclic dependency detected among tables: {cycle}")
|
|
131
|
+
layers.append(ready)
|
|
132
|
+
for key in ready:
|
|
133
|
+
remaining.remove(key)
|
|
134
|
+
return layers
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _collect_privilege_targets(
|
|
138
|
+
tables_conf_rows: list[IlmRule], owner_key: Literal["source_owner", "history_owner"], engine: DatabaseEngine
|
|
139
|
+
) -> list[tuple[str, str]]:
|
|
140
|
+
tables: set[tuple[str, str]] = set(
|
|
141
|
+
[
|
|
142
|
+
(engine.get_identifier_str(r[owner_key]), engine.get_identifier_str(r["table_name"]))
|
|
143
|
+
for r in tables_conf_rows
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
return sorted(tables)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _runtime_owner(config: Config, rule: IlmRule) -> str:
|
|
150
|
+
"""Return the schema whose rows are processed for the selected ILM action."""
|
|
151
|
+
return rule["source_owner"] if config.execution.action == "SOURCE_ILM" else rule["history_owner"]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _runtime_join_expression(config: Config, rule: IlmRule) -> str:
|
|
155
|
+
"""Render source joins, avoiding them when history predicates use stored derived columns."""
|
|
156
|
+
join_expr = rule["join_expr"]
|
|
157
|
+
if not join_expr or (config.execution.action == "HISTORY_ILM" and config.execution.use_added_columns):
|
|
158
|
+
return ""
|
|
159
|
+
join_type = "left outer" if rule["source_orphan_purge"] == "Y" else "inner"
|
|
160
|
+
return join_expr.replace("@", join_type)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _orphan_predicate(rule: IlmRule) -> str:
|
|
164
|
+
"""Return the predicate that identifies a missing referenced row."""
|
|
165
|
+
if rule["source_orphan_purge"] != "Y":
|
|
166
|
+
return ""
|
|
167
|
+
columns = [column.strip() for column in (rule["orphan_check_column"] or "").split(",") if column.strip()]
|
|
168
|
+
return " or ".join(f"{column} is null" for column in columns)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _validate_orphan_configuration(config: Config, rule: IlmRule, key: tuple[str, str]) -> None:
|
|
172
|
+
"""Validate the relationship data needed to archive orphan source rows."""
|
|
173
|
+
if rule["source_orphan_purge"] != "Y":
|
|
174
|
+
if rule["orphan_check_column"]:
|
|
175
|
+
raise ValueError(f"source_orphan_purge must be enabled when orphan_check_column is set for {key}")
|
|
176
|
+
return
|
|
177
|
+
missing = [
|
|
178
|
+
name
|
|
179
|
+
for name, value in (
|
|
180
|
+
("referencing_tables", rule["referencing_tables"]),
|
|
181
|
+
("join_expr", rule["join_expr"]),
|
|
182
|
+
("orphan_check_column", rule["orphan_check_column"]),
|
|
183
|
+
)
|
|
184
|
+
if not value
|
|
185
|
+
]
|
|
186
|
+
if missing:
|
|
187
|
+
raise ValueError(f"{', '.join(missing)} required when source_orphan_purge is enabled for {key}")
|
|
188
|
+
if not config.execution.use_added_columns:
|
|
189
|
+
raise ValueError(f"use_added_columns must be enabled when source_orphan_purge is enabled for {key}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _relationship_date_column(alias: str) -> str:
|
|
193
|
+
"""Return the stable unquoted name used to persist a related table's retention date."""
|
|
194
|
+
return f"LDB_DATE_{alias.upper()}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
_ORPHAN_MARKER_COLUMN = "LDB_IS_ORPHAN"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _plan_mode(config: Config) -> int:
|
|
201
|
+
rows = _load_offline_rows(config)
|
|
202
|
+
if not rows:
|
|
203
|
+
logger.info("No active ILM tables found for schema %s.", config.context.schema)
|
|
204
|
+
return 0
|
|
205
|
+
graph = _build_dependency_graph(rows)
|
|
206
|
+
layers = _compute_plan_layers(graph)
|
|
207
|
+
logger.info("ILM execution plan (PLAN mode):")
|
|
208
|
+
for idx, layer in enumerate(layers, start=1):
|
|
209
|
+
tables = ", ".join(f"{owner}.{table}" for owner, table in layer)
|
|
210
|
+
logger.info(" Stage %d (parallel=%d): %s", idx, len(layer), tables)
|
|
211
|
+
logger.info("Plan summary: %d stage(s), %d table(s).", len(layers), len(graph))
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _validate_environment(config: Config, engine: DatabaseEngine) -> int:
|
|
216
|
+
logger.info("VALIDATE mode: checking configuration and database connectivity.")
|
|
217
|
+
ok = True
|
|
218
|
+
connections: list[Any] = []
|
|
219
|
+
primary_conn: Any | None = None
|
|
220
|
+
required_specs: list[tuple[str, bool, Literal["SOURCE", "HISTORY"] | None]] = []
|
|
221
|
+
optional_specs: list[tuple[str, bool, Literal["SOURCE", "HISTORY"] | None]] = []
|
|
222
|
+
primary_env = "SOURCE" if config.execution.action == "SOURCE_ILM" else "HISTORY"
|
|
223
|
+
required_specs.append((f"{primary_env} runtime", False, None))
|
|
224
|
+
required_specs.append((f"{primary_env} admin", True, None))
|
|
225
|
+
if config.execution.action == "SOURCE_ILM":
|
|
226
|
+
required_specs.append(("HISTORY admin", True, "HISTORY"))
|
|
227
|
+
optional_specs.append(("HISTORY runtime", False, "HISTORY"))
|
|
228
|
+
else:
|
|
229
|
+
optional_specs.append(("SOURCE runtime", False, "SOURCE"))
|
|
230
|
+
optional_specs.append(("SOURCE admin", True, "SOURCE"))
|
|
231
|
+
for description, admin, env in required_specs:
|
|
232
|
+
desc_label = description.upper()
|
|
233
|
+
conn = _open_connection(config, engine, admin=admin, env=env, description=desc_label)
|
|
234
|
+
if conn is None:
|
|
235
|
+
ok = False
|
|
236
|
+
else:
|
|
237
|
+
connections.append(conn)
|
|
238
|
+
if description == f"{primary_env} runtime":
|
|
239
|
+
primary_conn = conn
|
|
240
|
+
for description, admin, env in optional_specs:
|
|
241
|
+
if not _credentials_present(config, admin=admin, env=env):
|
|
242
|
+
logger.info("%s: credentials not provided; skipping.", description.upper())
|
|
243
|
+
continue
|
|
244
|
+
conn = _open_connection(config, engine, admin=admin, env=env, description=description.upper())
|
|
245
|
+
if conn:
|
|
246
|
+
connections.append(conn)
|
|
247
|
+
try:
|
|
248
|
+
if primary_conn:
|
|
249
|
+
process_date = engine.get_system_date(primary_conn).strftime("%Y%m%d")
|
|
250
|
+
try:
|
|
251
|
+
tables_config = process_tables_cnf(primary_conn, config, engine, process_date)
|
|
252
|
+
logger.info("Configuration ready: %d table(s) evaluated.", len(tables_config))
|
|
253
|
+
_log_where_predicates(tables_config)
|
|
254
|
+
except Exception:
|
|
255
|
+
logger.error("Failed to process ILM configuration.", exc_info=True)
|
|
256
|
+
ok = False
|
|
257
|
+
else:
|
|
258
|
+
logger.error("Primary connection unavailable; skipping configuration validation.")
|
|
259
|
+
finally:
|
|
260
|
+
for conn in connections:
|
|
261
|
+
engine.close_connection(conn)
|
|
262
|
+
return 0 if ok else 1
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _clone_column_definition(name: str, template: ColumnDefinition) -> ColumnDefinition:
|
|
266
|
+
return ColumnDefinition(
|
|
267
|
+
name=name,
|
|
268
|
+
data_type=template.data_type,
|
|
269
|
+
length=template.length,
|
|
270
|
+
precision=template.precision,
|
|
271
|
+
scale=template.scale,
|
|
272
|
+
nullable=template.nullable,
|
|
273
|
+
default=template.default,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _build_history_table_definition(
|
|
278
|
+
config: Config,
|
|
279
|
+
engine: DatabaseEngine,
|
|
280
|
+
source_connection: Any,
|
|
281
|
+
table_cnf: ProcessedTableConfig,
|
|
282
|
+
) -> TableDefinition:
|
|
283
|
+
cnd0 = table_cnf["conds"][0]
|
|
284
|
+
history_owner, table_name = cnd0["history_owner"], cnd0["table_name"]
|
|
285
|
+
metadata_owner = _runtime_owner(config, cnd0)
|
|
286
|
+
required_columns: list[ColumnDefinition] = sorted(
|
|
287
|
+
table_cnf.get("columns_metadata", {}).values(), key=attrgetter("id")
|
|
288
|
+
)
|
|
289
|
+
for other_column in table_cnf.get("other_columns", []):
|
|
290
|
+
required_columns.append(other_column["metadata"])
|
|
291
|
+
pk_columns = list(engine.get_primary_key_columns(source_connection, metadata_owner, table_name, table_cnf))
|
|
292
|
+
if config.execution.add_ldb_columns:
|
|
293
|
+
if "LDB_PROCESS_DATE" not in pk_columns:
|
|
294
|
+
pk_columns.append("LDB_PROCESS_DATE")
|
|
295
|
+
primary_key: tuple[str, ...] | None = tuple(pk_columns) if pk_columns else None
|
|
296
|
+
return TableDefinition(
|
|
297
|
+
owner=history_owner,
|
|
298
|
+
name=table_name,
|
|
299
|
+
columns=tuple(required_columns),
|
|
300
|
+
primary_key=primary_key,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _ensure_history_tables(
|
|
305
|
+
config: Config,
|
|
306
|
+
engine: DatabaseEngine,
|
|
307
|
+
source_admin_connection: Any,
|
|
308
|
+
tables_config: TablesConfig,
|
|
309
|
+
) -> None:
|
|
310
|
+
history_admin_connection: Any | None = None
|
|
311
|
+
try:
|
|
312
|
+
history_admin_connection = engine.get_connection(config, admin=True, env="HISTORY")
|
|
313
|
+
for table_cnf in tables_config.values():
|
|
314
|
+
if table_cnf.get("skip"):
|
|
315
|
+
continue
|
|
316
|
+
table_def = _build_history_table_definition(config, engine, source_admin_connection, table_cnf)
|
|
317
|
+
engine.ensure_table_structure(history_admin_connection, table_def, table_cnf)
|
|
318
|
+
finally:
|
|
319
|
+
if history_admin_connection:
|
|
320
|
+
engine.close_connection(history_admin_connection)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _initialize_worker_logger(log_level: str) -> None:
|
|
324
|
+
"""Ensure background processes emit logs using the configured level."""
|
|
325
|
+
# ``ProcessPoolExecutor`` uses ``spawn`` on Windows, which re-imports the entry module and
|
|
326
|
+
# resets the logger configuration. Re-apply the desired level so worker logs reach stdout.
|
|
327
|
+
configure_logger(level=log_level)
|
|
328
|
+
reconfigure_logger(level=log_level)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _log_where_predicates(tables_config: TablesConfig) -> None:
|
|
332
|
+
any_predicates = False
|
|
333
|
+
for table_info in tables_config.values():
|
|
334
|
+
query_expr = table_info.get("query_expr")
|
|
335
|
+
if not query_expr:
|
|
336
|
+
continue
|
|
337
|
+
if not any_predicates:
|
|
338
|
+
logger.info("WHERE predicates evaluated for ILM:")
|
|
339
|
+
any_predicates = True
|
|
340
|
+
logger.info("\n%s", query_expr)
|
|
341
|
+
if not any_predicates:
|
|
342
|
+
logger.info("There are no WHERE predicates calculated for ILM tables.")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _append_unique_name(other_columns: list[_OtherColumn], other_column: _OtherColumn) -> None:
|
|
346
|
+
"""Appends a column to the other_columns list if its name is not already present."""
|
|
347
|
+
if other_column["name"] not in {col["name"] for col in other_columns}:
|
|
348
|
+
other_columns.append(other_column)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def process_table(config: Config, owner: str, table_name: str, plsql_code: str, process_date: str) -> WorkerResult:
|
|
352
|
+
logger.info(f"Processing table {owner}.{table_name}...")
|
|
353
|
+
prev_rows_processed = 0
|
|
354
|
+
engine: DatabaseEngine | None = None
|
|
355
|
+
conn: Any = None
|
|
356
|
+
process_start: Any = None
|
|
357
|
+
try:
|
|
358
|
+
engine = get_db_engine(config.connections.db_engine)
|
|
359
|
+
conn = engine.get_connection(config)
|
|
360
|
+
user, _, dsn = get_effective_credentials(config, admin=False)
|
|
361
|
+
logger.info(f"Connected to {dsn} as {user}")
|
|
362
|
+
process_start = engine.get_system_date(conn)
|
|
363
|
+
logger.info(f"Executing ILM for {owner}.{table_name}...")
|
|
364
|
+
prev_rows_processed = engine.get_rows_processed(conn, owner, table_name, process_date)
|
|
365
|
+
engine.sql_block_run(conn, plsql_code)
|
|
366
|
+
rows_processed = engine.get_rows_processed(conn, owner, table_name, process_date)
|
|
367
|
+
return (owner, table_name, Status.TABLE_END, prev_rows_processed, rows_processed, None, None)
|
|
368
|
+
except Exception as exc:
|
|
369
|
+
sqlcode, message = _exception_details(exc)
|
|
370
|
+
rows_processed = prev_rows_processed
|
|
371
|
+
logger.error("Worker failed for %s.%s: %s", owner, table_name, message, exc_info=True)
|
|
372
|
+
if engine is not None and conn is not None and process_start is not None:
|
|
373
|
+
try:
|
|
374
|
+
rows_processed = engine.get_rows_processed(conn, owner, table_name, process_date)
|
|
375
|
+
engine.save_error_status(
|
|
376
|
+
conn, config, owner, table_name, process_date, process_start, message, plsql_code
|
|
377
|
+
)
|
|
378
|
+
except Exception:
|
|
379
|
+
logger.critical("Failed to persist recovery evidence for %s.%s", owner, table_name, exc_info=True)
|
|
380
|
+
return (owner, table_name, Status.ERROR, prev_rows_processed, rows_processed, sqlcode, message)
|
|
381
|
+
finally:
|
|
382
|
+
if engine is not None and conn is not None:
|
|
383
|
+
engine.close_connection(conn)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def generate_script_output(config: Config, tables_config: TablesConfig) -> int:
|
|
387
|
+
print(
|
|
388
|
+
f"""
|
|
389
|
+
whenever oserror exit 1
|
|
390
|
+
whenever sqlerror exit 1
|
|
391
|
+
set echo on ver off trimspool on
|
|
392
|
+
spool ldb_{config.context.schema}.log
|
|
393
|
+
COLUMN process_date NEW_VALUE process_date
|
|
394
|
+
SELECT TO_CHAR(SYSDATE, 'YYYYMMDD') process_date FROM DUAL;
|
|
395
|
+
"""
|
|
396
|
+
)
|
|
397
|
+
while True:
|
|
398
|
+
cycle_printed = False
|
|
399
|
+
for (owner, table_name), table_info in tables_config.items():
|
|
400
|
+
if table_info["skip"]:
|
|
401
|
+
continue
|
|
402
|
+
cd = tables_config[(owner, table_name)]["conds"][0]
|
|
403
|
+
if cd["ctl_status"] in ["GENERATED"]:
|
|
404
|
+
continue
|
|
405
|
+
referencing_tables = table_info["referencing_tables"]
|
|
406
|
+
all_referencing_tables_ready = True
|
|
407
|
+
if referencing_tables:
|
|
408
|
+
for ref_owner, ref_table in referencing_tables:
|
|
409
|
+
cdr = tables_config[(ref_owner, ref_table)]["conds"][0]
|
|
410
|
+
if cdr["ctl_status"] != "GENERATED": # print end
|
|
411
|
+
all_referencing_tables_ready = False
|
|
412
|
+
break # referencing table not GENERATED status
|
|
413
|
+
if not all_referencing_tables_ready:
|
|
414
|
+
continue
|
|
415
|
+
print(f"rem table: {owner}.{table_name}")
|
|
416
|
+
print(table_info["sql_block"])
|
|
417
|
+
print("/")
|
|
418
|
+
cd["ctl_status"] = "GENERATED" # print end
|
|
419
|
+
cycle_printed = True
|
|
420
|
+
if not cycle_printed:
|
|
421
|
+
break # There are no processes to launch, so go to waiting some process to end
|
|
422
|
+
print("\nspool off\nexit 0\n")
|
|
423
|
+
return 0
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def get_next_ready_table(
|
|
427
|
+
tables_config: TablesConfig, active_tables: set[tuple[str, str]]
|
|
428
|
+
) -> tuple[str, str, ProcessedTableConfig] | None:
|
|
429
|
+
for (owner, table_name), table_cnf in tables_config.items():
|
|
430
|
+
if table_cnf["skip"]:
|
|
431
|
+
continue
|
|
432
|
+
cd = tables_config[(owner, table_name)]["conds"][0]
|
|
433
|
+
if cd["ctl_status"] in [Status.SKIPPED, Status.TABLE_END] or (owner, table_name) in active_tables:
|
|
434
|
+
continue
|
|
435
|
+
referencing_tables = table_cnf["referencing_tables"]
|
|
436
|
+
all_referencing_tables_ready = True
|
|
437
|
+
if referencing_tables:
|
|
438
|
+
for ref_owner, ref_table in referencing_tables:
|
|
439
|
+
cdr = tables_config[(ref_owner, ref_table)]["conds"][0]
|
|
440
|
+
if cdr["ctl_status"] != Status.TABLE_END:
|
|
441
|
+
all_referencing_tables_ready = False
|
|
442
|
+
break # referencing table not TEND status
|
|
443
|
+
if not all_referencing_tables_ready:
|
|
444
|
+
logger.debug(f"Table {owner}.{table_name} has to wait for referencing tables to finish...")
|
|
445
|
+
continue
|
|
446
|
+
return (owner, table_name, table_cnf) # Found a table ready to process
|
|
447
|
+
return None # No tables ready to process
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def ldb_exec_ilm(
|
|
451
|
+
config: Config,
|
|
452
|
+
tables_config: TablesConfig,
|
|
453
|
+
process_date: str,
|
|
454
|
+
engine: DatabaseEngine,
|
|
455
|
+
connection: Any,
|
|
456
|
+
) -> int:
|
|
457
|
+
processes: list[Future[WorkerResult]] = []
|
|
458
|
+
process_targets: dict[Future[WorkerResult], tuple[str, str]] = {}
|
|
459
|
+
active_tables: set[tuple[str, str]] = set()
|
|
460
|
+
with ProcessPoolExecutor(
|
|
461
|
+
max_workers=config.execution.parallel_max,
|
|
462
|
+
initializer=_initialize_worker_logger,
|
|
463
|
+
initargs=(config.execution.log_level,),
|
|
464
|
+
) as executor:
|
|
465
|
+
process_launched = False
|
|
466
|
+
while True:
|
|
467
|
+
# While there is space in the pool, try to launch new processes
|
|
468
|
+
while len(processes) < config.execution.parallel_max:
|
|
469
|
+
cycle_launched = False
|
|
470
|
+
# Search for a process to launch
|
|
471
|
+
next_ready_table = get_next_ready_table(tables_config, active_tables)
|
|
472
|
+
if next_ready_table:
|
|
473
|
+
owner, table_name, table_info = next_ready_table
|
|
474
|
+
logger.info(f"Launching background process for {owner}.{table_name}...")
|
|
475
|
+
future = executor.submit(
|
|
476
|
+
process_table, config, owner, table_name, table_info["sql_block"], process_date
|
|
477
|
+
)
|
|
478
|
+
processes.append(future)
|
|
479
|
+
process_targets[future] = (owner, table_name)
|
|
480
|
+
active_tables.add((owner, table_name))
|
|
481
|
+
table_info["conds"][0]["ctl_status"] = Status.TABLE_START
|
|
482
|
+
cycle_launched = True
|
|
483
|
+
process_launched = True
|
|
484
|
+
if not cycle_launched:
|
|
485
|
+
break # There are no processes to launch, so go to waiting some process to end
|
|
486
|
+
logger.debug(f"Active processes: {len(processes)}")
|
|
487
|
+
if processes:
|
|
488
|
+
# Wait for some process to finish
|
|
489
|
+
for completed_future in as_completed(processes):
|
|
490
|
+
owner, table_name = process_targets[completed_future]
|
|
491
|
+
try:
|
|
492
|
+
owner, table_name, status, prev_rows_processed, rows_processed, sqlcode, message = (
|
|
493
|
+
completed_future.result()
|
|
494
|
+
)
|
|
495
|
+
except Exception as exc:
|
|
496
|
+
sqlcode, detail = _exception_details(exc)
|
|
497
|
+
status = Status.ERROR
|
|
498
|
+
prev_rows_processed = rows_processed = 0
|
|
499
|
+
message = f"Worker process failed: {detail}"
|
|
500
|
+
logger.error("Worker process failed for %s.%s", owner, table_name, exc_info=True)
|
|
501
|
+
logger.debug(f"Process for table {owner}.{table_name} completed with status {status}")
|
|
502
|
+
cdr = tables_config[(owner, table_name)]["conds"][0]
|
|
503
|
+
if status == Status.ERROR and rows_processed == prev_rows_processed:
|
|
504
|
+
status = Status.SKIPPED
|
|
505
|
+
logger.info(
|
|
506
|
+
f"Table {owner}.{table_name} finished with status {status} and {rows_processed - prev_rows_processed} rows processed."
|
|
507
|
+
)
|
|
508
|
+
cdr["ctl_status"] = status
|
|
509
|
+
if status == Status.ERROR:
|
|
510
|
+
logger.error(f" Error {sqlcode}: {message}")
|
|
511
|
+
elif status == Status.SKIPPED:
|
|
512
|
+
logger.error(f" Error {sqlcode}: {message}")
|
|
513
|
+
active_tables.remove((owner, table_name))
|
|
514
|
+
processes.remove(completed_future)
|
|
515
|
+
del process_targets[completed_future]
|
|
516
|
+
break
|
|
517
|
+
else:
|
|
518
|
+
break
|
|
519
|
+
if engine.all_status_tend(connection, tables_config, process_date):
|
|
520
|
+
if process_launched:
|
|
521
|
+
logger.info("All tables processed successfully.")
|
|
522
|
+
else:
|
|
523
|
+
logger.info("No tables to process.")
|
|
524
|
+
return 0
|
|
525
|
+
else:
|
|
526
|
+
logger.error("Some tables did not finish successfully.")
|
|
527
|
+
return 1
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def process_tables_cnf(connection: Any, config: Config, engine: DatabaseEngine, process_date: str) -> TablesConfig:
|
|
531
|
+
logger.info("Processing table configuration...")
|
|
532
|
+
ldb_ctl_status_rows: list[dict[str, Any]] = []
|
|
533
|
+
ldb_conf_rows = _load_offline_rows(config)
|
|
534
|
+
if not config.execution.generate_script:
|
|
535
|
+
ldb_ctl_status_rows = engine.get_status(connection, process_date)
|
|
536
|
+
tables_config: TablesConfig = {}
|
|
537
|
+
# Populate tables_config with the raw rows and derive referencing tables from cnf_referencing_tables.
|
|
538
|
+
is_source_mode = config.execution.action == "SOURCE_ILM"
|
|
539
|
+
ldb_process_date_expr, ldb_insert_date_expr = engine.get_ldb_columns_expressions()
|
|
540
|
+
for ldb_cnf_row in ldb_conf_rows:
|
|
541
|
+
ldb_cnf_row["source_owner"] = engine.get_identifier_str(ldb_cnf_row["source_owner"])
|
|
542
|
+
ldb_cnf_row["table_name"] = engine.get_identifier_str(ldb_cnf_row["table_name"])
|
|
543
|
+
ldb_cnf_row["history_owner"] = engine.get_identifier_str(ldb_cnf_row["history_owner"])
|
|
544
|
+
key = (ldb_cnf_row["source_owner"], ldb_cnf_row["table_name"])
|
|
545
|
+
if key not in tables_config:
|
|
546
|
+
tables_config[key] = {"conds": [], "referencing_tables": []}
|
|
547
|
+
tables_config[key]["conds"].append(ldb_cnf_row)
|
|
548
|
+
for ldb_cnf_row in ldb_conf_rows:
|
|
549
|
+
key = (ldb_cnf_row["source_owner"], ldb_cnf_row["table_name"])
|
|
550
|
+
declared_references = ldb_cnf_row["referencing_tables"]
|
|
551
|
+
if declared_references:
|
|
552
|
+
for ref_table in declared_references.split(","):
|
|
553
|
+
ref_table = ref_table.strip()
|
|
554
|
+
if "." in ref_table:
|
|
555
|
+
ref_owner, ref_table = ref_table.split(".")
|
|
556
|
+
else:
|
|
557
|
+
ref_owner = ldb_cnf_row["source_owner"]
|
|
558
|
+
ref_table, alias = ref_table.split(" ")
|
|
559
|
+
ref_owner = engine.get_identifier_str(ref_owner)
|
|
560
|
+
ref_table = engine.get_identifier_str(ref_table)
|
|
561
|
+
ref_key = (ref_owner, ref_table)
|
|
562
|
+
if ref_key not in tables_config:
|
|
563
|
+
raise ValueError(f"Table {ref_owner}.{ref_table} not found in ldb_conf_rows")
|
|
564
|
+
tables_config[ref_key]["referencing_tables"].append(key)
|
|
565
|
+
if not config.execution.generate_script:
|
|
566
|
+
# Inject ctl_status values loaded from the database when resuming a run.
|
|
567
|
+
for ldb_ctl_status_row in ldb_ctl_status_rows:
|
|
568
|
+
key = (ldb_ctl_status_row["ctl_owner"], ldb_ctl_status_row["ctl_table_name"])
|
|
569
|
+
if key in tables_config:
|
|
570
|
+
for cond in tables_config[key]["conds"]:
|
|
571
|
+
cond["ctl_status"] = ldb_ctl_status_row["ctl_status"]
|
|
572
|
+
# Collect each table's active conditions along with inherited referencing-table conditions.
|
|
573
|
+
# Track every expression needed to build the WHERE clause for the combined predicate across all contributing conditions.
|
|
574
|
+
# Persist the resulting predicate in cond_expr so later stages can reuse it.
|
|
575
|
+
for key, table_cnf in tables_config.items():
|
|
576
|
+
cnd0 = table_cnf["conds"][0]
|
|
577
|
+
_validate_orphan_configuration(config, cnd0, key)
|
|
578
|
+
runtime_owner = _runtime_owner(config, cnd0)
|
|
579
|
+
inherited_references = cnd0["referencing_tables"]
|
|
580
|
+
added_conds = [("A", cond) for cond in table_cnf["conds"]]
|
|
581
|
+
orphan_history_date_predicates: list[str] = []
|
|
582
|
+
table_cnf["table_columns"], table_cnf["columns_metadata"] = engine.get_table_columns(
|
|
583
|
+
connection, runtime_owner, cnd0["table_name"]
|
|
584
|
+
)
|
|
585
|
+
if inherited_references:
|
|
586
|
+
for ref_table in inherited_references.split(","):
|
|
587
|
+
ref_table = ref_table.strip()
|
|
588
|
+
if "." in ref_table:
|
|
589
|
+
ref_owner, ref_table_name = ref_table.split(".")
|
|
590
|
+
else:
|
|
591
|
+
ref_owner, ref_table_name = ldb_cnf_row["source_owner"], ref_table
|
|
592
|
+
ref_table_name, alias = ref_table_name.split(" ")
|
|
593
|
+
ref_owner = engine.get_identifier_str(ref_owner)
|
|
594
|
+
ref_table_name = engine.get_identifier_str(ref_table_name)
|
|
595
|
+
if (ref_owner, ref_table_name) not in tables_config:
|
|
596
|
+
raise ValueError(f"Table {ref_owner}.{ref_table} not found in ldb_conf_rows")
|
|
597
|
+
ref_conds = tables_config[(ref_owner, ref_table_name)]["conds"]
|
|
598
|
+
added_conds.extend([(alias, cond) for cond in ref_conds])
|
|
599
|
+
# Build the combined WHERE expression from all contributing conditions.
|
|
600
|
+
for al, cd in added_conds:
|
|
601
|
+
cond_list: list[str] = []
|
|
602
|
+
purge_date_expr = nvl(cd["purge_date_expr"], "")
|
|
603
|
+
# Prefer the history-only filter during HISTORY_ILM runs; otherwise use the source filter.
|
|
604
|
+
additional_filter_expr = " ".join(nvl(cd["additional_filter_expr"], "").splitlines())
|
|
605
|
+
history_addtl_filter_expr = " ".join(
|
|
606
|
+
nvl(cd["history_addtl_filter_expr"], additional_filter_expr).splitlines()
|
|
607
|
+
)
|
|
608
|
+
addtl_expr = additional_filter_expr if is_source_mode else history_addtl_filter_expr
|
|
609
|
+
retain_months_source, retain_months_history = cd["retain_months_source"], cd["retain_months_history"]
|
|
610
|
+
if retain_months_source is not None and retain_months_history is not None:
|
|
611
|
+
retain_months_history += retain_months_source
|
|
612
|
+
retain_months = retain_months_source if is_source_mode else retain_months_history
|
|
613
|
+
if purge_date_expr and not retain_months_source:
|
|
614
|
+
raise ValueError(f"retain_months_source is required when cnf_purge_date_expr is set for {key}")
|
|
615
|
+
if retain_months_source and not purge_date_expr:
|
|
616
|
+
raise ValueError(f"purge_date_expr is required when cnf_retain_months_source is set for {key}")
|
|
617
|
+
if retain_months_history and not retain_months_source:
|
|
618
|
+
raise ValueError(f"retain_months_history is required when cnf_retain_months_source is set for {key}")
|
|
619
|
+
alias_prefix = al + "."
|
|
620
|
+
mod_alexp_uac = alias_prefix if is_source_mode or not config.execution.use_added_columns else "A."
|
|
621
|
+
if addtl_expr:
|
|
622
|
+
if not is_source_mode and config.execution.use_added_columns and al != "A":
|
|
623
|
+
related_identifiers = engine.get_identifiers_from_expression(history_addtl_filter_expr)
|
|
624
|
+
persisted_identifiers = {identifier: f"{identifier}_{al}" for identifier in related_identifiers}
|
|
625
|
+
cond_list.append(
|
|
626
|
+
engine.rewrite_expression_identifiers(
|
|
627
|
+
history_addtl_filter_expr,
|
|
628
|
+
persisted_identifiers,
|
|
629
|
+
qualifier="A",
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
else:
|
|
633
|
+
cond_list.append(addtl_expr.replace("@", mod_alexp_uac))
|
|
634
|
+
if purge_date_expr:
|
|
635
|
+
if is_source_mode or not config.execution.use_added_columns or al == "A":
|
|
636
|
+
cond_list.append(
|
|
637
|
+
engine.get_date_condition(purge_date_expr.replace("@", alias_prefix), cast(int, retain_months))
|
|
638
|
+
)
|
|
639
|
+
else:
|
|
640
|
+
relationship_date_predicate = engine.get_date_condition(
|
|
641
|
+
f"A.{_relationship_date_column(al)}", cast(int, retain_months_history)
|
|
642
|
+
)
|
|
643
|
+
cond_list.append(relationship_date_predicate)
|
|
644
|
+
orphan_history_date_predicates.append(relationship_date_predicate)
|
|
645
|
+
cd["cond_expr"] = " and ".join(cond_list)
|
|
646
|
+
# get the maximum cnf_retain_months_history from all added_conds
|
|
647
|
+
table_cnf["months_keep_history_max"] = max_ignore_none([cd["retain_months_history"] for _, cd in added_conds])
|
|
648
|
+
if cnd0["source_orphan_purge"] == "Y" and nvl(table_cnf["months_keep_history_max"], 0) <= 0:
|
|
649
|
+
raise ValueError(f"positive retain_months_history required when source_orphan_purge is enabled for {key}")
|
|
650
|
+
# if HISTORY_ILM and cnf_months_keep_history_max is None, skip the table
|
|
651
|
+
if not is_source_mode and table_cnf["months_keep_history_max"] is None:
|
|
652
|
+
table_cnf["skip"] = True
|
|
653
|
+
continue
|
|
654
|
+
table_cnf["skip"] = False
|
|
655
|
+
# get where expression from cond_expr of all added_conds joined by " or "
|
|
656
|
+
if config.execution.action == "SOURCE_ILM" and not any(cd["cond_expr"] for _, cd in added_conds):
|
|
657
|
+
raise ValueError(f"At least one condition must be specified for SOURCE_ILM on table {key}")
|
|
658
|
+
predicates = [f"({cd['cond_expr']})" for _, cd in added_conds if cd["cond_expr"]]
|
|
659
|
+
if is_source_mode:
|
|
660
|
+
orphan_predicate = _orphan_predicate(cnd0)
|
|
661
|
+
if orphan_predicate:
|
|
662
|
+
predicates.append(f"({orphan_predicate})")
|
|
663
|
+
elif cnd0["source_orphan_purge"] == "Y":
|
|
664
|
+
if not orphan_history_date_predicates:
|
|
665
|
+
raise ValueError(f"related retention date required when source_orphan_purge is enabled for {key}")
|
|
666
|
+
orphan_dates = "\n or ".join(f"({predicate})" for predicate in orphan_history_date_predicates)
|
|
667
|
+
predicates.append(f"(A.{_ORPHAN_MARKER_COLUMN} = 'Y' and ({orphan_dates}))")
|
|
668
|
+
where_expr = "\n or ".join(predicates)
|
|
669
|
+
other_columns: list[_OtherColumn] = []
|
|
670
|
+
table_cnf["other_columns"] = other_columns
|
|
671
|
+
if config.execution.use_added_columns and is_source_mode:
|
|
672
|
+
for al, cd in added_conds:
|
|
673
|
+
columns_metadata = tables_config[(cd["source_owner"], cd["table_name"])]["columns_metadata"]
|
|
674
|
+
if al != "A":
|
|
675
|
+
# Add the cnf_purge_date_expr to other_columns for referencing tables
|
|
676
|
+
if cd["purge_date_expr"]:
|
|
677
|
+
relationship_date_column = _relationship_date_column(al)
|
|
678
|
+
column_metadata = ColumnDefinition(
|
|
679
|
+
name=relationship_date_column, data_type="date", nullable=True
|
|
680
|
+
)
|
|
681
|
+
_append_unique_name(
|
|
682
|
+
other_columns,
|
|
683
|
+
{
|
|
684
|
+
"name": relationship_date_column,
|
|
685
|
+
"expr": engine.get_fallback_expression(
|
|
686
|
+
nvl(cd["purge_date_expr"], "").replace("@", al + "."),
|
|
687
|
+
ldb_process_date_expr,
|
|
688
|
+
)
|
|
689
|
+
if cnd0["source_orphan_purge"] == "Y"
|
|
690
|
+
else nvl(cd["purge_date_expr"], "").replace("@", al + "."),
|
|
691
|
+
"metadata": column_metadata,
|
|
692
|
+
},
|
|
693
|
+
)
|
|
694
|
+
# Add columns in cnf_history_addtl_filter_expr to other_cols_exprs and other_cols_alias for referencing tables
|
|
695
|
+
history_filter = " ".join(
|
|
696
|
+
nvl(cd["history_addtl_filter_expr"], cd["additional_filter_expr"] or "").splitlines()
|
|
697
|
+
)
|
|
698
|
+
for column_name in engine.get_identifiers_from_expression(history_filter):
|
|
699
|
+
column_metadata = _clone_column_definition(f"{column_name}_{al}", columns_metadata[column_name])
|
|
700
|
+
other_column: _OtherColumn = {
|
|
701
|
+
"name": f"{column_name}_{al}",
|
|
702
|
+
"expr": f"{al}.{column_name}",
|
|
703
|
+
"metadata": column_metadata,
|
|
704
|
+
}
|
|
705
|
+
_append_unique_name(other_columns, other_column)
|
|
706
|
+
orphan_predicate = _orphan_predicate(cnd0)
|
|
707
|
+
if orphan_predicate:
|
|
708
|
+
other_columns.append(
|
|
709
|
+
{
|
|
710
|
+
"name": _ORPHAN_MARKER_COLUMN,
|
|
711
|
+
"expr": f"case when {orphan_predicate} then 'Y' else 'N' end",
|
|
712
|
+
"metadata": ColumnDefinition(
|
|
713
|
+
name=_ORPHAN_MARKER_COLUMN,
|
|
714
|
+
data_type="string",
|
|
715
|
+
length=1,
|
|
716
|
+
nullable=True,
|
|
717
|
+
),
|
|
718
|
+
}
|
|
719
|
+
)
|
|
720
|
+
if config.execution.add_ldb_columns and is_source_mode:
|
|
721
|
+
other_columns.append(
|
|
722
|
+
{
|
|
723
|
+
"name": "LDB_PROCESS_DATE",
|
|
724
|
+
"expr": ldb_process_date_expr,
|
|
725
|
+
"metadata": ColumnDefinition(name="LDB_PROCESS_DATE", data_type="date", nullable=False),
|
|
726
|
+
}
|
|
727
|
+
)
|
|
728
|
+
other_columns.append(
|
|
729
|
+
{
|
|
730
|
+
"name": "LDB_INSERT_DATE",
|
|
731
|
+
"expr": ldb_insert_date_expr,
|
|
732
|
+
"metadata": ColumnDefinition(name="LDB_INSERT_DATE", data_type="date", nullable=False),
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
_, cnf_table_name = key
|
|
736
|
+
join_expr = _runtime_join_expression(config, cnd0)
|
|
737
|
+
# Prepare query_expr with the cnf_table_name, join_expr and where_expr
|
|
738
|
+
base_from = f"from {runtime_owner.lower()}.{cnf_table_name.lower()} A"
|
|
739
|
+
join_clause = f"\n {join_expr}" if join_expr else ""
|
|
740
|
+
where_clause = f"\nwhere {where_expr}" if where_expr else ""
|
|
741
|
+
table_cnf["query_expr"] = base_from + join_clause + where_clause
|
|
742
|
+
table_columns = table_cnf["table_columns"]
|
|
743
|
+
# if table has lob columns and long type columns, remove long type columns from table_columns
|
|
744
|
+
if cnd0["long_columns"] and cnd0["has_lob_columns"] != "N":
|
|
745
|
+
long_columns = [c.strip().lower() for c in cnd0["long_columns"].split(",")]
|
|
746
|
+
table_columns = list(set([col.lower() for col in table_columns]) - set(long_columns))
|
|
747
|
+
table_cnf["table_columns"] = table_columns
|
|
748
|
+
# get sql_block and add it to tables_config
|
|
749
|
+
sql_block = engine.generate_sql_block(config, table_cnf, process_date)
|
|
750
|
+
table_cnf["sql_block"] = sql_block
|
|
751
|
+
return tables_config
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def ldb_run(config: Config) -> None:
|
|
755
|
+
connection: Any = None
|
|
756
|
+
admin_connection: Any = None
|
|
757
|
+
engine: DatabaseEngine | None = None
|
|
758
|
+
try:
|
|
759
|
+
engine = get_db_engine(config.connections.db_engine)
|
|
760
|
+
if config.execution.mode == "PLAN":
|
|
761
|
+
rc = _plan_mode(config)
|
|
762
|
+
raise SystemExit(rc)
|
|
763
|
+
if config.execution.mode == "VALIDATE":
|
|
764
|
+
rc = _validate_environment(config, engine)
|
|
765
|
+
raise SystemExit(rc)
|
|
766
|
+
connection = engine.get_connection(config)
|
|
767
|
+
table_privileges = ("SELECT", "INSERT", "UPDATE", "DELETE")
|
|
768
|
+
tables_conf_rows = _load_offline_rows(config)
|
|
769
|
+
source_tables_for_privileges = _collect_privilege_targets(tables_conf_rows, "source_owner", engine)
|
|
770
|
+
if (
|
|
771
|
+
config.execution.action == "SOURCE_ILM"
|
|
772
|
+
and source_tables_for_privileges
|
|
773
|
+
and _credentials_present(config, admin=True, env="SOURCE")
|
|
774
|
+
):
|
|
775
|
+
source_admin_priv_conn = engine.get_connection(config, admin=True, env="SOURCE")
|
|
776
|
+
try:
|
|
777
|
+
engine.ensure_table_privileges(
|
|
778
|
+
source_admin_priv_conn,
|
|
779
|
+
config.administration.source_role_name,
|
|
780
|
+
source_tables_for_privileges,
|
|
781
|
+
table_privileges,
|
|
782
|
+
)
|
|
783
|
+
finally:
|
|
784
|
+
engine.close_connection(source_admin_priv_conn)
|
|
785
|
+
process_date = engine.get_system_date(connection).strftime("%Y%m%d")
|
|
786
|
+
tables_config = process_tables_cnf(connection, config, engine, process_date)
|
|
787
|
+
if config.execution.generate_script:
|
|
788
|
+
rc = generate_script_output(config, tables_config)
|
|
789
|
+
else:
|
|
790
|
+
admin_connection = engine.get_connection(config, admin=True)
|
|
791
|
+
_ensure_history_tables(config, engine, admin_connection, tables_config)
|
|
792
|
+
history_tables_for_privileges = _collect_privilege_targets(tables_conf_rows, "history_owner", engine)
|
|
793
|
+
if history_tables_for_privileges and _credentials_present(config, admin=True, env="HISTORY"):
|
|
794
|
+
history_admin_priv_conn = engine.get_connection(config, admin=True, env="HISTORY")
|
|
795
|
+
try:
|
|
796
|
+
engine.ensure_table_privileges(
|
|
797
|
+
history_admin_priv_conn,
|
|
798
|
+
config.administration.history_role_name,
|
|
799
|
+
history_tables_for_privileges,
|
|
800
|
+
table_privileges,
|
|
801
|
+
)
|
|
802
|
+
finally:
|
|
803
|
+
engine.close_connection(history_admin_priv_conn)
|
|
804
|
+
rc = ldb_exec_ilm(config, tables_config, process_date, engine, connection)
|
|
805
|
+
raise SystemExit(rc)
|
|
806
|
+
finally:
|
|
807
|
+
if engine and connection:
|
|
808
|
+
engine.close_connection(connection)
|
|
809
|
+
if engine and admin_connection:
|
|
810
|
+
engine.close_connection(admin_connection)
|