Skip to content

tdfs4ds.dependencies

tdfs4ds.dependencies

What depends on what in a feature store, and how to remove objects in order.

A dataset reads feature values of one process (FEATURE_VERSION = <PROCESS_ID> in its view), and a process view can read a dataset or another process's view. Removing a process, a feature or a dataset that other objects still read leaves them broken or frozen without a word: the dataset keeps returning the values the removed process last wrote, and a lineage rebuild then drops the link.

Three layers, as in the workflow assessment:

  • :func:collect_dependency_inputs - the only I/O (catalogs, persisted lineage, dataset view DDL, existence in DBC), fail-open: a failed read is recorded in unverified, never read as "nothing depends on it";
  • :func:build_dependency_graph, :func:dependents, :func:plan_removal_from and :func:lineage_gaps - pure;
  • :func:generate_removal_script / :func:write_removal_script - a runnable .py that is a dry run unless started with --apply.

:func:check_removal is the guardrail remove_process, remove_feature and remove_dataset call before deleting anything. No language model is involved.

PRODUCES module-attribute

PRODUCES = 'produces'

READS_FEATURE module-attribute

READS_FEATURE = 'reads_feature'

READS_VERSION module-attribute

READS_VERSION = 'reads_version'

READS_VIEW module-attribute

READS_VIEW = 'reads_view'

KIND_ORDER module-attribute

KIND_ORDER = {'attached': 0, 'dataset': 1, 'process': 2, 'feature': 3}

_PAIR_RE module-attribute

_PAIR_RE = compile("FEATURE_ID\\s*=\\s*'?\\s*(\\d+)\\s*'?\\s*AND\\s+FEATURE_VERSION\\s*=\\s*'([^']+)'", IGNORECASE)

_TEMPLATE module-attribute

_TEMPLATE = '#!/usr/bin/env python\n"""Removal script generated by tdfs4ds {version} on {generated_at}.\n\nRemoves, in dependency order (the objects that read others first):\n{summary}\n\nRun it as is for a DRY RUN: each step says what it would remove, nothing is\nchanged. Run it with --apply to remove. It stops at the first failed step and\nlists the steps left; when every step ran, it checks that nothing is left behind\n(catalog rows, documentation, provenance, SQL versions, vector-index entries)\nand exits with code 2 otherwise. Stored values are {values}.\n\nConnection: TDFS4DS_TD_HOST / USERNAME / PASSWORD (or LOGDATA) in the\nenvironment or a .env file, as tdfs4ds.reconnect_teradata() reads them.\n"""\nimport json\nimport sys\n\nPLAN = json.loads({plan_literal})\n\n\ndef main():\n    apply = "--apply" in sys.argv[1:]\n    import tdfs4ds\n    from tdfs4ds.utils.reconnect import reconnect_teradata\n    if not reconnect_teradata():\n        sys.exit("reconnect_teradata() failed: set TDFS4DS_TD_HOST / USERNAME / PASSWORD.")\n    if PLAN.get("schema"):\n        tdfs4ds.SCHEMA = PLAN["schema"]\n    if PLAN.get("schema_view"):\n        tdfs4ds.SCHEMA_VIEW = PLAN["schema_view"]\n    from tdfs4ds.dependencies import execute_step, preview_step\n    steps = PLAN["steps"]\n    print(("APPLY" if apply else "DRY RUN") + f": {{len(steps)}} step(s)")\n    for i, step in enumerate(steps, 1):\n        print(f"[{{i}}/{{len(steps)}}] {{step[\'action\']}} {{step[\'object\']}}  <- {{step[\'reason\']}}")\n        if not apply:\n            print("        would remove: " + preview_step(step))\n            continue\n        try:\n            execute_step(step)\n            print("        done")\n        except Exception as exc:\n            print(f"        FAILED: {{exc}}")\n            left = steps[i:]\n            if left:\n                print("Steps not run:")\n                for s in left:\n                    print(f"  - {{s[\'action\']}} {{s[\'object\']}}")\n            sys.exit(1)\n    if not apply:\n        print("Nothing was changed. Run again with --apply to remove.")\n        return\n    from tdfs4ds.dependencies import verify_removal\n    print("Checking what is left (catalogs, documentation, vector index)...")\n    check = verify_removal(PLAN)\n    for s in check["steps"]:\n        print(("  ok    " if not s["left"] else "  LEFT  ") + s["action"] + " " + s["object"]\n              + ("" if not s["left"] else ": " + "; ".join(s["left"])))\n    if not check["clean"]:\n        print("Some objects left documentation or index entries behind (listed above).")\n        sys.exit(2)\n    print("Clean: nothing left behind.")\n\n\nif __name__ == "__main__":\n    try:\n        main()\n    finally:\n        try:                       # end the session: an open one keeps a slot on the PE\n            import teradataml as tdml\n            tdml.remove_context()\n        except Exception:\n            pass\n'

