# Type resolution

How a column's type survives the trip from source to data platform, covering the three vocabularies involved and the order they are tried in.

## Overview

A `NUMERIC(10,2)` in PostgreSQL has to become something in DuckDB, and a property read from a CSV file has to become something in either. Deciding what is type resolution, and Amfelt does it in the data contract rather than on the wire.

That distinction matters more than it sounds. Data moves between plugins as Apache Arrow record batches, and it is tempting to assume Arrow decides the target type. It does not. Arrow carries the values; the contract decides the DDL.

## Three vocabularies

Every column is described three times on its way to your data platform.

| Vocabulary | Whose words | Example |
| --- | --- | --- |
| Source physical type | The source system's own type names | `NUMERIC(10,2)`, `VARCHAR(100)`, `TIMESTAMPTZ` |
| Open Data Contract Standard (ODCS) logical type | The contract's portable vocabulary | `number`, `string`, `date` |
| Target physical type | Your data platform's own type names | `DECIMAL(10,2)`, `VARCHAR`, `TIMESTAMPTZ` |

The logical vocabulary is deliberately small, seven categories: `string`, `date`, `number`, `integer`, `object`, `array`, and `boolean`. It is the pivot, and its smallness is the point. Mapping every source type directly to every target type is a separate table for each pair of systems; pivoting through seven categories is one mapping per system.

## Dimensions travel separately

Seven categories cannot express `VARCHAR(100)`, so the details ride alongside the category as structured fields rather than inside a type name.

String length is recorded as `maxLength` under the contract's `logicalTypeOptions`, because ODCS defines a key for it. Numeric precision and scale are recorded as custom properties, because ODCS defines no key for them and forbids adding one. Both are read structurally.

What Amfelt will not do is recover a dimension by parsing the source's type name back apart. A `NUMERIC(10,2)` string is a dialect artifact, and reading precision out of it means writing a parser per dialect, which is exactly what structured properties exist to avoid.

## The order types are resolved in

When the target builds a table, it tries four things in order and stops at the first that answers.

1. **Affinity.** A known, high-fidelity mapping for this specific source-and-target pair. Used when it exists, because it preserves the most.
2. **The target's physical type map.** The source's own type name, looked up directly in the target's vocabulary. This is the common case for one database into another: PostgreSQL into PostgreSQL resolves here every time.
3. **The logical pivot.** The contract's logical category, resolved to the target's own type for that category, dimension-matched where the contract carries dimensions, so a 64-bit integer lands as the target's `bigint` rather than its default integer type.
4. **Refusal.** If nothing above resolves, the target refuses to generate DDL for that column and reports an error naming the column and the type. A source-native type name is never allowed to reach `CREATE TABLE`.

Steps 1 and 2 are a fast path for systems that share a type vocabulary. Step 3 is what makes the other cases work at all, and step 4 is deliberate: a column whose type cannot be resolved stops the table from being built, rather than producing DDL that is quietly wrong.

## The case that needs the pivot

A CSV file has no type vocabulary. Every cell is text, so every column arrives claiming a physical type of `string`, which is honest, and useless to a data platform, because it says nothing about what the column holds. Without step 3, every column of every file source would land as text at best, and be refused at step 4 at worst.

On a target with no type named `string`, such as PostgreSQL, the logical pivot is what lets a file source land with real column types: the contract says the column is a `number`, and the target knows its own type for a number. This is why a contract's logical types are worth correcting during review even when the physical types look fine. For a source with no types of its own, the logical type is the only thing carrying the answer.

## Where the actual mappings live

Per-connector type tables are published on each connector's page, generated from the connector itself rather than written by hand. Find yours in the [integrations catalog](/integrations/). Each page carries a searchable table of every source type, its aliases, and the ODCS logical type it maps to.

## Next

- [Write modes and history](/docs/concepts/write-modes-and-history/): what the data platform does with the columns once they exist
- [Versioning and compatibility](/docs/concepts/versioning/): how a *change* to a type is scored, once it has one
- [Review the data contract](/docs/guides/sources/review-the-data-contract/): correcting types before data moves
- [Data contracts](/docs/reference/data-contract/): the full ODCS reference
