upload_features — register and ingest¶
tdfs4ds.upload_features is the single entry point for going from a Teradata
view to a registered, persisted, time-travel ready feature set.
Signature¶
tdfs4ds.upload_features(
df,
entity_id,
feature_names,
metadata = {},
primary_index = None,
partitioning = '',
filtermanager = None,
entity_null_substitute = {},
force_compute = True,
force_varchar_length = None,
dataset_view_name = None,
optimize_query = None,
update_view = True,
document = None,
max_candidates = None,
cascade_redocumentation = None,
vector_indexing = None,
sql_query = None,
target_view_name = None,
art_sql = None,
art_layers = None,
grouped = False,
return_process_id = False,
run_now = True,
on_failure = None,
raise_on_failure = None,
check_upstream = False,
strict_upstream = False,
explain = None,
time_manager = None,
close_absent_entities = False,
storage_tag = None,
track_entity_refresh = False,
dataset_time_manager = False,
valid_time_column = None,
soft_clone = None,
)
The arguments can be passed by position in this order; pass them by name.
| Argument | Type | Notes |
|---|---|---|
df |
teradataml.DataFrame or 'schema.view' str |
A DataFrame wrapping a permanent view, or a fully-qualified view name (schema mandatory). Use crystallize_view first if a DataFrame wraps a temporary view. |
sql_query |
str |
SQL-first path: a SELECT defining the source. Requires target_view_name. Takes precedence over df. |
target_view_name |
str |
'schema.view' where the view for sql_query is created (schema mandatory; must not already exist). |
entity_id |
list[str] |
The key column(s), e.g. ['CUSTOMER_ID']. Types are read from the source with their exact length and character set. A single string also works; a dict of types only overrides an inferred type. |
feature_names |
list[str] |
Subset of df.columns excluding the entity columns. |
metadata |
dict |
Free-form metadata stored on the process catalog row. |
filtermanager |
FilterManager |
Drives segmented ingestion. See FilterManager. |
dataset_view_name |
str |
If set, builds a denormalised dataset view in the same call. Convention: vw_feat_<x> → ds_<x>. |
document |
bool (default None = TDFS4DS_UPLOAD_DOCUMENT, True unless configured) |
When an instruct model is configured, document the process automatically. Silently skipped otherwise. |
explain |
bool (default None = TDFS4DS_UPLOAD_EXPLAIN, True unless configured) |
Part of document: also run the LLM analysis of the view's EXPLAIN plan. False documents without it and keeps any stored analysis. |
optimize_query |
bool (default None = TDFS4DS_UPLOAD_OPTIMIZE_QUERY, False unless configured) |
Run the query optimizer on the registered process view. |
The AI steps (document, explain, optimize_query, vector_indexing, cascade_redocumentation) take their default from the configuration, so a .env can turn the agentic side on or off without editing any call. TDFS4DS_AGENTIC_FEATURES=false turns them all off. An argument passed explicitly always wins. See Environment variables.
| Argument | Type | Notes |
|---|---|---|
update_view |
bool |
When True and an optimization improves the score, REPLACE VIEW is issued so subsequent runs use the rewritten SQL. |
art_sql |
list[str] |
UAF ART view (opt-in). A list of EXECUTE FUNCTION ... INTO VOLATILE ART(<name>) statements materialised per partition before ingestion so the feature view can read a secondary ART layer. Registers the process as PROCESS_TYPE='uaf art view'. Requires art_layers. See UAF ART-layer features. |
art_layers |
list[str] |
Required with art_sql: the declared LAYER(...) names the feature view reads (e.g. ['ARTFITMETADATA']). Used for documentation and lineage. |
return_process_id |
bool (default False) |
Return the registered process's PROCESS_ID (a UUID str) instead of the dataset, so you can hold it for a later run(process_id) / roll_out([process_id], …) without looking it up in the process catalog. Mirrors run(return_dataset=…) — the flag toggles what is returned, it is not a tuple. The default return is already None unless tdfs4ds.BUILD_DATASET_AT_UPLOAD is True, so in the common case nothing is given up. |
time_manager |
TimeManager (default None) |
The TimeManager the view reads, recorded at registration so run(pid) computes at its business date. A manager the view does not read follows TIME_MANAGER_CHECK (warning, or refusal under 'error'). An explicit manager is paired as before 0.3.6.0, even the data domain's clock. None (0.3.6.0): the view's lineage is walked and the manager it reads is recorded; the data domain's clock (set_domain_time_manager) gives the process its own soft clone. TDFS4DS_TIME_MANAGER_DETECT=false turns detection off. See One clock per data domain. |
soft_clone |
bool (default None) |
0.3.6.0. None: a soft clone <view>_TM for a process whose view reads its data domain's clock (found by detection). True: a clone of any manager. False: never. |
primary_index / partitioning |
list[str] / str |
Primary index and partitioning of the storage tables (default: the entity columns, no partitioning). Recorded with the process. |
entity_null_substitute |
dict |
Per-column replacement of NULL entity keys (COALESCE); an uncovered NULL key refuses the batch. |
force_compute |
bool (default True) |
Recompute even when the follow-up records the run COMPLETED at this valid time. |
on_failure / raise_on_failure |
'continue' / 'stop' / 'raise', or bool |
What a failed FilterManager segment does; 'raise' attempts every segment then raises FeatureIngestionError. |
check_upstream / strict_upstream |
bool |
Refuse the run when an upstream process FAILED (also RUNNING) at this valid time. |
grouped |
bool (default False) |
Strategy of the dataset built with dataset_view_name (see build_dataset). |
storage_tag |
str |
Store this process's features in a table of their own (__<TAG>); see Relocating features. |
close_absent_entities |
bool (default False) |
A process option, recorded in FS_PROCESS_OPTIONS and applied by every later run() / roll_out(). When True, each run ends the validity of the stored entities the process no longer produces, from the run's valid time on. See Entities that disappear. |
track_entity_refresh |
bool (default False) |
A process option, recorded in FS_PROCESS_SETTINGS and applied by every later run() / roll_out(). The ingestion writes nothing when a value is unchanged, so a feature row only tells when a value last changed; with the option on, each batch also records in an admin table (FS_R_*, never a feature, in no catalog) that the process confirmed each entity. Read with tdfs4ds.entity_refresh(pid) / tdfs4ds.entity_refresh_summary(pid); see Entity refresh. One extra SELECT DISTINCT and MERGE per batch; a failed write is a warning and the run stays COMPLETED. set_process_options(pid, track_entity_refresh=True), run(pid, track_entity_refresh=...) and roll_out(..., track_entity_refresh=...) change it. |
dataset_time_manager |
bool (default False) |
A process setting (FS_PROCESS_SETTINGS) applied by every later run() / roll_out(). When True and the process has a TimeManager (time_manager=, else the recorded one), the dataset of dataset_view_name is built with build_dataset(time_manager=...), grouped or not: it shows the manager's business date and tm.update(i) moves it without a re-run. Without a TimeManager: a warning and the usual current dataset. A dataset that already exists at CURRENT VALIDTIME is not rebuilt silently; the warning names tdfs4ds.rebuild_dataset(name, time_manager=..., replace=True). set_process_options(pid, dataset_time_manager=True) and run(pid, dataset_time_manager=...) change it. |
run_now |
bool (default True) |
False registers the process without executing it — catalog rows written and view created, but run() is not called and no feature values are computed. For a lazy "declare now, compute later" flow; pair with return_process_id=True and drive it with run(process_id) / roll_out(...). The source is validated with EXPLAIN before anything is registered (see below). Caveat: register_entity/register_features and FS_T_* creation happen inside run(), so until the first run the feature catalog has no entry for these features and their tables do not exist. Dataset documentation is skipped too. |
Validation when run_now=False¶
With run_now=True the run itself proves the source executes. When the run is skipped nothing
else does, so the source is EXPLAINed first — before any catalog row is written, so an
invalid source never leaves a registered process behind. On failure nothing is registered: a
prescriptive message is logged and None is returned, no traceback.
For a UAF ART view only the first art_sql statement is EXPLAINed, and the ART is not
materialised. art_sql order is authoritative — a later statement may read an ART an earlier one
creates — so without materialising, statements 2..n would fail for a reason unrelated to their own
validity. The feature view is skipped for the same reason (it reads the ART → Error 3807).
EXPLAIN is not a guarantee
It proves the statement compiles and its objects are reachable. It does not prove the
process will ingest: run() additionally resolves types (Gettdtypes /
validate_feature_types) and rejects NULL entity keys not covered by
entity_null_substitute, and the real ingestion statement is an unpivot CTAS, not
SELECT *. A lazily-declared process can EXPLAIN clean and still fail on its first run.
Return value¶
return_process_id |
Returns |
|---|---|
False (default) |
The dataset view — None unless tdfs4ds.BUILD_DATASET_AT_UPLOAD is True. |
True |
The PROCESS_ID string of the process this call registered. |
Either way a recoverable input error (missing schema, target view already exists,
SQL that will not compile) returns None after logging a prescriptive message —
no traceback. A None return means "check the log".
process_id = tdfs4ds.upload_features(
df, entity_id=['CUSTOMER_ID'],
feature_names=['nb_transactions', 'total_amount'],
return_process_id=True,
)
tdfs4ds.run(process_id) # re-run later without a catalog lookup
Three ways to supply the source¶
upload_features accepts the feature source in three forms. The classic
DataFrame path is unchanged; the other two are for pure-SQL users who don't
build teradataml DataFrames.
1. A teradataml DataFrame (classic)¶
df = tdml.DataFrame(tdml.in_schema('my_db', 'FEAT_ENG_CUSTOMER'))
tdfs4ds.upload_features(df, entity_id=['CUSTOMER_ID'],
feature_names=['nb_transactions', 'total_amount'])
2. A view name ('schema.view')¶
Already have a view? Pass its fully-qualified name. The schema is mandatory — a bare view name is rejected.
tdfs4ds.upload_features(
'my_db.FEAT_ENG_CUSTOMER', # schema.view — schema required
entity_id = ['CUSTOMER_ID'],
feature_names = ['nb_transactions', 'total_amount'],
)
3. A SQL query + a target view (SQL-first)¶
Give a SELECT and the schema.view to create. tdfs4ds validates the SQL
with EXPLAIN, creates the view (it must not already exist), confirms it is
executable, and uses it as the source. The created view is permanent — it
becomes the process view re-run by run(process_id), so pick a stable name.
tdfs4ds.upload_features(
sql_query = """
SELECT CUSTOMER_ID,
COUNT(*) AS nb_transactions,
SUM(AMOUNT) AS total_amount
FROM source_db.TRANSACTIONS
GROUP BY CUSTOMER_ID
""",
target_view_name = 'my_db.FEAT_ENG_CUSTOMER', # schema.view to CREATE (schema required)
entity_id = ['CUSTOMER_ID'],
feature_names = ['nb_transactions', 'total_amount'],
)
Errors are logged, not raised
Recoverable input problems — a view name without a schema, a
target_view_name that already exists, or SQL/a view that fails to
compile or isn't executable — do not raise a Python traceback. Instead a
clear message with a fix is written to the log and upload_features
returns None. If a call returns None, read the log. For example, if
the target view already exists you'll be told to
DROP VIEW my_db.FEAT_ENG_CUSTOMER; first (or choose a different
target_view_name) — the existing view is never silently overwritten.
UAF ART-layer features (art_sql / art_layers)¶
For a UAF (Unbounded Array Framework) feature whose value lives in a secondary
ART layer — ARIMA fit-metrics (R_SQUARE/MAPE/AIC in ARTFITMETADATA),
residual tests over ARTFITRESIDUALS, TD_PACF — pass art_sql (the
INTO VOLATILE ART statements that materialise the ART) and art_layers (the
layer names your view reads). tdfs4ds materialises the ART per partition,
in-session, before ingestion, so the view can read the layer. See the dedicated
guide: UAF ART-layer features.
Entities that disappear (close_absent_entities)¶
A run writes the entities its view returns and leaves every other stored entity as
it was. So when a customer leaves the source, its last values stay valid
(UNTIL_CHANGED) and the current datasets keep showing it.
Declare the process with close_absent_entities=True and every run also ends the
validity of the entities the process no longer produces, at the run's valid time:
pid = tdfs4ds.upload_features(
'my_db.FEAT_ENG_CUSTOMER',
entity_id = ['CUSTOMER_ID'],
feature_names = ['nb_transactions', 'total_amount'],
time_manager = tm,
close_absent_entities = True,
return_process_id = True,
)
tdfs4ds.roll_out([pid], tm) # nothing else to pass: the option belongs to the process
What it does, exactly:
- The same rule as the values. A run states the truth from its valid time t
onward: the entities it returns hold their values from t, the ones it no longer
returns have no value from t. t is the TimeManager date under
roll_out,FEATURE_STORE_TIMEwhen set, else the run's real-time instant. - History is kept. The period before t is not touched; datasets read
as_ofan earlier date still return the entity. - An entity that comes back gets a new period; its absence stays a gap (a historical dataset shows two periods).
- Idempotent. Re-running the same date changes nothing; replaying a history in
date order gives the same result. As for the values, re-running an earlier
date on its own rewrites everything from that date on: replay a history from its
first date (
roll_out(..., time_id_start=1, force_compute=True)). - Never on an empty run. A run whose view returned no row closes nothing (an
empty or failed upstream must not end every entity) and records
COMPLETED,0 rows. - A failed close fails the run: the batch is recorded
FAILEDwith the database error andon_failureapplies. - Segmented runs close only their own segment. With a
FilterManager, every filter column must be an entity column, so that a segment can tell which stored entities are its own; otherwise the registration is refused (nothing registered,Nonereturned).
Change the option of an existing process without re-registering it, or override it for one call:
tdfs4ds.set_process_options(pid, close_absent_entities=False)
tdfs4ds.run(pid, close_absent_entities=True) # this call only
tdfs4ds.roll_out([pid], tm, close_absent_entities=False)
Re-registering a view with the default close_absent_entities=False switches
the option off. get_process_info(pid)['CLOSE_ABSENT_ENTITIES'] shows it.
History from a column of the source (valid_time_column)¶
If the source holds one row per entity and valid time, register the process with the column that carries the time. One run writes the whole history.
tdfs4ds.upload_features(
'<DATABASE>.<HISTORY_VIEW>',
entity_id=['<ENTITY_COLUMN>'],
feature_names=['<FEATURE_1>'],
valid_time_column='<EVENT_TS>',
)
- Meaning. The column is a DATE or TIMESTAMP of the source, neither an entity column nor a feature. A row's value
is valid from that time until the entity's next row (the last row:
9999-01-01). The key (entity, column) must be unique; a NULL or duplicated valid time refuses the batch and the error names the column. - Rules. Equal consecutive values are merged. Each entity's history is replaced from its earliest valid time in the load onward and kept before it. A re-run is a no-op, an incremental load rewrites only its own tail, a backdated correction needs a full load. An incremental load can leave two adjacent periods with the same value at its start; a later full load merges them, and history datasets merge them too.
- Valid time of the run.
FEATURE_STORE_TIMEand the TimeManager date do not set the values' valid time (a TimeManager can still bound the rows the view returns). The follow-up row is unchanged. - Options.
close_absent_entities: an entity absent from a load has no value from the load's earliest valid time.track_entity_refreshrecords the load's latest valid time. - Types. DATE becomes midnight; TIMESTAMP(6) is truncated to the second; without a time zone the session time zone applies.
- Refused. A UAF ART view; an entity column, a feature, a missing column or a non-date type at registration
(returns
None, nothing registered); a store withoutFS_PROCESS_SETTINGS. If that table exists but cannot be read,run()raises. - Change it.
tdfs4ds.set_process_options(pid, valid_time_column='COL'), orvalid_time_column=Falseto switch it off.get_process_info(pid)['VALID_TIME_COLUMN']shows it; migration carries it over. - Compatibility. Opt-in. A client older than 0.3.5.0 running such a process ignores the setting.
- Before 0.3.5.0. Load a TimeManager from the distinct dates and use
roll_out, one run per date, replayed from the first date. Still valid when each date must be recomputed with the view's logic as of that date.
Types of features¶
force_varchar_lengthis a process setting, recorded inFS_PROCESS_SETTINGS(FORCE_VARCHAR_LENGTH). Not given (None, the default): the value recorded for the view's process, else 1024, so a re-registration never resets it.0or'none': exact lengths. A new text feature takes the stored type of a registered text feature of the process that holds it; otherwise the new text features share oneVARCHAR(ceil(longest / N) * N)per character set (longest over the new features; LATIN and UNICODE apart): at most two text tables per process.run()androll_out()takeforce_varchar_length=None, meaning "as recorded" (1024 when nothing is recorded, i.e. a process registered before 0.3.5.1). Change it withtdfs4ds.set_process_options(pid, force_varchar_length=N)(or'none');get_process_info(pid)['FORCE_VARCHAR_LENGTH']shows it. Migration carries it over.- A registered feature keeps its type. When the source type changes, it is compared to the stored one by whether the values fit: a shorter VARCHAR of the same character set or into UNICODE, an integer into a wider integer, FLOAT or a large enough DECIMAL, a smaller DECIMAL, a lower TIMESTAMP precision. A fitting type changes nothing.
- A type that does not fit is refused.
upload_featuresregisters nothing and returnsNone; the log gives the alternatives with code. (1)CASTthe column in this view to the stored type. (2) Widen the stored type for every process computing the feature:tdfs4ds.plan_type_change(types={...}, entity=[...], data_domain=...), thenwrite_relocation_script, a dry run,--apply(see Changing the type of a feature). (3) For incompatible types (text and number, ...), give the column another feature name.run()/roll_out()log the same text as a warning and write with the stored type. - Truncation warning. When a registered VARCHAR feature's source is longer than its stored type, each batch reads
the longest staged value and warns (it fits, or N values are too long: truncated in Teradata mode, refused in ANSI
mode). The batch writes and stays
COMPLETED. - Changing
valid_time_columnis never refused: no table definition is involved.
Full example¶
tdml.execute_sql("""
REPLACE VIEW my_db.FEAT_ENG_CUSTOMER AS
SELECT
CUSTOMER_ID,
COUNT(*) AS nb_transactions,
SUM(AMOUNT) AS total_amount,
AVG(AMOUNT) AS avg_amount
FROM source_db.TRANSACTIONS
GROUP BY CUSTOMER_ID
""")
df = tdml.DataFrame(tdml.in_schema('my_db', 'FEAT_ENG_CUSTOMER'))
tdfs4ds.upload_features(
df,
entity_id = ['CUSTOMER_ID'],
feature_names = ['nb_transactions', 'total_amount', 'avg_amount'],
metadata = {'project': 'customer_analytics'},
dataset_view_name = 'ds_customer_spend',
document = True,
)
What happens internally¶
| Step | Module |
|---|---|
| Type detection on entity columns | utils.info.Gettdtypes |
| Entity & feature registration | feature_store.entity_management, feature_store.feature_store_management |
| Process registration | process_store.process_registration_management |
Optional simplify_query + optimize_query |
agent.query_optimizer |
Optional document_process_incremental |
genai.documentation |
Volatile staging table + MERGE (or UPDATE+INSERT) |
feature_store.feature_data_processing |
Optional build_dataset + document_dataset_incremental |
dataset.builder, genai.documentation |
COLLECT STATS on every target feature table |
feature_store.feature_data_processing.apply_collect_stats |
A failing batch always re-raises after recording FAILED in the follow-up
table. With a FilterManager, each filter is wrapped in try/except so a
single failed partition does not abort the whole loop.
Rules¶
- Always pass a permanent view. Temporary views vanish with the
teradataml context — your registered process will be unable to re-run.
Use
tdfs4ds.utils.lineage.crystallize_viewif your DataFrame chain produced a temporary view. - Never construct
selected_featuresmanually for the dataset. Whendataset_view_nameis set,tdfs4dsresolves feature versions automatically. upload_featurescallsrun()internally — this is intentional, not a bug. It exercises the exact same code path used byroll_outand re-execution, catching corner cases at registration time.- Entity-key types are preserved exactly. When
entity_idis a list or string, the SQL type of each key is inferred from the source view with its full length and character set — aCHAR(14)key is created asCHAR(14)in the feature table, never truncated toCHAR(1). Pass a dict only when you want to override the inferred type (it is then used verbatim — include the length). - NULL entity keys are forbidden. A NULL in any entity / primary-index key
column raises a
ValueErrorduring ingestion, because a NULL key produces a cartesian-productMERGE(severe skew and storage blow-up). Either filter the NULL rows out of the source view, or passentity_null_substitute={'COL': <value>}to replace them viaCOALESCE.
Re-execution¶
run re-applies the registered view at the current FEATURE_STORE_TIME.
For historical replay, advance TimeManager and re-run — see
TimeManager & roll_out.