DependentObjectsError

Bases: ValueError

A removal refused because other objects depend on the object, or because what depends on it could not be read.

dependents: [{"kind", "label", "reason"}] in removal order; unverified: the reads that failed; plan: a :func:plan_removal result that removes the object and every dependent, in order.

dependents instance-attribute

dependents = list(dependents or [])

unverified instance-attribute

unverified = list(unverified or [])

plan instance-attribute

plan = plan

__init__

__init__(message, dependents=None, unverified=None, plan=None)

logger_safe

logger_safe(level, message, *args, **kwargs)

Wrapper around the global logger that only emits logs when tdfs4ds.DISPLAY_LOGS is True. level is a string like "info", "error", etc.

qualified

qualified(database, object_name, force=False)

Build db.obj with each part quoted only as needed.

qualified('TDB', 'FS_X') == 'TDB.FS_X' (unchanged for ordinary names); qualified('SVC@CORP', 'FS_X') == '"SVC@CORP".FS_X'. An empty/None database yields the bare object name, so unqualified callers keep working.

force=True quotes both parts unconditionally — only for the sites that have always emitted quotes and whose exact text is already persisted (dataset view DDL, returned volatile-table names).

split_qualified

split_qualified(name)

Split db.obj into (database, object), both de-quoted.

Quote-aware, so a dot inside a quoted part does not split the name: split_qualified('"DEINF.CARLOSN"."FS_X"') == ('DEINF.CARLOSN', 'FS_X'). The naive rsplit('.', 1) this replaces returns ('"DEINF.CARLOSN"', '"FS_X"') only by luck and ('"DEINF', 'CARLOSN"."FS_X"') in the split('.') variant.

An unqualified name returns ('', name).

sql_literal

sql_literal(value)

Escape value for use inside a single-quoted SQL string literal.

For the DBC probes and catalog predicates that compare a name as data (WHERE DatabaseName = '…'). The caller supplies the surrounding quotes.

_u

_u(v) -> str

_clean

_clean(v) -> Optional[str]

_fid

_fid(v) -> Optional[int]

_short

_short(name) -> str

_err

_err(exc) -> str

_node_id

_node_id(name) -> str

'DB.OBJECT' as the persisted lineage graph spells it.

P

P(pid) -> str

D

D(dsid) -> str

A

A(name) -> str

An attached view (genai/attached_objects.py, 0.3.5.2), keyed by its 'DB.OBJECT' node id.

F

F(domain, entity, name) -> str

dataset_reads

dataset_reads(ddl: str) -> List[Tuple[int, str, Optional[str], Optional[str]]]

[(FEATURE_ID, FEATURE_VERSION, column, storage database)] a dataset view reads. Pure.

The DDL is parsed with :func:tdfs4ds.dataset.ddl_spec.parse_dataset_ddl; when it is not a view tdfs4ds can read back, the FEATURE_ID = n AND FEATURE_VERSION = '<pid>' predicates are read directly (no column name, no database). The database tells which store the ids belong to: FEATURE_IDs and PROCESS_IDs are those of the store owning the storage table.

_dataset_table_info

_dataset_table_info(ddl: str)

(reads, current): the 6-tuples of :func:dataset_table_reads and whether the view reads the current valid time (None when the view cannot be parsed). Pure.

dataset_table_reads

