introspect_schema, run_read_only_sql, extract_definitions, extract_evidence), never on vendor name, so adding a new source is a registration, not a core-code change.
The connector factory
canonic.yaml stores a connection as a descriptor: id + type + params (+ optional credentials_ref), not a live instance. At startup, the ConnectorFactory looks up type in its registry and builds the connector:
type raises UnknownConnectorType (exit 13) listing what is registered, never a silent fallback. Manage connections with canonic connection.
The credential provider registry
Credentials resolve through a second registry with the same shape. Acredentials_ref of env:, keyring: or file: resolves once to a fixed string. A provider:<name> ref instead selects a registered credential provider, which fetches a short-lived credential from a cloud issuer and reports its expiry:
UnknownCredentialProvider listing what is registered, mirroring UnknownConnectorType.
The consequence for a connector is when it resolves its credential. A static ref can be baked into a connection string at startup. A provider-backed one cannot: a 15 minute Redshift IAM credential is dead long before a daemon is, so the connector resolves on every connect instead. redshift is the first connector that does this, and any future provider-backed connector inherits the same rule. See dynamic credentials for the configuration surface.
Three classes of connector
Queryable (primary)
Implementintrospect_schema + run_read_only_sql: these feed the semantic layer and are executed against directly by canonic query / canonic sql.
Definition
Implementextract_definitions: feed the semantic layer and canonical-binding candidates from modeling code, never the query path (no run_read_only_sql).
A definition connector is its own connection entry (own
id) but describes tables that live in a different, queryable connection. params.target_connection names that connection’s id, so the evidence is attributed to it rather than to the definition connector’s own id: this is what lets its RelationSchema proposals land on the same semantics/<connection>/<name>.yaml target as that connection’s live introspection and reconcile against it (see the worked example), instead of colliding on the project-wide unique source name. Omitting it falls back to the definition connector’s own id, fine for a definitions-only project with no paired physical connection, but such sources can never actually be queried (no run_read_only_sql).
Evidence
Implementextract_evidence: feed knowledge pages and reconciliation signal from docs and BI usage. Also never queryable.
A BI question’s SQL is only ever evidence, never executed. If a Metabase/Looker-encoded metric is adopted, it’s recompiled through the deterministic compiler like any other definition, never run as-is.
extract_evidence capability as notion and url today, and can be added as a new registered type without touching core logic.
Normalized evidence, one shape per kind
Every connector translates its native output into one of a few normalized shapes, so the ingestion engine and compiler never see vendor-specific structures:RelationSchema, a table/view: columns (normalized types), primary key, foreign keys, row-count estimate. From queryable connectors’introspect_schema.DefinitionEvidence, a named measure/dimension/join from modeling code, with its expression and additivity. Fromextract_definitions.DocEvidence, a title + body + candidate topic references, with ausage_hintthat maps to a knowledge page’susage_mode. Fromextract_evidencedoc sources.UsageEvidence, a BI artifact (question/dashboard), the metric it appears to define, and how often it’s used, always a candidate, never auto-promoted to canonical.
Read-only enforcement
For queryable connectors, read-only is defense in depth, not a convention: a read-only role/credential where the engine supports it, a parse-level check onrun_read_only_sql that rejects anything but a single SELECT/WITH…SELECT, and a hard row cap plus statement timeout on every execution. Any layer failing aborts with READ_ONLY_VIOLATION before the query runs.
Schema acquisition ladder
When live introspection is unavailable or partial, canonic descends a priority order. Every tier still emits the sameRelationSchema, tagged with which tier produced it:
- Live introspection: catalog views.
- Modeling code as schema: via a
dbt/definition connector. - Query-history inference (not yet implemented).
- Declarative import: user supplies DDL / a schema export.
- Sample-based inference (not yet implemented).
- Hand-authored
semantics/*.yaml: validated against the live source before being trusted (below).
dbt import still enters reconciliation at provenance inferred, exactly like raw introspection, so it never displaces a human_curated or board_approved fact. Its acquisition tier only breaks ties within the inferred band: when a modeling-code definition and a raw-introspection fact describe the same relation without disagreeing on type, the modeling-code evidence is preferred. Structural fields (grain, joins, measures) still always require review regardless of confidence. See a worked example. A fact only reaches human_curated through an explicit canonic review curate, never automatically from a higher acquisition tier.
Partial capability is never silent: if only some relations are introspectable, the gap is reported rather than omitted.
Schema validation probe
Whenever a schema is acquired declaratively or hand-authored (tiers 4–6), canonic issues a zero-data, read-only probe (SELECT <declared columns> FROM <relation> WHERE false) against the live source before trusting the evidence. A mismatch returns SCHEMA_MISMATCH with a diff of missing/extra columns and type conflicts, never a silent accept.