Snowflake¶
Runnable example
snowflake_usage.ipynb
walks through both pieces below — SnowflakeSource and
SnowflakeLineageStore — end to end. It needs a real Snowflake account to
execute, but every cell is annotated so it reads top-to-bottom without one.
ConsentML integrates with Snowflake in two independent places. You can use either on its own:
SnowflakeSourcereads the training set out of Snowflake and records its provenance — the data source. Lineage still lands in the default SQLite store unless you change it.SnowflakeLineageStorepersists the hash-chained lineage in Snowflake — the audit log. Reached withstore=(on@trackandverify_audit_log) or directly viaopen_store.
Install¶
Both pieces need the snowflake-connector-python driver, which comes with this
extra. Without it, importing the Snowflake modules fails with a clear message
telling you to install the extra rather than a bare ImportError. The
connector is never imported at package import time, so import consentml stays
dependency-free for offline users.
Credentials come from you at call time (user/password or key-pair), never from a file in the repo. Only the non-secret coordinates — account, database, schema, warehouse — are ever recorded; the user, password, or key never are.
SnowflakeSource: Snowflake supplies the training data¶
SnowflakeSource takes a connection dict, the query that produces the training
data, and the name of the column that identifies the subject:
from consentml import track
from consentml.sources.snowflake import SnowflakeSource
connection = {
"account": "ab12345.us-east-1",
"user": "analyst_readonly", # a role holding read-only grants
"password": "...", # from os.environ / a secret, never hardcoded
"database": "CLINIC",
"schema": "PUBLIC",
"warehouse": "ANALYTICS_WH",
}
@track(
model_name="readmission-risk",
source=SnowflakeSource(
connection=connection,
query="SELECT PATIENT_ID, AGE, LDL, OUTCOME FROM patients",
subject_id_col="PATIENT_ID",
),
)
def train(df):
return LogisticRegression().fit(df[["AGE", "LDL"]], df["OUTCOME"])
model = train() # no argument: ConsentML runs the query and supplies the data
source= is evaluated once, when the decorator runs, not each time train is
called — see Tracking a run for what that
means.
Read-only is your responsibility¶
Unlike Postgres, Snowflake exposes no connection-level read-only flag, so
ConsentML cannot force it. Supply a role that holds read-only grants on
the data you read; ConsentML records the coordinates but cannot police the
permissions.
What provenance records¶
Each run's provenance holds the exact query, its SHA-256, the tables the plan
touched (best-effort via EXPLAIN USING JSON), and where the data came from —
never the credentials:
{
"kind": "snowflake",
"account": "ab12345.us-east-1",
"database": "CLINIC",
"schema": "PUBLIC",
"warehouse": "ANALYTICS_WH",
"query": "SELECT PATIENT_ID, AGE, LDL, OUTCOME FROM patients",
"query_sha256": "5bf0b4af91782c86...",
"referenced_tables": ["CLINIC.PUBLIC.PATIENTS"],
"referenced_tables_source": "explain",
"n_rows": 5
}
referenced_tables_source is "explain" when Snowflake's plan was parseable
and "unavailable" otherwise. A failed EXPLAIN is advisory only — it never
fails the training run.
SnowflakeLineageStore: the audit log lives in Snowflake¶
To keep the hash-chained lineage itself in Snowflake, pass a connection dict as
store= on @track. A dict target selects Snowflake; a path or None keeps
the default SQLite store.
@track(
model_name="readmission-risk",
store=connection, # lineage -> Snowflake
source=SnowflakeSource(connection=connection, query=..., subject_id_col=...),
)
def train(df):
...
store= and db_path= are mutually exclusive; passing both raises
ConsentMLError. The same target works when verifying:
Because the connection dict is an explicit target (unlike a typo-able file
path), verification does not pre-check for a missing database: verifying a
never-written Snowflake chain reports ok=True with zero entries rather than a
missing_database finding.
Driving the store directly¶
When you are not using @track — recording a run trained outside a decorated
function — reach the store through open_store:
from consentml.store import open_store
store = open_store(connection) # dict -> SnowflakeLineageStore
try:
store.record_training_run(...)
for entry in store.audit_entries():
...
finally:
store.close()
A snowflake:// URI is deliberately rejected as a target: it cannot safely
carry a password, so pass a dict.
Same chain, different backend¶
The Snowflake schema is denormalized relative to SQLite — no subject interning,
since a columnar warehouse does not benefit from it — but the audit-log row
shape and the entry_hash formula are byte-for-byte identical. The same
verify_audit_log code verifies a chain no matter which backend wrote it.
Single writer per lineage table
The Snowflake store assumes a single logical writer per lineage table.
The audit chain is a read-then-append (read the last entry_hash, write a
row whose prev_hash is that value); a warehouse will not serialize that
for you, so two concurrent writers could fork the chain. Coordinating
multiple writers is an explicit non-goal.
CLI commands and their Snowflake equivalents¶
The consentml command-line tool operates on local SQLite databases only —
every subcommand takes a --db file path and none accepts a connection dict
or snowflake:// target. Where a Snowflake equivalent exists at all, it lives
in the Python API:
| CLI (SQLite only) | Snowflake equivalent |
|---|---|
consentml verify --db x.db |
verify_audit_log(store=connection) (Python only) |
consentml migrate --db x.db |
none — see below |
consentml revoke / export |
none (Python API is SQLite-only for these too) |
verify has a Snowflake path — through Python¶
There is no consentml verify flag that points at Snowflake, but the same
check is available in the Python API by passing store=:
from consentml import verify_audit_log
report = verify_audit_log(store=connection) # verifies the chain in Snowflake
print(report.ok)
This is the store= target described in
the section above —
the identical row shape and entry_hash formula mean the same verification
logic runs regardless of backend.
migrate has no Snowflake equivalent — by design¶
There is no way to migrate a Snowflake lineage store, in the CLI or the
Python API — migrate_database() takes only a db_path, never a store=.
This is intentional, not a gap. Migration exists to move a SQLite database
between schema versions (v0 → v1 introduced subject interning; the current
schema builds on that). The Snowflake store is deliberately denormalized —
no subject-interning table, so there is no schema-version progression for a
migration to walk. A Snowflake lineage table written by any released version of
ConsentML is already on the only shape the Snowflake backend uses.
If you need to move lineage between SQLite and Snowflake, that is a copy/re-record operation, not a migration, and ConsentML does not automate it today.