Skip to main content
A connector declares capabilities, not identity. canonic’s core dispatches on capability (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:
An unregistered 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. A credentials_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:
Caching and refresh live once, in the layer that wraps every provider, so no provider reimplements expiry arithmetic and no connector reimplements caching. An unregistered name raises 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)

Implement introspect_schema + run_read_only_sql: these feed the semantic layer and are executed against directly by canonic query / canonic sql.

Definition

Implement extract_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

Implement extract_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.
This list isn’t closed: any wiki or knowledge base (Confluence, Jira, internal docs tools, etc.) fits the same 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. From extract_definitions.
  • DocEvidence, a title + body + candidate topic references, with a usage_hint that maps to a knowledge page’s usage_mode. From extract_evidence doc 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.
Unmappable native constructs are recorded with a warning, never silently dropped.

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 on run_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 same RelationSchema, tagged with which tier produced it:
  1. Live introspection: catalog views.
  2. Modeling code as schema: via a dbt/definition connector.
  3. Query-history inference (not yet implemented).
  4. Declarative import: user supplies DDL / a schema export.
  5. Sample-based inference (not yet implemented).
  6. Hand-authored semantics/*.yaml: validated against the live source before being trusted (below).
Acquisition tier and provenance are independent axes. A tier-2 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.