Skip to content

tdfs4ds.agent.vector_store_teradata

tdfs4ds.agent.vector_store_teradata

Teradata Enterprise Vector Store backend for the tdfs4ds business dictionary.

Provides the same public surface as the Chroma backend (:func:tdfs4ds.agent.vector_index.build_vector_index and :func:tdfs4ds.agent.vector_index.search_vector_index) but persists the three collections as Teradata vector stores via langchain_teradata:

  • {prefix}_objects -> FS_BUSINESS_DICTIONARY_OBJECTS
  • {prefix}_sections -> FS_BUSINESS_DICTIONARY_SECTIONS
  • {prefix}_columns -> FS_BUSINESS_DICTIONARY_COLUMNS

Embeddings are produced by :func:tdfs4ds.agent.embedding.get_vector_store_embedding which selects either teradatagenai.TeradataAI (when tdfs4ds.TERADATA_AI_API_TYPE is set) or the standard LangChain Embeddings — TeradataVectorStore accepts both at the embedding= argument.

Indexing source (v0.3.1.131): documents are supplied to TeradataVectorStore.from_datasets/add_datasets reading from a permanent CURRENT VALIDTIME view (FS_V_BUSINESS_DICTIONARY_*, created by genai.documentation.documentation_tables_creation()) — NOT via from_documents/add_documents (LangChain Document list input). The Document-list path has a confirmed bug: it internally stages the input through teradataml.DataFrame.from_dict(..., persist=True) / DataFrame.materialize(), which takes no database argument and may materialize as a volatile table (session-scoped) or a view — either way teradataml itself documents this choice as internal/opaque ("decided and taken care by teradataml"). A volatile table is invisible to the separate Vector Store REST session, causing TDML_2412: Object ... does not exist regardless of target_database (which only scopes where the FINAL collection metadata is registered, not where the input data is read from).

Deeper pitfall found on live VCL2 testing (v0.3.1.14x): reading from the permanent view is necessary but not sufficient on its own — the way the view is handed to from_datasets/add_datasets matters too. _from_datasets_kwargs() used to wrap the view in tdml.DataFrame.from_query("SELECT ... FROM <view>") before v0.3.1.14x. Confirmed live: DataFrame.from_query(...) sets _table_name to a freshly materialized, ephemeral ml___frmqry_v_<id> view at construction time (before teradatagenai is even called), and teradatagenai bakes whatever _table_name it receives permanently into the collection's _inputdata view DDL (confirmed via SHOW VIEW on a live collection: ... FROM "data_scientist"."ml___frmqry_v_<id>"). Once Vantage garbage-collects that ephemeral view (observed to happen almost immediately — confirmed absent from DBC.TablesV within the same session that created it), _inputdata — and collectionV_<name>, which joins through it — is permanently broken: every query touching it, from any client (similarity_search, a plain teradataml query, an unrelated MCP session), fails with Error 3807 / TDML_2412: Object ... does not exist, forever, not transiently (confirmed: 3/3 retries fail identically). The fix: data= is now passed as a plain fully-qualified "schema.view" string naming the permanent view directly (see _from_datasets_kwargs) — never a from_query() result, which guarantees no ephemeral materialization ever happens.

No row-level filtering of data= — ever. Neither from_datasets nor add_datasets exposes any filter/predicate/where-clause parameter (checked directly against the teradatagenai signatures/docstrings). Pointing data= at a filtered view instead of the full source view was considered and rejected: since _inputdata is a live view (re-evaluated on every query, not a frozen snapshot), a filtered data= would permanently narrow what the whole collection can ever return — hiding previously-indexed rows outside the filter from search forever. Separately, _index (the real embeddings table) is keyed by TD_ID, which _inputdata assigns via ROW_NUMBER() OVER (ORDER BY <text column>) — recomputed fresh on every evaluation of whatever data= points to, so any change in row count/ordering between the _index write and a later _inputdata evaluation risks silently misaligning embeddings with the wrong row's text/metadata. Always pass the entire, unfiltered source view.

logger module-attribute

logger = getLogger(__name__)

_SOURCES module-attribute

_SOURCES = ('objects', 'sections', 'columns')

_VALID_SOURCES module-attribute

_VALID_SOURCES = set(_SOURCES)

_SERVICE_DOWN_MSG module-attribute

_SERVICE_DOWN_MSG = "Teradata Vector Store service is unreachable or not enabled on this system (VECTOR_STORE_BACKEND='teradata'). Check that the Enterprise Vector Store feature is licensed/activated on this Vantage instance. Detail: {detail}"

