Table Structure Conversion
DBConvert Streams automatically converts table structure between source and target when they use different engines or formats. This includes data types, indexes, foreign keys, and check constraints.
Structure conversion applies to both Load and CDC modes.
Supported conversion paths
Database ↔ database
- MySQL → PostgreSQL
- PostgreSQL → MySQL
- MariaDB → PostgreSQL
- PostgreSQL → MariaDB
- MySQL / PostgreSQL → Snowflake
Files / S3 → database
- CSV → MySQL / PostgreSQL
- JSON / JSONL → MySQL / PostgreSQL
- Parquet → MySQL / PostgreSQL
Database → files / S3
- MySQL / PostgreSQL → CSV, JSONL, or Parquet files (local or S3)
Structure options
Structure creation is controlled per-stream via the structureOptions object in the target spec. All options are booleans and default to true.
| Option | Default | Effect |
|---|---|---|
tables | true | Create tables on the target if they do not exist |
indexes | true | Create indexes (primary keys, unique, secondary) |
foreignKeys | true | Create foreign key constraints |
checkConstraints | true | Create check constraints |
Set an option to false to skip that structure type. For example, disabling indexes on large tables can speed up the initial load — you can create them manually afterward.
API example
{
"target": {
"id": "conn_TARGET_ID",
"spec": {
"db": {
"database": "target_db",
"structureOptions": {
"tables": true,
"indexes": false,
"foreignKeys": false,
"checkConstraints": true
}
}
}
}
}
In the UI, the stream wizard Structure & Data step exposes these as checkboxes under the advanced section.
Data type mapping
These tables are generated from the conversion code itself, so they describe what the current release actually does. Each row is one column as it exists in the source — a MySQL database, a PostgreSQL database, or a file — and the columns marked with an arrow are the type it gets on that target.
Moving between two databases of the same engine is not in the tables. MySQL to MySQL, or PostgreSQL to PostgreSQL, recreates each column exactly as it was declared — nothing is mapped. File targets (Parquet, CSV, JSONL) go through DuckDB, so its type is what lands in the file's schema.
Notes say what a mapping cannot carry. No note means nothing is lost.
Snowflake appears in the tables because the mapping exists, but the Snowflake target is still in development.
From a MySQL source
Each row is one MySQL column and what it becomes on each target.
| MySQL column | → PostgreSQL | → Snowflake | → Parquet / CSV / JSONL | Notes |
|---|---|---|---|---|
| TINYINT(1) | BOOLEAN | NUMBER(3,0) | TINYINT | MySQL's boolean |
| TINYINT UNSIGNED | SMALLINT | NUMBER(3,0) | UTINYINT | widened, because a signed TINYINT cannot hold 255 |
| INT | INTEGER | NUMBER(10,0) | INTEGER | |
| INT UNSIGNED | BIGINT | NUMBER(10,0) | UINTEGER | widened |
| BIGINT UNSIGNED | NUMERIC(20,0) | NUMBER(19,0) | UBIGINT | widened |
| DECIMAL(20,6) | NUMERIC(20,6) | NUMBER(20,6) | DECIMAL(20,6) | |
| FLOAT | REAL | FLOAT | FLOAT | |
| DOUBLE | DOUBLE PRECISION | FLOAT | DOUBLE | |
| BIT(1) | BIT(1) | VARIANT | VARCHAR | |
| CHAR(10) | CHAR(10) | VARCHAR(10) | CHAR(10) | |
| VARCHAR(255) | VARCHAR(255) | VARCHAR(255) | VARCHAR(255) | |
| TEXT | TEXT | VARCHAR | VARCHAR | |
| LONGTEXT | TEXT | VARCHAR | VARCHAR | |
| BINARY(16) | BYTEA | BINARY(8388608) | BLOB | |
| BLOB | BYTEA | VARCHAR(25000000) | BLOB | |
| LONGBLOB | BYTEA | VARCHAR(25000000) | BLOB | |
| DATE | DATE | DATE | DATE | |
| DATETIME | TIMESTAMP(0) | TIMESTAMP_NTZ | TIMESTAMP | |
| DATETIME(3) | TIMESTAMP(3) | TIMESTAMP_NTZ | TIMESTAMP | |
| TIMESTAMP | TIMESTAMP(0) | TIMESTAMP_NTZ | TIMESTAMP | MySQL normalizes to UTC on write; the target column is not zone-aware, so that conversion does not follow the data |
| TIME(6) | INTERVAL | TIME | TIME | |
| YEAR | SMALLINT | NUMBER(4,0) | VARCHAR | |
| ENUM | TEXT | VARCHAR | VARCHAR | on PostgreSQL: text plus a CHECK constraint holding the labels |
| SET | TEXT | VARCHAR | VARCHAR | |
| JSON | JSONB | VARIANT | JSON | |
| GEOMETRY | TEXT | GEOGRAPHY | VARCHAR | carried as text; spatial indexes are not recreated |
From a PostgreSQL source
Each row is one PostgreSQL column and what it becomes on each target.
| PostgreSQL column | → MySQL | → Snowflake | → Parquet / CSV / JSONL | Notes |
|---|---|---|---|---|
| SMALLINT | SMALLINT | NUMBER(5,0) | SMALLINT | |
| INTEGER | INT | NUMBER(10,0) | INTEGER | |
| BIGINT | BIGINT | NUMBER(19,0) | BIGINT | |
| NUMERIC(20,6) | DECIMAL(20,6) | NUMBER(20,6) | DECIMAL(20,6) | |
| REAL | FLOAT | FLOAT | FLOAT | |
| DOUBLE PRECISION | DOUBLE | FLOAT | DOUBLE | |
| MONEY | DECIMAL(19,2) | VARIANT | DECIMAL(19,2) | the currency symbol belongs to the server's locale, not the value, so only the amount travels |
| BOOLEAN | TINYINT(1) | BOOLEAN | BOOLEAN | |
| CHAR(10) | CHAR(10) | VARCHAR(10) | CHAR(10) | |
| VARCHAR(255) | VARCHAR(255) | VARCHAR(255) | CHAR(255) | |
| TEXT | LONGTEXT | VARCHAR | VARCHAR | |
| BYTEA | LONGBLOB | VARCHAR(25000000) | BLOB | |
| DATE | DATE | DATE | DATE | |
| TIMESTAMP | DATETIME(6) | TIMESTAMP_NTZ | TIMESTAMP | |
| TIMESTAMPTZ | TIMESTAMP(6) | TIMESTAMP_TZ | TIMESTAMP | |
| TIME | TIME(6) | TIME | TIME | |
| TIMETZ | TEXT | VARIANT | TIMETZ | only PostgreSQL has a zone-aware time; elsewhere the offset is kept in text |
| INTERVAL | VARCHAR(255) | VARCHAR(255) | VARCHAR | |
| UUID | CHAR(36) | VARCHAR(36) | VARCHAR | |
| JSON | JSON | VARIANT | JSON | |
| JSONB | JSON | VARIANT | JSON | |
| TEXT | JSON | VARIANT | VARCHAR | no other engine has array columns; the values are carried as JSON |
| INTEGER | JSON | VARIANT | VARCHAR | carried as JSON |
| ENUM type | ENUM('sad','ok') | VARIANT | VARCHAR | |
| INET | VARCHAR(45) | VARCHAR(45) | VARCHAR | |
| CIDR | TEXT | VARIANT | VARCHAR | |
| MACADDR | VARCHAR(17) | VARCHAR(17) | VARCHAR | |
| BIT(8) | BIT(8) | VARIANT | VARCHAR | |
| TSVECTOR | TEXT | VARIANT | VARCHAR | the searchable form is rebuilt by the target, not copied |
| POINT | TEXT | GEOGRAPHY | VARCHAR | carried as text; spatial indexes are not recreated |
| XML | TEXT | VARIANT | VARCHAR |
From a file source
Each row is one column as DuckDB reports it from a Parquet, CSV or JSON file, and what it becomes on each database target. CSV and JSON have no types of their own, so these are the types inferred from their contents.
| File column type | → MySQL | → PostgreSQL | → Snowflake | Notes |
|---|---|---|---|---|
| BOOLEAN | TINYINT(1) | BOOLEAN | BOOLEAN | |
| TINYINT | TINYINT | SMALLINT | NUMBER(3,0) | |
| UTINYINT | TINYINT UNSIGNED | SMALLINT | NUMBER(3,0) | unsigned; widened where the target has no unsigned type |
| SMALLINT | SMALLINT | SMALLINT | NUMBER(5,0) | |
| INTEGER | INT | INTEGER | NUMBER(10,0) | |
| UINTEGER | INT UNSIGNED | BIGINT | NUMBER(10,0) | unsigned; widened |
| BIGINT | BIGINT | BIGINT | NUMBER(19,0) | |
| UBIGINT | BIGINT UNSIGNED | NUMERIC(20,0) | NUMBER(19,0) | unsigned; widened |
| HUGEINT | TEXT | TEXT | VARIANT | 128-bit; no target has one, so it is carried as text |
| FLOAT | FLOAT | REAL | FLOAT | |
| DOUBLE | DOUBLE | DOUBLE PRECISION | FLOAT | |
| DECIMAL(20,6) | DECIMAL(20,6) | NUMERIC(20,6) | NUMBER(20,6) | precision and scale are carried, not rounded to a float |
| VARCHAR | LONGTEXT | TEXT | VARCHAR | DuckDB strings carry no length, so a length is not invented |
| DATE | DATE | DATE | DATE | |
| TIMESTAMP | DATETIME(6) | TIMESTAMP | TIMESTAMP_NTZ | |
| TIMESTAMP WITH TIME ZONE | TIMESTAMP(6) | TIMESTAMPTZ | TIMESTAMP_TZ | |
| TIME | TIME(6) | TIME | TIME | |
| BLOB | BLOB | BYTEA | VARCHAR(25000000) | |
| UUID | CHAR(36) | TEXT | VARCHAR(36) | |
| INTERVAL | VARCHAR(255) | TEXT | VARCHAR(255) |
Types not listed map to text on a database target and VARCHAR in a file.
File source → database target
When the source is a local file or S3 object, DBConvert Streams uses DuckDB to infer the schema:
- Parquet: schema is read from the file metadata (column names, types, nullability)
- CSV:
read_csv_auto()infers column names from headers and types from sampled values - JSON / JSONL:
read_json_auto()infers structure from the document fields
What each inferred type becomes on a database target is in From a file source above.
Database source → file target
When the target is a local directory or an S3 bucket, columns are written through DuckDB — the Parquet / CSV / JSONL column of the tables above is the type that ends up in the file's schema. Parquet carries those types in its metadata; CSV and JSONL are text formats, so the type shapes how each value is written rather than being stored.
Two properties worth knowing:
- Decimals stay exact. A
DECIMAL(20,6)is written as a decimal, not as a floating-point approximation. - Unsigned integers keep their range. A MySQL
TINYINT UNSIGNEDholding 255 is written as an unsigned type, not as -1.
Output format is determined by the target configuration:
| Format | File extension | Default compression |
|---|---|---|
| CSV | .csv | uncompressed |
| JSONL | .jsonl | uncompressed |
| Parquet | .parquet | zstd |
All formats also support gzip and zstd compression. Parquet additionally supports snappy.
Default value conversion
DBConvert Streams also converts default values between database engines:
- MySQL → PostgreSQL: MySQL zero dates (
0000-00-00) convert toNULL. Boolean defaults0/1convert tofalse/true. - PostgreSQL → MySQL: PostgreSQL sequences (
nextval()) are skipped. Boolean defaultstrue/falseconvert to1/0. Type casts (::type) are removed.
Check constraint conversion
Check constraints are translated between SQL dialects:
- PostgreSQL → MySQL:
ANY(ARRAY[...])converts toIN(...). PostgreSQL functions likejsonb_typeofmap to MySQL equivalents likeJSON_TYPE. - MySQL → PostgreSQL: MySQL functions like
json_validmap to PostgreSQL equivalents likejsonb_typeof. Backtick identifiers convert to double quotes.