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.
Snowflake stores unquoted identifiers in upper case, and canonic quotes every identifier it emits. Relation and column names in a semantic source must therefore match the stored case, for example PUBLIC.ORDERS. Introspection returns names exactly as Snowflake stores them, so sources generated from it work as is. Snowflake has no read-only session flag, so point read_only_role at a role that only holds SELECT grants.

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). An ossie connection has no such fallback. Ossie has no notion of a connection, so target_connection is required and must name a queryable connection.

Ossie mapping

Ossie is read as an interchange format, never as canonic’s own format. Parsing is deterministic, no LLM is involved, and every item enters reconciliation as inferred at the modeling tier. The connector emits no RelationSchema: Ossie types are optional, so column types always come from the live schema of target_connection. The expression variant written in the target’s own dialect is used first, then OSSIE_SQL_2026, then ANSI_SQL, then any other SQL dialect sqlglot can read, transpiled to the target. Expressions only in MDX, TABLEAU, MAQL, DAX, SIGMA or THOUGHTSPOT, and expressions that do not parse, are unmappable. Every unmappable object is named in a warning by canonic connection test and canonic ingest, never dropped silently.
Binding candidates are proposals only, an import never makes a definition canonical. The ingest draft uses the Ossie measures, dimensions, joins and descriptions. A binding candidate becomes a proposed contracts/metrics/<metric>.yaml at inferred, which never auto-applies. A ratio also proposes bindings for its two components, named after their measures, and metric synonyms become binding aliases. A candidate is never proposed over an existing binding, and only when every measure, column and component binding it references will exist after the run. Doc evidence is extracted but not yet drafted into knowledge/. A definition that cannot be placed, such as a measure with unknown additivity or a dimension on a column the live table lacks, is listed under skipped in the ingest output.

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. How far the engine itself enforces read-only differs per connector, and canonic connection test reports it: A connector without engine-side enforcement is still guarded by the parse check. The warning only says that nothing sits behind it.

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.