fs-upload¶
Engineer features, persist them as a permanent view, and register them in
the feature store with upload_features.
Source file
Lives at tdfs4ds/skills/fs-upload/SKILL.md.
name: "Feature Store: Upload" description: "Generate Python code to engineer features from a Teradata source, persist as a permanent view, and register in the tdfs4ds feature store with upload_features. Invoke when the user wants to create or update a feature engineering process." argument-hint: "[source-table][entity-column] [feature-columns]" allowed-tools: "Read, Edit, Write, Bash, Glob, Grep"
Generate Python code to engineer features from a Teradata source, persist the logic as a permanent view, and register + upload the features to the tdfs4ds feature store.
User context: $ARGUMENTS
Related skills (chain in this order)¶
| Before / After | Skill | Why |
|---|---|---|
| Before | fs-setup |
Open the Teradata context and activate a data domain. |
| Optional | fs-filter |
Segment a large source into iterative passes with FilterManager. |
| After | fs-document |
LLM-generated business description + EXPLAIN score 1–5 for the registered process. |
| After | fs-analyze |
If USER_SCORE is 1–3, run the full scalability analysis and propose a rewrite. |
| After | fs-rollout |
Backfill features across a date range with TimeManager. |
Demos: demos/notebooks/01 - Feature Engineering.ipynb, 02 - Register Your Features.ipynb.
Ask for any missing values before generating code:
- Source database and table — where the raw data lives.
- Entity column(s) — the primary key. A list of column names, e.g. ['CUSTOMER_ID'] or ['CUSTOMER_ID', 'EVENT_DATE']; the SQL types are read from the source. Do not ask for types.
- Feature columns — list of derived columns to register as features.
- View name — name for the permanent Teradata view.
- Metadata — optional dict with descriptive tags.
- Dataset view name — optional; if provided, also creates a ready-to-use dataset view.
Pattern A — SQL query, the view created by tdfs4ds (preferred)¶
Use for any feature logic written in SQL (aggregations, PERCENTILE_CONT, window functions). upload_features
compiles the query (EXPLAIN), refuses a target view that already exists, creates the view and registers it.
import tdfs4ds # reads the TDFS4DS_* settings of .env
tdfs4ds.reconnect_teradata(raise_error=True) # teradataml context from TDFS4DS_TD_HOST / _USERNAME / _PASSWORD
tdfs4ds.connect(database='<DATABASE>')
tdfs4ds.select_data_domain('<DATA_DOMAIN>') # or set TDFS4DS_DATA_DOMAIN in .env
import teradataml as tdml
feature_sql = """
LOCKING ROW FOR ACCESS
SELECT
<ENTITY_COLUMN>
, <AGGREGATION_EXPRESSION> AS <FEATURE_NAME>
FROM <SOURCE_DATABASE>.<SOURCE_TABLE>
GROUP BY <ENTITY_COLUMN>"""
process_id = tdfs4ds.upload_features(
df=None, # the source is the query
sql_query=feature_sql,
target_view_name='<DATABASE>.<VIEW_NAME>', # must not exist yet
entity_id=[<ENTITY_COLUMNS>], # list of key columns — SQL types read from the source
feature_names=['<FEATURE_NAME>'],
metadata={'project': '<PROJECT_NAME>'},
return_process_id=True, # keep the id for run() / roll_out()
# run_now=False, # register only; compute later under a TimeManager (fs-rollout)
# time_manager=tm, # attach the TimeManager the view reads: run(pid) then uses it
# close_absent_entities=True, # entities the view no longer returns lose their validity
# dataset_time_manager=True, # with time_manager=tm + dataset_view_name: the dataset follows tm (tm.update(i) moves it)
# track_entity_refresh=True, # admin record of when each entity was last confirmed (fs-inspect: entity_refresh)
# dataset_view_name='<DATASET_VIEW>', # also build a dataset of these features
# filtermanager=fm, # segmented ingestion (fs-filter)
# document=False, explain=False, # override the .env defaults of the AI steps for this call
)
The source can also be the name of an existing view (upload_features('<DATABASE>.<VIEW_NAME>', ...)) or a
teradataml DataFrame (Pattern B).
Pattern B — teradataml DataFrame methods + crystallize_view (fallback)¶
Use only when the feature transformation is expressed through the teradataml DataFrame API. Those build a temporary view that disappears when the context is removed; crystallize_view persists it as a permanent database object.
# ... connection setup same as Pattern A ...
from tdfs4ds.utils.lineage import crystallize_view
from tdfs4ds import upload_features
# Chained teradataml DataFrame methods produce a *temporary* Teradata view.
# crystallize_view persists it as a permanent database object before upload.
source_data = tdml.DataFrame(tdml.in_schema('<SOURCE_DATABASE>', '<SOURCE_TABLE>'))
df_features = source_data.groupby('<ENTITY_COLUMN>').agg({
'<VALUE_COLUMN>': ['sum', 'mean'],
})
feature_names = ['sum_<VALUE_COLUMN>', 'mean_<VALUE_COLUMN>']
df_features_proc = crystallize_view(
df_features,
view_name='<VIEW_NAME>',
schema_name='<DATABASE>',
)
upload_features(
df_features_proc,
entity_id=[<ENTITY_COLUMNS>],
feature_names=feature_names,
metadata={'project': '<PROJECT_NAME>'},
dataset_view_name='<DATASET_VIEW>', # ds_<base_name>
)
History in the source: valid_time_column¶
When the source already holds one row per entity and valid time (several rows per entity), give the process the
column that carries that time. One run then writes the whole history; no TimeManager and no roll_out are needed.
upload_features(
'<DATABASE>.<HISTORY_VIEW>',
entity_id=['<ENTITY_COLUMN>'],
feature_names=['<FEATURE_1>', '<FEATURE_2>'],
valid_time_column='<EVENT_TS>', # DATE or TIMESTAMP; not an entity column, not a feature
)
# change or switch off later: tdfs4ds.set_process_options(pid, valid_time_column='<COL>') / valid_time_column=False
- A row's value is valid from its time until the entity's next row (the last one until
9999-01-01). Equal consecutive values are merged. - (entity, column) must be unique; a NULL or duplicated valid time refuses the batch and the error names the column.
- 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.
FEATURE_STORE_TIMEand the TimeManager date no longer set the values' valid time. Not for a UAF ART view.- Prefer it to
roll_outwhen the dates are in the data. Keep a TimeManager when the logic must be recomputed as of each business date (seefs-rollout).
Types: force_varchar_length and a refused re-registration¶
force_varchar_lengthis a process setting (not given = as recorded for the view's process, else 1024;0or'none'= exact lengths). A new text feature joins the stored type of a registered text feature of the process that holds it, else the new text features share oneVARCHAR(ceil(longest / N) * N)per character set (at most two text tables).run()/roll_out()takeforce_varchar_length=None= as recorded. Change it:tdfs4ds.set_process_options(pid, force_varchar_length=N)(or'none'); read it inget_process_info(pid)['FORCE_VARCHAR_LENGTH'].- A registered feature keeps its stored type. The new source type is compared by whether the values fit (shorter VARCHAR, int into a wider int / FLOAT / DECIMAL, smaller DECIMAL, lower TIMESTAMP precision): a fit changes nothing.
- When it does not fit,
upload_featuresregisters nothing and returnsNone: read the log. It offers (1)CASTthe column in this view to the stored type; (2) widen the stored type for all producers withplan = tdfs4ds.plan_type_change(types={'<FEATURE>': '<TYPE>'}, entity=['<ENTITY_COLUMN>'], data_domain='<DOMAIN>'),tdfs4ds.write_relocation_script(plan, 'type_change.py'), a dry run, then--apply(seefs-relocate); (3) for incompatible types, another feature name.run()only warns and writes with the stored type. - A VARCHAR source longer than its stored type is reported as a warning per batch (the batch still completes).
Rules¶
-
Value types: a feature keeps the type the view computes (exact length, precision, character set); text is rounded up to
force_varchar_length. CLOB, BLOB, JSON and XML are refused at registration (0.3.5.17: nothing is registered, the call returnsNone): they cannot be compared, so an unchanged value would open a new period at every run. Extract scalar fields in the view (JSONExtractValue,XMLEXTRACT,SUBSTR) or CAST short text to VARCHAR. PERIOD and ST_GEOMETRY are accepted but cannot be aggregated (seefs-dataset). -
Prefer Pattern A — SQL passed as
sql_query=withtarget_view_name=: tdfs4ds checks it and creates the view. A failed check is logged with its fix and the call returnsNone(no traceback): read the log. dfis the first, required parameter even withsql_query=: passdf=Nonethere (omitting it is aTypeError).- Re-running a process is cheap and safe: the temporal MERGE writes nothing for a value that did not change (no new row, the period in force stands) and closes/opens a period only where a value moved, so a 15-minute schedule does not inflate the history. "Unchanged" means bit-identical: a float aggregate recomputed in another order can differ in its last digits and then opens a period.
- Only use
crystallize_view(Pattern B) when the teradataml DataFrame API is unavoidable and produces a temporary view. entity_idis a list of the key columns, e.g.['CUSTOMER_ID']or['CUSTOMER_ID', 'EVENT_DATE']. The SQL types are read from the source with their exact length and character set: never write them. (A dict of types is an early-version form, still accepted only to override an inferred type.)feature_namesmust be a subset of the view's columns, excluding the entity column(s).- Never register a 1:1 copy of a raw source column (e.g. uploading
claims_raw.amountunchanged). That is pure data duplication with no governance value. Join the source table in the training query instead and append the column tocolumn_names_X. upload_featuresregisters then callsrun(process_id)by default. Passrun_now=Falseto register only: the source is still checked withEXPLAINbefore anything is written, and the process is computed later — under a TimeManager withrun(..., time_manager=tm)orroll_out(seefs-rollout). Use it for any time-managed process: an immediate real-time run would store values valid from today, after the business dates.- The AI steps are configuration. With an instruct model configured,
upload_featuresdocuments the process and analyses its EXPLAIN plan by default; the defaults come from.env:TDFS4DS_AGENTIC_FEATURES(master switch),TDFS4DS_UPLOAD_DOCUMENT,TDFS4DS_UPLOAD_EXPLAIN,TDFS4DS_UPLOAD_OPTIMIZE_QUERY(off),TDFS4DS_UPLOAD_VECTOR_INDEXING(off). An argument in the call (document=,explain=,optimize_query=,vector_indexing=) always wins. Without an instruct model they are skipped silently. - Under a scheduler pass
on_failure='raise'(toupload_features,run,roll_out): a failed ingestion then raisesFeatureIngestionError. The default'continue'records the failure in the follow-up table and returns normally. time_manager=tmattaches the TimeManager the view reads (recorded inFS_TIME_MANAGER, likefiltermanager=), sorun(process_id)computes at its business date with no argument. The view's lineage is checked first: undertdfs4ds.TIME_MANAGER_CHECK='error'a manager it does not read is refused and nothing is registered (a view created fromsql_queryis dropped again); under the default'warn'it is a warning and the pairing is not recorded. Withouttime_manager, registration is unchanged before 0.3.6.0.- From 0.3.6.0 the manager is found without
time_manager=:upload_featureswalks the view's lineage and records the one TimeManager it reads (several: a warning, none recorded;tdfs4ds.TIME_MANAGER_DETECT = Falseturns it off). When the data domain has a clock (tdfs4ds.set_domain_time_manager(tm)), a process reading it gets its own soft clone<view>_TMand its view is rewritten to read it (soft_clone=Falsekeeps the clock,soft_clone=Trueclones any manager). Seefs-rolloutforroll_out(data_domain=)andrun([...], at=). storage_tag='SALES'for a process that must not share a storage table (the parallel planner serialises processes writing the sameFS_T_*table; also lets its primary index change alone later). Recorded inFS_FEATURE_PLACEMENTbefore the first run. A feature already registered elsewhere is refused: seefs-relocate.close_absent_entities=Truewhen entities can leave the source (closed accounts, delisted products). By default a run writes only the entities its view returns: one that disappears keeps its last values, and current datasets keep showing it. With the option (a process property inFS_PROCESS_OPTIONS, applied by every laterrun/roll_out), each run ends their validity from its valid time on; history before is kept and an entity that comes back gets a new period. A run that returned 0 rows closes nothing. With afiltermanager, every filter column must be an entity column (else the registration is refused). Change it later withtdfs4ds.set_process_options(pid, close_absent_entities=...);run/roll_out(..., close_absent_entities=...)override it for one call.- Manager-driven processes (TimeManager, FilterManager) read the manager's one-row view with a scalar subquery (
WHERE col = (SELECT ... FROM <manager view>)), never a join: seefs-filterfor what the execution plan reads. - Pass
return_process_id=Truewhen the generated script needs thePROCESS_IDafterwards — for a laterrun(process_id)or to assemble theprocess_listforroll_out. It returns the UUID string instead of the dataset, so the caller does not have to look it up in the process catalog: Default isFalse(returns the dataset, unchanged). - Always pass
dataset_view_nameto build a denormalised dataset view alongside feature registration. Name it by replacing thevw_feat_prefix withds_(e.g.vw_feat_savings_dist→ds_savings_dist). upload_featuresalways uses the classic multi-join (grouped=False) by default when it builds a dataset view — passgrouped=Trueat upload time for wide datasets (many features sharing the same source table) to reduce JOIN count viaMAX(CASE WHEN)pivoting instead of building the dataset separately afterward withbuild_dataset(..., grouped=True).- Always include
LOCKING ROW FOR ACCESSas the first line of everyREPLACE VIEW … ASbody. This tells Teradata to use access locks, preventing read queries from blocking concurrent writers. Omitting it causes read-lock contention on busy feature views. - When features will feed hyper-segmented ML (one model per partition, e.g. per region): include the partition column (e.g.
region) in the view SQL and infeature_names. This registers it as a feature sobuild_dataset()includes it naturally — no manual JOIN to the source table needed at training time. Usepartitioning=separately if you want to physically co-locate rows by that column for efficient partition elimination. partitioning=may only use entity columns (andFEATURE_ID).RANGE_Nrefuses aDECIMALcolumn (Error 3732: cast it toINTEGERin the view) andDATEbounds on aTIMESTAMPcolumn (Error 5714: useTIMESTAMP '...'bounds). Since 0.3.4.3upload_featureschecks the definition on a volatile table before registering anything, and returnsNonewith the reason when it is refused.- Ratios: CAST to FLOAT before dividing. A DECIMAL division keeps the operands' scale, so
SUM(dec)/COUNT(*)or an AUC computed in DECIMAL is silently rounded (seen: every KPI rounded to 2 digits).CAST(x AS FLOAT) / y. - Document the source tables before the first
upload_features(set_object_documentationor the business-dictionary loaders, seefs-document): the process documentation is written at registration from what the LLM knows then, and is not regenerated later withoutforce_update=True. - A model scoring process (BYOM / ONNX) is an ordinary process: a view calling
ONNXPredict(orPMMLPredict,H2OPredict...) from the database where BYOM is installed (oftenmldborTD_MLDB: check withHELP DATABASE), reading the model from the model table. Several models over time: store the training date with the model (save_byom(..., additional_columns=...)) and pick, in a sub-query, the latest one trained at or before the TimeManager's business date, so each roll-out date scores with the model available then. Feed the model COALESCEd inputs (NULLs from lags). Since 0.3.5.15 the documentation reads the model table's metadata (fs-document). - Script / tdstone2 not available?
SELECT * FROM SCRIPT(...)failing with Error 9854 means the Script table operator is not licensed on that system: use the BYOM path above (train in the client, export ONNX, score in the database) and check parity between the in-database scores and the client model on a sample. - Monitoring KPIs as a process: a view computing AUC / lift per business date (scores joined to the realised
labels) registered with a constant entity (e.g.
'propensity' AS use_case,entity_id=['use_case']) and the same TimeManager: the KPI history is then a feature history, with lineage and freshness. - The primary index and partitioning are recorded with the process; passing other values when registering the same view again only logs a warning. Change them with
tdfs4ds.plan_relocation(process_id=..., primary_index=..., partitioning=...).