_NOT_CONFIGURED_MSG module-attribute

_NOT_CONFIGURED_MSG = "Teradata Vector Store client has no base URL configured (VECTOR_STORE_BACKEND='teradata'). tdfs4ds automatically calls teradataml.set_auth_token(...) during connect()/setup() when one of the following TDFS4DS_VECTOR_STORE_AUTH_* environment variable groups is set: BASE_URL+AUTH_TOKEN (JWT, tried first — a token you already hold; in-cluster, map the platform's injected token onto it), BASE_URL+PAT_TOKEN+PEM_FILE (PAT), BASE_URL+USERNAME+PASSWORD (Basic), or BASE_URL+CLIENT_ID (OAuth). Reaching this error means either none of those groups were configured, or set_auth_token itself failed — check the connect()/setup() log for a 'Vector Store auth ... failed' warning line for the underlying reason. Detail: {detail}"

_AUTH_REJECTED_MSG module-attribute

_AUTH_REJECTED_MSG = "The Teradata Vector Store service REJECTED THE CREDENTIAL (SQLState 28000 — invalid authorization). The service itself is reachable and enabled: it answered, then failed to open a database session on your behalf. This is NOT a licensing/activation problem. Most likely causes, in order: (1) the token behind TDFS4DS_VECTOR_STORE_AUTH_AUTH_TOKEN has EXPIRED — a platform-injected OAuth/JWT access token is short-lived, and teradataml stores a ready-made token WITHOUT validating it, so set_auth_token() and a health probe both still succeed with a stale one; re-read the current token and call tdfs4ds.setup()/connect() again (or tdfs4ds.utils.vector_store_auth.configure_vector_store_auth()). (2) For the Basic group, TDFS4DS_VECTOR_STORE_AUTH_USERNAME/_PASSWORD are wrong for this system. (3) The database user cannot log on the way the service logs on for it — teradataml's own set_auth_token documentation requires the privilege to log in with a NULL password (see GRANT LOGON in the Teradata documentation). Detail: {detail}"

_BROKEN_COLLECTION_MSG module-attribute

_BROKEN_COLLECTION_MSG = "Teradata Vector Store collection '%s' is broken: its backing object is missing (%s). It cannot be searched or refreshed, and recreating it is the remedy, but this session could not destroy it (destroy() needs admin rights). Ask an admin to run tdfs4ds.agent.vector_store_teradata.reset_teradata_store(['%s']), or use tdfs4ds.VECTOR_STORE_BACKEND = 'chroma' meanwhile."

_COLLECTION_FAILED_MSG module-attribute

_COLLECTION_FAILED_MSG = "Teradata Vector Store collection '{name}' is in a failed state ('{status}') and will not recover on its own — retrying writes or searches against it will not help. Call tdfs4ds.agent.vector_store_teradata.reset_teradata_store(sources=['{source}']) (requires admin privileges) to destroy and recreate it from the current business dictionary, then rebuild the index."

_SOURCE_ALREADY_ATTACHED_MSG module-attribute

_SOURCE_ALREADY_ATTACHED_MSG = "build_teradata_index: '%s' already has its business-dictionary source attached, so add_datasets() added nothing — the collection is unchanged and its existing rows stay searchable. Business-dictionary rows documented since it was created are NOT in the index. The Enterprise Vector Store offers no admin-free way to resync an attached source (see build_teradata_index's KNOWN LIMITATION), so to pick them up call build_vector_index(force_rebuild=True) — or tdfs4ds.agent.vector_store_teradata.reset_teradata_store(sources=['%s']) directly. Both destroy and recreate the collection and require admin privileges."

_RESYNC_FAILED_MSG module-attribute

_RESYNC_FAILED_MSG = "build_teradata_index: could not refresh '%s'. The collection is intact and its existing rows stay searchable, but business-dictionary rows documented since it was last built are NOT in the index. Both mechanisms declined: %s. The usual cause of the second one is missing ADMIN rights — Collection.destroy() is admin-only, so a non-admin session cannot perform the full refresh that the incremental path fell back to. Ask an admin to run tdfs4ds.agent.vector_store_teradata.reset_teradata_store(sources=['%s']), or run build_vector_index(force_rebuild=True) from an admin session."

_SOURCE_VIEW_SPEC module-attribute