dataset_table_reads(ddl: str) -> List[Tuple[int, str, Optional[str], Optional[str], Optional[str], Optional[str]]]

[(FEATURE_ID, FEATURE_VERSION, column, storage database, storage table, join type)] a dataset view reads, from the parsed spec. Pure.

:func:dataset_reads keeps its 4-tuples for the callers that unpack them. With the regex fallback (a view tdfs4ds cannot read back) the table and the join type are None; join type is 'INNER' or 'FULL OUTER'.

_rows

_rows(sql)

_read_ddl

_read_ddl(db, name) -> str

read_existing

read_existing(databases: Iterable[str]) -> Optional[set]

{'DB.OBJECT'} of the tables and views of databases, or None when DBC could not be read. One read.

collect_dependency_inputs

collect_dependency_inputs(*, snapshot=None, graph=None, dataset_ids=None, max_ddl: int = 300, existence: bool = False, read_graph: bool = True, emit=None) -> dict

Everything the pure layer needs.

snapshot / graph: already-read catalogs (store_consistency. collect_catalog_snapshot) and lineage graph, to avoid reading them twice. dataset_ids: read the view DDL of these datasets only (None = every registered dataset, at most max_ddl). existence: also read which views and tables exist (DBC), for the lineage-gap checks.

_entity_list

_entity_list(entity) -> List[str]

_walk_inputs

_walk_inputs(G, node, process_nodes: dict, dataset_nodes: dict)

Objects a process view reads: the registered processes and datasets it reaches through plain views (stopping there), and every object on the way. Returns (processes, datasets, unregistered_datasets, visited).

build_dependency_graph

build_dependency_graph(inputs: dict)

The dependency DiGraph (upstream -> dependent), pure.

