Validation checklist¶
Prerequisites · You have authored a metadata JSON file.
End state · Confidence that your metadata is correct before you press run.
Use this checklist before the first run on a new pipeline. You can also return to it whenever a run fails with an unexpected error. The links beside relevant checks open the field shapes, allowed values, and defaults in the Metadata reference.
1. Document / provider preflight¶
- If you use the file provider, JSON is your canonical source and any YAML or Excel sibling has been regenerated after the latest edit.
- If you use the database or API provider, you know which
connection.workspace_idordataflow.workspace_idthe run should target. - Every connection has a nonblank
name. Keep names unique within the active document/provider scope for name-based references. Distinct explicitconnection_idvalues may share a display name, but a name reference then fails as ambiguous and must be replaced with the ID. If a workspace is supplied, this scope is thatworkspace_id. - Every name-based dataflow reference resolves to one dataflow name in its
scope. A dataflow may use an explicit
dataflow_idwithout a name. - If explicit
connection_idordataflow_idvalues are used, they are stable, unique, and intentionally owned by an external identity contract. Duplicate connection display names require distinct explicit IDs because omitted IDs derive from the name. - If
$schemais present, it follows the Metadata document contract: use the currentlatestalias for ordinary authoring or the exact compatible version for reproducible artifacts;dc validatestill reports the local framework-resolved version. - Root
extensionsvalues are project-owned annotations and are not being relied on as framework runtime settings. - Any nested JSON stored in Excel cells (
configure,secrets_ref,source_configure,destination_configure,transform) is valid JSON. - Excel rows include
nameandconnection_type; each source row includes a connection plussource_table,source_query, orsource_python_function. JSON/YAML may deriveconnection_typefrom an unambiguousformat, but Excel does not. - If metadata is exported to Excel,
source_filter_expressionis present as its own source column and survives an Excel → FileProvider round-trip. - You are not expecting
connection_type:"streaming"to work yet; that model value exists, but no built-in formats are mapped to it.
2. Connection basics¶
-
connection_typeandformatare a valid pair in Connection:connection_typeValid formatfilecsvparquetjsonjsonlavroexcellakehousedeltaicebergdatabasesqlapiapifunctionfunction -
If
connection_typeis omitted,formatalone still identifies the intended connection family. -
configure.base_pathexists on disk (or the cloud path is reachable) forfileandlakehouseconnections. Seeconnections[].configurefor shared endpoint settings. - Lakehouse connections using metastore registration have the right
catalog/databasevalues. - Database connections have either
configure.urlor a valid combination ofdatabase_type,host,port, anddatabase; see Connection settings by endpoint type. - API connections use
configure.base_urlrather thanconfigure.url. -
secrets_refonly lists field names that actually exist inconfigure. - No
configurefield appears under two differentsecrets_refsources. - For API auth, the required fields for the selected
auth_typeare present and the runtime has its optional dependency (for examplebotocoreforaws_sigv4). See API authentication. - Database transport-specific options such as MSSQL TLS flags match the selected engine/driver; open configure maps do not guarantee portability.
Quick database connectivity check:
```python
from sqlalchemy import create_engine, text
engine = create_engine("postgresql+psycopg2://user:pass@host:5432/db")
with engine.connect() as conn:
print(conn.execute(text("SELECT 1")).fetchone())
```
3. Dataflow envelope¶
- In the Dataflow envelope,
every
source.connection_namematches aconnection.name. - Every
destination.connection_namematches aconnection.name. -
stageis set — it is the filter you pass todriver.run(stage=…). All dataflows in the same logical step should share the samestagestring. - If execution order matters,
group_numberandexecution_orderare set explicitly instead of relying on file order. -
processing_modeisbatchfor the built-in driver. The model acceptsmicrobatchandstreamingvalues for specialized or future runtimes, but the normal built-in ETL path does not implement those modes. -
dataflow.is_activewas not accidentally set tofalseon the dataflow. - The source and destination connections'
is_activefields aretruefor every dataflow expected to run. Inactive connections remain in metadata but cause a selected dataflow to be skipped.
4. Source¶
- Each Source uses the right selector style:
- file / lakehouse / database table mode →
source.table - database query mode →
source.query - function source →
source.python_function
- file / lakehouse / database table mode →
- If a query source also has
source.table, treat it as a logical alias, not a limit on the SQL read. If a function source hassource.table, check how that function uses it and other Source fields. If either uses Shared schema hint entries, ensure the matching hints describe the output. - If the source is in a sub-folder/schema,
source.schema_nameis set. - If you want incremental loads,
source.watermark_columnsis set and the column actually exists in the source data. - For database table sources: the SQL schema (
source.schema_name) and table (source.table) exist in the target database. - For inline database queries:
source.queryruns successfully by itself. - For SQL-file queries: the
.sqlpath is resolved with the runner'ssql_base_pathorartifact_base_path; check the single-root or multiple-root prefix rules in Source patterns. - For SQL-file queries: the resolved SQL text runs successfully and the
selected query returns every column named by
watermark_columnsor later transform/destination rules. - For API sources:
connection.configure.base_urlandsource.configure.endpointtogether form the correct URL. - For API sources: pagination keys (
pagination_type,page_size,cursor_path,next_link_path,total_path) match the actual response. Seesource.configurefor the request options. - For API sources:
data_pathresolves to the response records list (or one object); a wrong path otherwise looks like an empty result. - For API cursor/offset pagination, custom parameter names (
cursor_param,offset_param,limit_param) match the provider contract. Built-in offset mode sends record offsets, not 1-based page numbers. - For API offset pagination with
total_path, its value is numeric,offset_max_workersrespects the provider's rate limit, andmax_pagescannot silently truncate the intended result.rate_limit_delaydoes not throttle those parallel requests. - For the legacy incremental API split,
watermark_range_interval_unithaswatermark_to_paramandwatermark_param_mapping, and the first run haswatermark_range_start; the API accepts both lower and upper bounds. These fields are not required for canonical bounded replay. - For API bounded reads or replay, prefer
range_param_mappingwith an explicit lower and upper binding for the selected field. Check each binding's location, operator, wire format, andresponse_column; useformat: integerfor numeric bounds so values are not stringified. - For API
range_param_mapping, choose onewatermark_valuemeaning per active request:observed_maxrequires a returned response field, whilerequest_endrequires an exact covered end and complete pagination. Mixed active meanings are rejected before HTTP. - For canonical bounded reads or replay, the selected field has a
range_param_mappingentry with explicit lower and upper operators. The endpoint wire format preserves the authored precision:datevalues are calendar dates,datetimevalues are whole seconds, and millisecond formats require millisecond-aligned values. Unrepresentable fractional precision fails before the request. - For API
next_linkpagination, treat the continuation URL as opaque by default. Configurenext_link_bound_mode: repeat_query_boundsonly when the endpoint contract requires query bounds on every page; matching, missing, duplicate, and conflicting bounds have distinct outcomes. - A bounded API replay
chunk_columnhas a matchingrange_param_mappingentry with both lower and upper bindings. The selected field may be outsidesource.watermark_columns; legacywatermark_param_mappingpluswatermark_to_paramis not sufficient for an exact[start, end)read. - For API next-link pagination, returned links remain on the configured HTTP(S) origin. The reader rejects a foreign host/port, scheme downgrade, userinfo, or non-HTTP(S) continuation before sending the next request.
- For a legacy endpoint whose upper bound is inclusive,
watermark_range_to_exclusive_offsetis intentionally set and its precision matches the API parameter. Do not use it to emulate an exclusive canonical range; userange_param_mappingoperators. - If
watermark_to_param_timezoneis set, the source-level value is intentional; it overrides the connection-level value. - For function sources:
source.python_functionis a dotted path likemypkg.loaders.load_ordersand is allowed by runtime prefix rules if you useallowed_function_prefixes. - Any
source.configure.read_optionsoverride is intentional and engine-valid. - If
source.filter_expressionis set, the SQL predicate references columns in the reader output (including aliases returned bysource.query), not columns added later by transforms. - For API sources,
source.filter_expressionis understood as a local DataFrame filter after response materialization. Put endpoint push-down parameters insource.configure.paramsorsource.configure.body. - If the file source uses
date_folder_partitionsor backward replay, you have verified the folder layout matches the pattern. - If a look-back is configured, it uses a supported shorthand or nested
backwardkey (hours,days,months,years,closing_day), and a source override contains the complete intended value rather than relying on a partial merge with the connection.
5. Destination¶
- The destination
formatis supported by a built-in writer:parquet,csv,json,jsonl,avro,delta, oriceberg. - The Destination block's
load_typeis set to one of:append,overwrite,full_load,merge_upsert,merge_overwrite,scd2. - If the destination is a flat-file writer (
parquet,csv,json,jsonl,avro), the load type is onlyappend,overwrite, orfull_load. - If
load_typeismerge_upsertorscd2:-
destination.merge_keysis set and is a list. - Every column in
merge_keysexists in the source data.
-
- If
load_typeismerge_overwriteanddestination.configure.replace_by_watermarkis not using a usable replacement window,destination.merge_keysis set and every key column exists in the source data. - If
load_typeisscd2:-
destination.configure.scd2_effective_columnis set. - The column named in
scd2_effective_columnexists in the source data.
-
- If
destination.configure.replace_by_watermarkistrue(seedestination.configure):-
destination.load_typeismerge_overwrite. -
source.watermark_columnsidentifies the window column and eithersource.configureor the referenced connectionconfigurecontains a look-back option such asbackward_daysorbackward. - You understand that
date_backwardis computed at runtime and is not an authored metadata field. - The source covers the complete replacement window, including rows that must be removed from the destination. See Cross-boundary combinations and Replace a watermark window.
-
- If
destination.partition_columnsare used:- Each item follows the Partition column
shape. Its
columneither already exists in the source data, or itsexpressionreferences columns that do.
- Each item follows the Partition column
shape. Its
- If
connection.configure.date_folder_partitionsis used for a flat-file destination, you understand thatpartition_columnstakes precedence when both are present. - Any
destination.configure.write_optionsoverride is intentional and engine-valid. - If you use
catalog/databaseregistration, the resulting qualified name resolves to the intended lakehouse table. - If an existing database/API metadata provider is upgraded, the additive
source_filter_expressionschema migration has run and passed its postflight check before the new runtime is started.
6. Transform¶
The Transform section defines the top-level fields checked here.
-
transform.schema_hintsentries follow the Schema hint shape and use a supporteddata_typefor the selected source type system. See Datatypes and schema hints. - Decimal hints use explicit
decimal(precision,scale)or matchingprecisionandscalefields; baredecimal/numberis rejected. - If a weak source reuses vendor hints, its source connection has
configure.schema_hint_type_systemset to the authored source dialect. -
source.connection.use_schema_hintis not disabled if you expect schema hints to take effect. -
deduplicate_columnsandlatest_data_columnsreference columns that actually exist in the source data. - Every
hash_columnsentry follows the Hash column shape and declares a non-empty, orderedcolumnslist; hash inputs do not fall back automatically to deduplication or merge keys. - For a surrogate-key-style hash, the explicit hash columns match the intended business/natural key; do not assume deduplication and merge keys always mean the same thing.
- The selected
algorithmmatches the use case:xxhash64only when signed 64-bit collision risk is acceptable,sha256when a larger digest is preferred, and neither as plain low-entropy PII protection. - If a hash target or its input columns change, downstream type/value migration has been planned; changing SHA-256 to XXHash64 changes String output to signed BIGINT.
- If you rely on deduplication but left
latest_data_columnsempty, you intentionally want ordering to fall back tosource.watermark_columns. - The SQL
expressionvalues in Additional column items are valid for your engine:- Polars: use
EXTRACT(YEAR FROM col), notyear(col). - Polars: use
CAST(col AS DATE), notdate(col). - Both: standard SQL arithmetic,
CASE WHEN, string concatenation work.
- Polars: use
- If
transform.filter_expressionis set:- The SQL predicate is valid for your engine.
- It only references source columns or columns created by
additional_columns(not system columns added later at order 70).
- You have not configured
__created_at,__updated_at,__updated_by, or__dataflow_run_idinadditional_columns— these are added automatically. - You are not trying to reference system columns inside
additional_columns; they are added later in the pipeline. - In
transform.configure,convert_timestamp_ntzdefaults tofalse; when it istrue,transform.configure.timestamp_timezoneis set deliberately. -
transform.configure.deduplicate_by_rankis only set when you want that behavior. - Downstream expectations account for final lowercase column names after
ColumnNameSanitizerruns.
7. Secrets¶
- Credentials are not hardcoded in
configure.urlorconfigure.password. - The Connection
secrets_reflists the correct field names fromconfigure. - The current value of each
configurefield listed insecrets_refis a vault key or environment variable name, not the real credential. - The vault/environment variables are available in the execution environment.
- The same
configurefield is not listed under two different secret sources.
Environment-variable example:
Quick check — environment-variable secrets:
import os
# Every variable referenced indirectly by secrets_ref must be set:
print(os.environ.get("DC_POSTGRES_URL")) # should not be None
See Concepts · Secrets · secrets_ref schema for the full secrets_ref
schema.
8. Load and run a quick smoke test¶
Before a full production run, test with a small subset. The quickest way is to
use dry_run=True to confirm metadata loading, selection, SQL-file references,
and replay-window structure without touching business data. It deliberately
does not resolve secrets, construct readers/writers, or execute transforms:
from datacoolie.core.models.run_config import DataCoolieRunConfig
from datacoolie.orchestration.driver import DataCoolieDriver
with DataCoolieDriver(
engine=engine,
metadata_provider=metadata,
config=DataCoolieRunConfig(dry_run=True),
) as driver:
result = driver.run(stage="ingest")
print(result)
# Valid targets are skipped (validated only); malformed paths/ranges are failed.
Or validate the metadata load alone:
from datacoolie.metadata.file_provider import FileProvider
from datacoolie.platforms.local_platform import LocalPlatform
provider = FileProvider(config_path="metadata.json", platform=LocalPlatform())
flows = provider.get_dataflows(stage="ingest")
print(flows) # list of DataFlow objects — inspect fields here
conns = provider.get_connections()
print(conns) # list of Connection objects
If either call raises, the error message points directly at the invalid field.
For merge-style destinations, remember that the first successful run may create the table with overwrite-style behavior before later runs switch to true merge semantics.
9. Common errors quick-reference¶
| Error message | Root cause | Fix |
|---|---|---|
Format 'delta' is not valid for connection_type 'file' |
Type/format mismatch | Use the valid pairs table in section 2 |
APIReader requires 'base_url' in connection.configure |
API connection used the wrong key | Put the root URL in connection.configure.base_url |
PythonFunctionReader requires source.python_function |
Function path is missing or was put on the connection | Put a dotted path on source.python_function |
connection 'X' not found |
connection_name typo in source or destination |
Check spelling against connections[].name |
Field 'url' listed in secrets_ref is missing from configure |
secrets_ref points at a non-existent config field |
Add configure.url first, then resolve it via secrets_ref |
MergeUpsertStrategy requires merge_keys |
Merge load type requires business keys | Add "merge_keys": [...] to destination |
MergeOverwriteStrategy requires merge_keys when no usable replacement window is available |
Key-based merge-overwrite was selected without keys or a usable watermark window | Add merge keys, or configure a valid watermark window with replace_by_watermark |
SCD2Strategy requires scd2_effective_column |
SCD2 without effective date | Add "configure": {"scd2_effective_column": "..."} to destination |
FileWriter only supports ['append', 'full_load', 'overwrite'] |
Merge or SCD2 was configured on a flat-file destination | Use Delta/Iceberg for merge-style writes or switch the load type |
Column not found: updated_at |
Watermark or dedup column doesn't exist | Check actual column names in source data |
year() / date() fails on Polars |
Unsupported SQL helper was used in metadata expressions | Use EXTRACT(...) or CAST(... AS DATE) |
JSONDecodeError in Excel cell |
configure cell contains invalid JSON |
Fix the JSON in that cell; ensure it's a valid object |
| All dataflows skipped / 0 loaded | is_active is false, the stage filter did not match, or the source legitimately returned zero rows |
Check is_active, driver.run(stage=...), and the source query/path |
Ready to run¶
If all boxes above are checked, run your first stage:
with DataCoolieDriver(engine=engine, metadata_provider=metadata) as driver:
result = driver.run(stage="ingest")
assert result.failed == 0, f"Pipeline failed: {result}"
print(f"Processed {result.total} dataflows, {result.succeeded} succeeded")
→ Back to Metadata guide overview