_SOURCE_VIEW_SPEC = {'objects': ('DOCUMENTATION_BUSINESS_DICTIONARY_OBJECTS_VIEW', 'BUSINESS_DESCRIPTION', ['DATABASE_NAME', 'OBJECT_NAME', 'OBJECT_TYPE']), 'sections': ('DOCUMENTATION_BUSINESS_DICTIONARY_SECTIONS_VIEW', 'SECTION_CONTENT', ['DATABASE_NAME', 'OBJECT_NAME', 'SECTION_NAME']), 'columns': ('DOCUMENTATION_BUSINESS_DICTIONARY_COLUMNS_VIEW', 'BUSINESS_DESCRIPTION', ['DATABASE_NAME', 'TABLE_NAME', 'COLUMN_NAME'])}

_SOURCE_DESCRIPTIONS module-attribute

_SOURCE_DESCRIPTIONS = {'objects': 'tdfs4ds business dictionary — object-level (table/view/dataset) descriptions.', 'sections': 'tdfs4ds business dictionary — dataset documentation sections (overview, entity, feature themes, business questions, intended audience).', 'columns': 'tdfs4ds business dictionary — column-level descriptions.'}

_TEXT_KEYS module-attribute

_TEXT_KEYS = ('content', 'page_content', 'text', 'document', 'business_description', 'section_content')

_SCORE_KEYS module-attribute

_SCORE_KEYS = ('score', 'similarity', 'distance')

_ResyncFailed

Bases: RuntimeError

No resync mechanism applied. Carries every attempt for the log.

attempts instance-attribute

attempts = attempts

__init__

__init__(message, attempts)

get_vector_store_embedding

get_vector_store_embedding()

Return the embedding object for the active vector-store backend.

Selection rule
  • tdfs4ds.TERADATA_AI_API_TYPE set -> teradatagenai.TeradataAI(...) (in-database embedding; used by the 'teradata' backend).
  • otherwise -> get_embeddings() (LangChain Embeddings; works with vLLM / TEI / OpenAI-compatible setups and is also accepted as embedding= by TeradataVectorStore).

The ChromaDB backend always uses get_embeddings() regardless of this setting — TeradataAI is a Vantage-side construct and is only meaningful when the vector store also lives in Vantage.

_store_name

_store_name(source: str) -> str

_import_vector_store

_import_vector_store()

_raise_service_unavailable

_raise_service_unavailable(exc: Exception)

_raise_not_configured

_raise_not_configured(exc: Exception)

_raise_auth_rejected

_raise_auth_rejected(exc: Exception)

_is_auth_rejected_error

_is_auth_rejected_error(exc: Exception) -> bool

True when exc is the Vector Store service reporting that the DATABASE rejected the credential it presented on the caller's behalf — e.g. Failed to execute collections_list. Response Code: 500, Message: [SQLState 28000] The UserId, Password or Account is invalid.

SQLState 28000 is the SQL standard's "invalid authorization specification": a credential rejection, nothing to do with reachability or licensing. It must be checked BEFORE _reraise_if_service_down's generic "response code:" branch, which would otherwise report it as "service unreachable or not enabled ... check licensing/activation" and send the user hunting for a feature flag while the real fix is a fresh token (see _AUTH_REJECTED_MSG).

Deliberately NOT gated on TeradataMlException: the same rejection can surface through other wrappers, and both markers matched here are specific enough that a false positive would have to be quoting a DB auth error verbatim.

_is_not_ready_error

_is_not_ready_error(exc: Exception) -> bool

True when exc is the transient "Collection ... cannot be updated as it is not in a ready state" 409 — the collection is still processing a prior async operation (its own creation, or a previous update), NOT a real failure. Distinguished from a generic 409/HTML service-down error (which also contains "Response Code:") by requiring the more specific "not in a ready state" phrase; must be checked BEFORE _reraise_if_service_down's generic "response code:" branch, which would otherwise misclassify this transient, retryable condition as "service unreachable/unlicensed."

_is_transient_staging_object_error

_is_transient_staging_object_error(exc: Exception) -> bool

True when exc is a 400 "Object ... does not exist" from similarity_search/ask — confirmed live: the Enterprise Vector Store REST backend materializes a short-lived per-query staging view (ml___frmqry_v_<id>) to run the search, and that object can be gone (dropped/never-committed/session mismatch) by the time the search reads it back — a platform-side race, not a licensing/activation problem. Must be checked BEFORE _reraise_if_service_down's generic "response code:" branch, which otherwise misclassifies this as "service unreachable/unlicensed" (every TeradataMlException from this API embeds "Response Code:", so that check alone can't distinguish a genuine gateway failure from this per-query staging race).