Nodes P:<pid>, D:<dataset id>, F:<domain>|<entity>|<name> (or F#<id> for a FEATURE_ID absent from the catalog) carry kind, label and missing (referenced but not registered: a gap). The graph also keeps, in graph.graph, the inputs of each process view (inputs), the datasets whose view was not read (reads_unknown) and the existence set.

_out

_out(H, node, scope_pid=None)

The dependents one step from node that its removal breaks.

_reason

_reason(H, up, down, data) -> str

dependents

dependents(H, targets) -> dict

What removing targets breaks, pure.

targets: node keys, or (node key, process_id) for a feature removed as produced by one process (nothing breaks when another process computes it). Returns {"order": [node keys, dependents first, targets last], "dependents": [{"key", "kind", "label", "reason", "missing"}], "lineage_needed": bool} (a dataset or process was reached while the lineage graph is unknown: its readers could not be listed).

resolve_objects

resolve_objects(H, objects) -> Tuple[list, list]

(targets, unknown). An object is a PROCESS_ID or a process view name, a DATASET_ID or a dataset view name (db.name or bare), or a feature as a tuple (name, entity, data_domain[, process_id]) or a dict with keys feature, entity, data_domain, process_id.

_purge_sql

_purge_sql(db, table, fid, version=None) -> str

plan_removal_from

plan_removal_from(H, targets, *, purge_values: bool = False, drop_views: bool = False, unknown=(), unverified=()) -> dict

The ordered removal steps for targets and everything that depends on them, from a dependency graph. Pure; nothing is executed.

_candidate_datasets

_candidate_datasets(H_inputs: dict, pids: set, fids: set) -> set

Datasets that may read the given processes' values or features: their feature list (no view read needed) names one of those FEATURE_IDs.

plan_removal

plan_removal(objects, *, purge_values: bool = False, drop_views: bool = False, inputs: dict = None, max_rounds: int = 5) -> dict

The removal steps for objects and every object depending on them, in order.

objects: process ids or process view names, dataset ids or names, and features as (name, entity, data_domain[, process_id]). Nothing is executed: pass the result to :func:write_removal_script, or read plan["steps"]. purge_values=True adds the deletion of the removed versions' stored values (irreversible); drop_views=True drops the views.

Only the dataset views that may be concerned are read (their feature list names a feature of a removed process), round after round as the plan grows.

_target_line

_target_line(kind, ref, process_id=None) -> str

check_removal

check_removal(kind: str, ref, *, process_id=None, plan_fn=None) -> dict

Refuse a removal that would break other objects. Called by remove_process / remove_feature / remove_dataset unless force=True.

kind: 'process' (ref = PROCESS_ID), 'dataset' (ref = DATASET_ID or name) or 'feature' (ref = (name, entity, data_domain)). Returns the plan when nothing depends on the object; raises :class:DependentObjectsError when something does, or when what depends on it could not be read.

execute_step

execute_step(step: dict, origin: str = 'script')

Run one plan step. Called by the generated script with --apply only; the removal log records it with origin.

_execute_step

_execute_step(step: dict)

_count

_count(sql) -> Optional[int]

_in_ids

_in_ids(ids) -> str

_bd

_bd(db, obj, column=None) -> Optional[int]

Current business-dictionary rows of an object (description, sections, columns), or of one of its columns.

_provenance

_provenance(db, obj, column=None) -> Optional[int]

index_entries

index_entries(db, obj, columns=None) -> dict

The vector-index entries of an object (or of some of its columns).

Chroma: {"backend": "chroma", "entries": n}, counted by id. Teradata Vector Store: entries cannot be counted (embeddings are keyed by row position) and follow the documentation at each refresh, so the state of the collections is reported: {"backend": "teradata", "failed": [collections not ready]}.

footprint

footprint(step: dict) -> dict

What one step removes, counted now (read only): catalog rows, documentation (business dictionary, process documentation, provenance), stored SQL versions, vector-index entries, stored values. Before a removal it is what will go (the dry run); after it, everything must be 0 (:func:verify_removal). A count that could not be read is None.

_left

_left(fp: dict) -> List[str]

What a footprint still holds: names with a count above 0, an index not clean, or a count that could not be read.

_describe

_describe(fp: dict) -> str

preview_step

preview_step(step: dict) -> str

What one step would remove, read only (counts and existence).

verify_removal

verify_removal(plan: dict) -> dict

After a removal, check that every step left nothing behind: catalog rows, documentation (business dictionary, process documentation, provenance), stored SQL versions, vector-index entries and, with purge_values, stored values. Read only.

Returns {"clean": bool, "steps": [{"action", "object", "left": [...]}]}. A step whose feature catalog entry stays for another process is checked for this process's part only. Values kept on purpose (no purge_values) are not counted.

_ascii

_ascii(text: str) -> str

generate_removal_script

generate_removal_script(plan: dict, *, out_path: Optional[str] = None) -> str

A runnable .py for a :func:plan_removal result: a dry run unless started with --apply. Pure ASCII. Refuses a plan with no step.

_write

_write(script: str, out_path: str) -> str

write_removal_script

write_removal_script(plan: dict, out_path: Optional[str] = None) -> str

Write :func:generate_removal_script to out_path (default {tdfs4ds.SCHEDULING_SCRIPT_DIR}/tdfs4ds_removal_<ts>.py); return the path.

lineage_gaps

lineage_gaps(H, *, explain=None, max_explain: int = 30) -> List[dict]

Holes in the lineage: datasets reading a removed process or feature, features no process computes, process views reading an object that is gone or an unregistered dataset, catalog rows whose view is gone, and lineage edges to views that are no registered process. Each gap carries the dead branch (what :func:dependents reaches from it) and the processes it affects. explain: callable(view) -> error text or None, run on the processes whose inputs include an object absent from DBC. Pure but for it.

dataset_feature_reads

dataset_feature_reads(H) -> List[dict]

The feature reads of every dataset of this store whose view was parsed (with their storage table), for the storage-level check: {dataset_id, dataset, fid, version, column, database, table, join, current, feature, domain, pids}. pids: the producing process and the processes reading the dataset. Pure.

annotate_removed

annotate_removed(gaps) -> None

Add, to each gap left by a removed process, when and by whom it was removed and what its view was, from FS_REMOVAL_LOG. Fail-open.

show_view

show_view(view: str)

(ddl, None) from SHOW VIEW <view>, else (None, error text).

explain_view

explain_view(view: str) -> Optional[str]

None when EXPLAIN SELECT * FROM <view> compiles, else the error text.