_is_transient_gateway_error

_is_transient_gateway_error(exc: Exception) -> bool

True when exc is a bare HTTP 5xx gateway error (502/503/504) — confirmed live on a VCL2 test instance: the Vector Store REST gateway occasionally returns a raw <html>...502 Bad Gateway...</html> body (or 503/504) with no relation to any code path or configuration issue — pure transient infrastructure flakiness that resolves on retry within seconds. Distinguished from a genuine "service unreachable/unlicensed" condition only by being retryable: unlike a persistent misconfiguration (missing auth, wrong URL, licensing), a gateway 502/503/504 is by definition a transient proxy/gateway-layer failure, so retrying a few times before giving up is safe and appropriate. Checked BEFORE _reraise_if_service_down's generic "<html"/"response code:" branch so a transient gateway blip doesn't immediately surface as the terminal _SERVICE_DOWN_MSG on the very first retry-able occurrence.

_is_source_already_attached_error

_is_source_already_attached_error(exc: Exception) -> bool

True when exc is the 400 the Vector Store returns from add_datasets() because the source object is ALREADY attached to the collection — Failed to execute collection. Response Code: 400, Message: You can only add object names that do not exist in the collection.

This is the server saying "nothing to add", not a failure of the service. _from_datasets_kwargs() always names the same FS_V_BUSINESS_DICTIONARY_* view, so EVERY second-and-later build_teradata_index() call reaches it once the collection exists (see that function's KNOWN LIMITATION) — this is an expected steady-state condition, not an edge case.

Must be checked BEFORE _reraise_if_service_down's generic "response code:" branch: matching on that alone reported this benign, expected condition as "service unreachable or not enabled ... check licensing/activation", sending the user hunting for a feature flag on a system whose Vector Store had just answered the request. Same misclassification shape as _is_auth_rejected_error (SQLState 28000) and _is_not_ready_error (the 409) — every TeradataMlException from this API embeds "Response Code:", so that substring can never distinguish a genuine gateway failure from an in-service one.

Deliberately NOT gated on TeradataMlException (same reasoning as _is_auth_rejected_error): the marker matched here is a full sentence specific to this API, so a false positive would have to be quoting it verbatim.

_is_broken_collection_error

_is_broken_collection_error(exc: Exception) -> bool

True when the collection's OWN backing object is gone (seen live, 0.3.2.1): Response Code: 500, Message: Object 'collection_<name>_index' does not exist.

The collection is listed and answers, but its _index / _inputdata table or its collectionV_ view no longer exists (dropped with the database objects, or a create that never finished), so it can neither be searched nor refreshed. Waiting or retrying changes nothing; recreating it loses nothing. It used to fall into the generic "Response Code:" branch and be reported as "service unreachable or not enabled ... check licensing" - the fourth misclassification of that shape (after the 409, 28000 and the attached-source 400). The match is on the collection's own object names, so the per-query staging race (ml___frmqry_v_*, a 400) is not caught here.

_retry_if_transient_gateway_error

_retry_if_transient_gateway_error(fn, label: str, timeout_seconds: float = 60.0, poll_interval: float = 5.0)

Call fn(), retrying while it raises a transient gateway 5xx (see _is_transient_gateway_error). Re-raises the original exception once timeout_seconds elapses, or immediately for any other exception shape.

A short default timeout (60s, vs. the 120s/1800s used for genuine async "not ready" waits) is deliberate — a gateway 502/503/504 either clears in a few seconds or reflects a real outage, in which case failing fast and surfacing the clear _SERVICE_DOWN_MSG (via the caller's own _reraise_if_service_down) is more useful than a long silent wait.

_reraise_if_service_down

_reraise_if_service_down(exc: Exception)

Inspect exc and raise a clear RuntimeError when it looks like either:

  • the Vector Store client itself is unconfigured (e.g. requests' MissingSchema/InvalidURL from a request built with a None base URL — "Invalid URL 'None/api/v2/...': No scheme supplied" — meaning set_auth_token/session auth was never set up). This is distinguished from "service down" because the fix is entirely client-side (call set_auth_token), not a licensing/activation problem on the Vantage instance.
  • the service answered but the DATABASE rejected the credential it presented (SQLState 28000, see _is_auth_rejected_error) — reported as an auth problem naming the token/credential variables, never as "unreachable or not enabled", because the fix is a fresh credential and not a licensing check.
  • the Vector Store REST gateway itself is unreachable/not enabled (any other transport-level failure, or a reachable-but-wrong endpoint returning a raw HTML/status body — e.g. a gateway 502 — instead of a JSON error).

Returns normally (does nothing) for any other exception, leaving it to the caller — in particular for the transient "not in a ready state" 409 (see _is_not_ready_error), the transient per-query staging-object race (see _is_transient_staging_object_error), and a transient gateway 502/503/504 (see _is_transient_gateway_error), which callers should retry rather than treat as fatal, so none of the three is ever raised as service-down here; and for the benign "source already attached" 400 (see _is_source_already_attached_error), which is terminal but means "nothing to add" and is handled by the caller.

Shared by every call site that talks to teradatagenai/langchain_teradata directly (_lookup_store_database, build_teradata_index's from_datasets/add_datasets/status calls) so neither failure mode is ever mistaken for "store doesn't exist yet, fall back to rebuild" — which would just fail again later with a much less clear message.

_collection_manager

_collection_manager()

Import and instantiate teradatagenai's CollectionManager.

Raises a clear RuntimeError (mirroring _import_vector_store) if the package is missing — the Teradata backend depends on it transitively via langchain_teradata, so a missing import here means a broken install.

_lookup_store_info

_lookup_store_info(name: str, target_database: Optional[str] = None) -> Optional[dict]

Return {"target_database": ..., "collection_status": ...} for a named vector store, or None if no store with that name exists.

target_database (cross-store federation, v0.3.1.148): when given, a row matches only if its own target_database also matches — because two feature stores using the same TERADATA_VECTOR_STORE_PREFIX have identically-named collections (e.g. tdfs4ds_objects) in different databases, and a name-only match would return the wrong store's status. Default None ⇒ name-only match (unchanged for existing callers).

Uses CollectionManager.list() — the same list-and-match pattern teradatagenai uses internally (Collection.exists, VectorStore.__init__) — so a caller can discover where a store lives before attaching, instead of assuming it matches tdfs4ds.SCHEMA. This catches the case where a store was created before a target_database fix (or a schema change) and now sits in a different database than expected.

collection_status is the authoritative signal for whether a write actually succeeded — confirmed live to take values like 'ready', 'update_failed', 'create_failed' (see _is_failed_status). A write call (from_datasets/add_datasets) returning without an exception only means "the request was accepted" (these operations are asynchronous) — it does NOT mean the operation succeeded; only collection_status reflects the true server-side outcome.

Raises RuntimeError (via _raise_service_unavailable) when the Vector Store service itself is unreachable/not enabled — distinguished from "store not found", which returns None normally. A transient gateway 502/503/504 (confirmed live on a VCL2 test instance — pure infrastructure flakiness, unrelated to code/config) is retried a few times first via _retry_if_transient_gateway_error before falling through to that service-down classification.

_lookup_store_database

_lookup_store_database(name: str) -> Optional[str]

Backward-compatible wrapper over :func:_lookup_store_info returning just the target_database (existing callers only need this).

_is_failed_status

_is_failed_status(status) -> bool

True when status (a collection_status value from :func:_lookup_store_info) is a terminal failure — e.g. update_failed, create_failed. Matches any ..._failed suffix rather than hardcoding the two observed strings, in case other failure variants exist.

A failed collection will NOT recover by waiting or retrying — it requires an explicit :func:reset_teradata_store (destroy + recreate).

_raise_collection_failed

_raise_collection_failed(name: str, status, source: str)

_from_datasets_kwargs

_from_datasets_kwargs(source: str) -> dict

Build the data/data_columns/key_columns/metadata_columns kwargs for TeradataVectorStore.from_datasets/add_datasets.

data is passed as a plain fully-qualified "schema.view" string pointing directly at the permanent FS_V_BUSINESS_DICTIONARY_* view — NOT a tdml.DataFrame.from_query(...) result. Confirmed live: DataFrame.from_query(...) sets _table_name to a freshly materialized, ephemeral ml___frmqry_v_<id> view at construction time — before teradatagenai ever sees it — and teradatagenai bakes whatever _table_name it receives permanently into the collection's _inputdata view DDL. Once Vantage garbage-collects that ephemeral view (observed to happen almost immediately), _inputdata (and anything joining through it, including similarity_search) is permanently broken (see module docstring). The view already exposes exactly the columns needed, so no projection/SELECT is needed here — data_columns/key_columns/metadata_columns tell from_datasets/add_datasets which columns of the named view to use.

_has_rows

_has_rows(source: str) -> bool

Cheap existence check — avoids calling from_datasets/add_datasets on an empty source (mirrors the old if not docs: continue guard).

_target_database_kwargs

_target_database_kwargs() -> dict

target_database= kwarg for from_datasets/add_datasets, scoped to tdfs4ds.SCHEMA — where the Vantage REST API registers the final collection's metadata.

_safe_destroy

_safe_destroy(vs_class, name: str) -> None

Destroy a named vector store if it already exists.

Looks up the store's ACTUAL database via CollectionManager.list() first, rather than assuming it lives at tdfs4ds.SCHEMA — a store created before a target_database/schema change can be sitting in a different database, and attaching only at the expected database would silently miss it, leaving an orphaned duplicate once a fresh store is created elsewhere on force_rebuild.

Calls destroy() unconditionally once existence is confirmed — do NOT gate on status() first. Collection.status() reports the async create/destroy/update OPERATION history, not current existence; a store that exists but has no reported operation history returns an empty DataFrame, which previously made this function conclude "nothing to destroy" and skip the call — leaving the real collection in place. The subsequent from_datasets() call then collided with a TeradataGenAIException: Collection ... already exists (Collection.create() does its own authoritative self.exists check), silently caused by a swallowed destroy() failure below (e.g. non-admin session — destroy() requires admin privileges per its docstring) going unnoticed because this function used to only log a warning and return.

A destroy() failure is therefore re-raised (not swallowed): silently continuing after a failed destroy just defers the same collision to the from_datasets()/add_datasets() call with a far less clear error.

_destroy_for_force_rebuild

_destroy_for_force_rebuild(vs_class, name: str, reason: str = 'force_rebuild=True') -> bool

Best-effort destroy of collection name for a force_rebuild=True index build. Returns True only when the collection is CONFIRMED gone (the caller may then treat the build as a fresh create); False, after logging a warning, whenever it was not destroyed for any reason.

:func:build_teradata_index deliberately never destroys on its routine path — destroy() is admin-only ("Only admin users can use this method" per Collection.destroy()'s docstring), so keeping it out is what lets a non-admin session index at all. But force_rebuild=True is an EXPLICIT opt-in, and from 0.3.1.133 to 0.3.1.166 it did nothing whatsoever: both branches ran identical code. A user who set it — which they do precisely because the index looks stale — got the same no-op, and once the source was attached, the same failure on the retry. Honouring it here restores the flag's meaning.

A refused destroy is a warning, never fatal: the caller falls through to the ordinary reuse path, so a non-admin session ends up exactly where it was before this function existed rather than newly broken.

_wait_until_collection_ready

_wait_until_collection_ready(collection, name: str, timeout_seconds: float = 1800.0, poll_interval: float = 5.0) -> None

Poll collection.get_details() — a read-only method — every poll_interval seconds until it succeeds, treating any exception as "the collection is still processing a prior async operation" (Collection create/update/destroy are all asynchronous: the server reports a request as "accepted and in progress" well before the operation actually completes, so a write issued right after can hit the collection while it's still busy and fail with a 409 "not in a ready state").

Deliberately read-only: unlike retrying the write itself, this probe can't create additional server-side contention while checking. Raises the last exception observed if the collection is not ready within timeout_seconds (a genuinely stuck/failed operation should surface clearly rather than hang the caller forever).

_verify_write_succeeded

_verify_write_succeeded(name: str, source: str, timeout_seconds: float = 300.0, poll_interval: float = 5.0, before: Optional[dict] = None, stale_polls: int = 3) -> None

After a write call (from_datasets/add_datasets) returns without raising, confirm the operation actually completed by polling collection_status (via :func:_lookup_store_info) until it reports 'ready'.

A write call returning cleanly only means "the request was accepted" (Collection writes are asynchronous) — NOT that the operation succeeded. Confirmed live: a store can log "indexed successfully" here while collection_status is actually update_failed/ create_failed and the collection holds no searchable data.

If collection_status ever reports a *_failed status while polling, raises immediately via :func:_raise_collection_failed (no point waiting further — it's terminal). Raises a plain RuntimeError on timeout if it never resolves to 'ready' or a failed status.

before is the collection's info (:func:_lookup_store_info) read just before a write to an EXISTING collection. Its 'ready' status is still reported for a moment after the write is accepted, so a 'ready' only counts once last_updated has changed or a non-ready status has been seen. A customer's log showed the race: a finalize pass logged "complete" seconds before the collection turned update_failed. When the service reports no last_updated, 'ready' is accepted after stale_polls polls. Not needed after a create: the collection did not exist before.

_first_line

_first_line(exc, limit=200)

_create_collection

_create_collection(vs_class, embedding, source: str, name: str)

Create the collection for source and confirm it really became ready.

_resync_attached_collection

_resync_attached_collection(attached, source: str, name: str, *, vs_class, embedding)

Make an EXISTING collection reflect the current rows of its source view. Returns (mechanism, collection) — the collection to use afterwards, which is a NEW object when the fallback recreated it.

Incremental first, full refresh as the fallback.

update(data_refresh=True) is the documented way to "refresh/regenerate vectors ... using existing backing/source tables", and that is exactly tdfs4ds's situation: the attached object never changes (always the same FS_V_BUSINESS_DICTIONARY_* view), only its rows do. It is cheap, needs no admin rights, and preserves the collection.

Until 0.3.1.169 the only call tried here was add_datasets, which means something else entirely: attach a NEW source object. It correctly refuses an object that is already attached (400 "You can only add object names that do not exist in the collection"), so on tdfs4ds's always-same-view source it was guaranteed to fail on every second and later build — the reported behaviour. It is not tried here at all any more: it cannot succeed in this branch by construction, and it is not free (~50s per attempt observed live before the 400 comes back).

data_refresh was investigated in 0.3.1.140 and rejected as unreliable — it worked once, then repeatedly failed with "At least one of collection_index, collection_parameters, ... must be provided for update". That finding stands for the service version it was made on; it was never evidence about newer ones, and a 2.2.17 service is reported to update incrementally without trouble. Where it does fail, the fallback below still gets the rows in, so an old service degrades instead of breaking.

The fallback is a full refresh: destroy and recreate the collection from the same source. It needs admin rights (destroy() is admin-only) and costs a full re-embed, which is why it is second — but it is the only mechanism that is guaranteed to pick up new rows.

Whether unchanged rows get re-embedded is the service's business, not ours. data_refresh's contract is only "refresh/regenerate vectors ... using existing backing/source tables"; it does not say whether it skips rows whose text is unchanged, and the client offers no way to ask for a delta. Verified against the installed teradatagenai: get_ids() raises NotImplementedError ("Currently this is not being supported by service"), and delete_by_ids keys on TD_ID, which _inputdata recomputes as ROW_NUMBER() OVER (ORDER BY <text>) — a position, not an identity, so a surgical delete can leave _index's frozen embeddings aligned to different rows (the 0.3.1.140 corruption). So a hand-rolled per-row incremental is expressible but unsafe, and is deliberately NOT attempted: data_refresh is assumed to handle it, as the best mechanism the service exposes.

build_teradata_index

build_teradata_index(force_rebuild: bool = False) -> None

Build or refresh the three Teradata vector stores.

Creates each collection on first use, and thereafter refreshes it in place via update(data_refresh=True) — falling back to destroy + recreate only when that does not apply (see :func:_resync_attached_collection). destroy() requires admin privileges ("Only admin users can use this method" per Collection.destroy()'s own docstring) and previously ran as a routine step of every force_rebuild=True call — a non-admin session, or any other destroy failure, was silently swallowed, leaving the stale collection in place so the following from_datasets() call then failed with TeradataGenAIException: Collection ... already exists (raised by Collection.create()'s own authoritative existence check). Reusing the collection sidesteps the admin requirement entirely and matches how the Enterprise Vector Store is meant to be refreshed incrementally.

Refreshing an existing collection (0.3.1.169): incremental first, full refresh as the fallback — see :func:_resync_attached_collection. update(data_refresh=True) is the documented "regenerate vectors from the existing backing tables" call and matches tdfs4ds's situation exactly (same attached view every time, changing rows); if it does not apply, the collection is destroyed and recreated, which always picks the rows up but needs admin rights.

Until 0.3.1.169 this branch called only add_datasets(), which means "attach a NEW source object" and therefore could not succeed against an always-identical source: every second and later build failed with 400 "You can only add object names that do not exist in the collection", so newly documented rows never reached the index. That 400 is no longer produced here at all; the classifier (:func:_is_source_already_attached_error) is kept for collections whose source was attached outside tdfs4ds, and because until 0.3.1.167 the 400 surfaced — via _reraise_if_service_down's "Response Code:" branch — as "service unreachable or not enabled ... check licensing/activation", which was doubly wrong: the service had just answered, and the two later stores were skipped.

force_rebuild=True really rebuilds (0.3.1.167): it destroys and recreates each collection (see :func:_destroy_for_force_rebuild), which is the only working way to pick up business-dictionary rows added since the collection was created, and also the way out of a *_failed collection. It requires admin privileges; when the destroy is refused the call falls back to the ordinary reuse path with a warning, so a non-admin session is never worse off. Between 0.3.1.133 and 0.3.1.166 the flag was purely informational — both branches ran identical code, so setting it (which users do precisely when the index looks stale) changed nothing at all.

force_rebuild=False still never destroys, so rows whose source business-dictionary entry was deleted are NOT purged from the index by that path (no admin-free per-id deletion API is available yet — see Collection.get_ids, currently an unimplemented stub upstream).

_retry_if_not_ready

_retry_if_not_ready(fn, name: str, timeout_seconds: float = 120.0, poll_interval: float = 5.0)

Call fn(), retrying while it raises the transient "not in a ready state" 409 (see _is_not_ready_error) — the collection is still processing a prior async operation (its own creation, or a previous update/add_datasets call). Every Collection write operation is asynchronous ("... request is accepted and in progress"); calling another write immediately after can hit the collection while it's still busy.

Re-raises the original exception once timeout_seconds elapses, or immediately for any other exception shape (not a not-ready 409).

_wait_until_destroyed

_wait_until_destroyed(name: str, timeout_seconds: float = 60.0, poll_interval: float = 2.0) -> bool

Poll _lookup_store_database(name) until it reports the collection gone, or timeout_seconds elapses.

Collection.destroy() is asynchronous — it returns as soon as the request is accepted ("Collection destroy request is accepted and in progress"), not once the collection is actually gone. Calling from_datasets() (create) immediately afterward is a race: the collection can still exist server-side, so create collides with TeradataGenAIException: Collection ... already exists even though destroy() itself reported success.

Returns True if the collection was confirmed gone, False on timeout (caller decides whether that is fatal).

reset_teradata_store

reset_teradata_store(sources: Optional[List[str]] = None) -> None

Destroy and recreate the named Teradata vector store collection(s) from scratch — the true "clean rebuild" that :func:build_teradata_index no longer performs (it always reuses/upserts, see its docstring), so stale rows (business-dictionary entries deleted since the last index) are never purged there.

Requires the connected user to have admin privileges — destroy() (via :func:_safe_destroy) raises otherwise (see Collection.destroy()'s docstring: "Only admin users can use this method"). Call this explicitly when you know you have admin rights and want stale rows gone; the routine build_vector_index()/rebuild_vector_indexing() path deliberately does not call this, so it keeps working for non-admin sessions.

Parameters

sources: Subset of "objects", "sections", "columns" to reset. None (default) resets all three.

_parse_one

_parse_one(source: str, hit) -> Optional[dict]

Normalise a single similarity-search hit into the standard row schema.

Confirmed live: a Teradata Vector Store hit's dict shape is flattened — the text lives directly under the source's real business-dictionary column name (business_description for objects/columns, section_content for sections), NOT a generic content/text key, and metadata (object_name, section_name, column_name, database_name, table_name) is flattened into the same dict, not nested under a metadata key. Before business_description/ section_content were added to _TEXT_KEYS, every hit's text stayed empty and was silently dropped by the if not text.strip() guard below — search_teradata_index would return 0 rows even when the underlying similarity_search call itself returned real, relevant hits.

_iter_results

_iter_results(raw)

Yield individual hits from whatever TeradataVectorStore.similarity_search returns (json string, list of Documents, list of dicts, object with .similar_objects attribute, etc.).

search_teradata_index

search_teradata_index(query: str, sources: list[str] = ('objects', 'sections', 'columns'), section_names: Optional[List[str]] = None, top_k: int = 10) -> pd.DataFrame

Search the Teradata vector stores using similarity search.

Cross-store federation (v0.3.1.148): searches the primary store's collections AND, when read-only stores are attached (tdfs4ds.read_stores()), each read-only store's OWN materialized collections — each store keeps its own collections in its own database, so this needs no re-index, only additional queries scoped by (vector_prefix, target_database) per store. Hits are merged, tagged with FEATURE_STORE provenance (only when >1 store), deduped on (OBJECT_NAME, FEATURE_STORE) and run through the shared distance gate. Fail-open: a store whose collections are absent/failed is skipped with a warning (the agent still answers it via the catalog / business-dictionary paths). A single store (the default) is byte-identical to the historical read. Chroma is a single local index and does NOT federate (see search_vector_index).