Skip to content
adapters.io

Convert MySQL data types to Snowflake: the full mapping table, the query that proves each risky column before you load it, and the two places Snowflake's own connectors disagree

12 min read Migration The Adapters team

Last updated September 2026

Field mapping auto-plugged · tap a port to rewire

5 sample records ready

Most MySQL types have an obvious Snowflake target and a connector will pick it for you without complaint. The full mapping table is below. The problem is the handful that produce valid DDL, load without a single error, reconcile perfectly on row counts, and change what your data means. MySQL BOOL is really TINYINT and arrives in Snowflake as a number, and the two engines disagree about whether the value 2 is true. This page gives every mapping, then gives the actual query that proves each risky one is safe, run against MySQL before the load rather than against Snowflake three weeks after go-live.

Key takeaways

  • TINYINT(1) is not a boolean and does not become one. It arrives as INT. MySQL evaluates 2 = TRUE as false because TRUE is an alias for 1; Snowflake treats every non-zero number as true. Same predicate, different row count.
  • BIT becomes a hexadecimal string. Not a boolean. Snowflake's SQL Server connector maps that database's BIT straight to BOOLEAN, so one vendor answers one concept two ways.
  • A DECIMAL past 38 digits loses precision quietly. The MySQL connector documents lost precision; the SQL Server connector answers the same overflow by storing TEXT, which at least breaks loudly.
  • DATETIME and TIMESTAMP are correctly mapped to different types. That one is right, and it means two MySQL types that look interchangeable get different time zone behavior in Snowflake.
  • Spatial types are not on the mapping list at all, so they take the documented TEXT default with no warning.
  • Verify on the source, before the load. Every query in the second table runs against MySQL, where the evidence still exists.

How does MySQL to Snowflake data type mapping work?

Sort the columns into three piles. Pile one converts cleanly and needs no thought: DATE, TIME, VARCHAR, TEXT, the ordinary integers, BINARY and JSON all have honest equivalents, and JSON is genuinely better in Snowflake than it was in MySQL. Pile two is not on the mapping list at all, and Snowflake tells you what happens: any unlisted type is mapped to TEXT by default. Pile three is the dangerous one. Those columns convert, load and behave differently, and nothing in the migration will mention it.

Pile three is invisible to the tooling for a structural reason. A connector can tell you that a type has no target, because that is a fact about its own mapping table. It cannot tell you that a column which converted perfectly well was carrying a meaning the target will not preserve, or that a flag column now answers a filter differently. Those are facts about your data and your application, and the only place the evidence still exists is the MySQL instance you are about to stop querying.

There is a second reason to check rather than trust, and it is unusual enough to be worth stating plainly. Snowflake publishes a data mapping page for its MySQL connector and another for its SQL Server connector. They are the same product family. They do not answer a decimal overflow the same way, and they do not answer a bit column the same way. Neither page acknowledges the other. A published mapping is a starting position, not a fact, even when the vendor publishing it owns the destination.

MySQL to Snowflake data type mapping table

The target column is what Snowflake publishes for its own MySQL connector. Every row was checked against the MySQL 8.4 Reference Manual and the Snowflake SQL reference on 6 September 2026. Rows marked "see below" are the ones that look equivalent and are not.

MySQL Snowflake Note
DECIMAL(M,D) / NUMERIC NUMBER Clean to 38 digits. MySQL allows 65. See the verification table below
TINYINT INT The 1 byte range stops being enforced. Harmless on its own
TINYINT(1) / BOOL / BOOLEAN INT Your booleans arrive as numbers, and 2 changes sides. See below
SMALLINT / MEDIUMINT / INT INT All collapse into NUMBER(38,0). Three range guards stop being enforced
BIGINT INT Clean, including UNSIGNED. Snowflake holds it more exactly than MySQL does. See below
YEAR INT The 1901 to 2155 range and the two digit input rules are no longer enforced
BIT(M) TEXT A hexadecimal string, not a boolean. The SQL Server connector maps BIT to BOOLEAN. See below
FLOAT / DOUBLE FLOAT Both become 64 bit. Snowflake documents roughly 15 digits and warns about rounding
DATE DATE Clean, unless zero dates exist in the source. See the verification table below
DATETIME TIMESTAMP_NTZ Correct. MySQL performs no zone conversion on DATETIME, so no zone is honest
TIMESTAMP TIMESTAMP_TZ Also correct, and deliberately different from the row above. Ends at 2038. See below
TIME TIME Clean as a clock time. MySQL also allows -838 to 838 hours as a duration, which does not survive
CHAR(n) TEXT Snowflake states trailing spaces are not preserved. Padded comparisons behave differently
VARCHAR(n) TEXT Clean. The declared length constraint disappears and becomes an application concern
TINYTEXT / TEXT / MEDIUMTEXT TEXT All three land in one type. A storage distinction MySQL made stops carrying meaning
LONGTEXT TEXT Supported to 16 MB by default against a 4 GB source ceiling. Raisable. See below
ENUM TEXT The allowed value list is gone. Nothing in Snowflake rejects an out of range value
SET TEXT A comma separated string in column declaration order, not a set. See below
BINARY / VARBINARY BINARY Clean. Compare on length as well as content, since padding arrives as stored
TINYBLOB / BLOB BINARY Clean. Both sit comfortably inside the connector default
MEDIUMBLOB / LONGBLOB BINARY Supported to 8 MB by default against a 4 GB source ceiling. See below
JSON VARIANT The one place Snowflake is genuinely better. Requires binlog_row_value_options left empty
GEOMETRY / POINT / POLYGON TEXT Not in the published mapping at all, so they take the unlisted default. See below
Anything unlisted TEXT Snowflake states unlisted MySQL types are mapped to TEXT by default, with no warning

Eight risky mappings, and the query that proves each one is safe

This is the part no mapping table carries. For each risky conversion below, there is a query you can run against MySQL today, before you load anything, that tells you whether the risk is theoretical or whether it is already sitting in your data. Run all eight in an afternoon. They are the cheapest work in the entire migration.

DECIMAL wider than Snowflake can hold

MySQL documents a DECIMAL maximum of 65 digits with up to 30 after the point. Snowflake caps NUMBER at 38 digits. Snowflake's MySQL connector says only that "precision is lost when exceeded", while its SQL Server connector answers the same overflow by storing the value as TEXT. One returns a shorter number that still adds up.

SELECT table_name, column_name, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_schema = DATABASE()
  AND data_type = 'decimal'
  AND numeric_precision > 38;

How to read the result: Every row returned is a column where a load will succeed and the number will be shorter than it was. Declare NUMBER(38,s) by hand for each one, choosing the scale from the real magnitude rather than the declared width.

TINYINT(1) and the value 2

MySQL states BOOL and BOOLEAN are synonyms for TINYINT(1), that non-zero is true, but that the constants TRUE and FALSE are aliases for exactly 1 and 0, so 2 = TRUE evaluates to false. Snowflake converts every non-zero number to TRUE. Openflow maps the column to INT and faithfully preserves the value, so the data is right and the answer changes.

SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = DATABASE()
  AND column_type = 'tinyint(1)';

-- then, for each column returned:
SELECT is_active, count(*) FROM orders GROUP BY is_active;

How to read the result: If the second query returns only 0, 1 and NULL, cast to BOOLEAN in the target and move on. If anything else appears, and on a schema more than a few years old it usually does, you have a decision to make per column before the load, not after somebody queries it.

BIT columns used as flags

Openflow maps MySQL BIT to TEXT and states it is "represented as a hexadecimal string". BIT(1) is a common way to spell a yes or no flag in MySQL. The same vendor's SQL Server connector maps that database's BIT straight to BOOLEAN, so one concept lands as two different types from one connector suite.

SELECT table_name, column_name, column_type
FROM information_schema.columns
WHERE table_schema = DATABASE()
  AND data_type = 'bit';

How to read the result: Each row is a column where a boolean filter written in Snowflake will match no rows rather than raise an error. That reads on a dashboard as a metric that fell to zero, not as a broken pipeline. Cast these in the target DDL.

Zero dates

MySQL accepts 0000-00-00 as a date under some SQL modes, and legacy schemas are full of them because that was the idiomatic way to spell an unknown date before NULL handling settled down. Snowflake has no representation for a zero date at all.

SELECT count(*) FROM orders
WHERE created_at = '0000-00-00 00:00:00'
   OR delivered_on = '0000-00-00';

How to read the result: Any non-zero count is a decision you have to make deliberately: these become NULL, or a sentinel date, or the rows are corrected on the source. Deciding during the load, under time pressure, is how a sentinel like 1970-01-01 ends up in a revenue report.

TIMESTAMP columns holding future dates

MySQL documents the TIMESTAMP range as ending at 2038-01-19 03:14:07 UTC, while DATETIME runs to the year 9999. This is usually described as a problem for 2038, and it is not. It is a problem today for any column that stores a future date: subscription end dates, contract expiry, warranty terms, amortization schedules.

SELECT count(*) FROM subscriptions
WHERE expires_at > '2037-01-01';

How to read the result: A non-zero count means the source type is already close to a ceiling the target does not have. Migrating is the cheapest moment you will ever get to change the source column to DATETIME, because you are rewriting the schema anyway.

Large objects against the connector default

Snowflake documents LONGTEXT as supported to 16 MB by default and MEDIUMBLOB and LONGBLOB to 8 MB, against a MySQL ceiling of 4 GB for each. Both limits are raisable and neither is raised for you, so the affected rows are precisely your largest documents and attachments.

SELECT max(length(body)) AS max_text_bytes FROM articles;
SELECT max(length(file_data)) AS max_blob_bytes FROM attachments;

How to read the result: Compare against 16777216 for text and 8388608 for binary. If either maximum is close, raise the connector limit before the first load rather than discovering it on the one row that mattered. Large binaries usually belong in object storage with a URL in the table anyway.

SET and ENUM columns

MySQL stores a SET as a bitmask. Snowflake receives "a comma-separated string in column declaration order", so the ordering is a property of DDL written years ago rather than of the data. ENUM loses its allowed value list entirely, and nothing in Snowflake rejects a value that MySQL would have refused at write time.

SELECT table_name, column_name, data_type, column_type
FROM information_schema.columns
WHERE table_schema = DATABASE()
  AND data_type IN ('set','enum');

How to read the result: For each SET column, decide now whether downstream code parses the string or whether the values belong in a bridge table. For each ENUM, rebuild the allowed list as a CHECK constraint in Snowflake, which is enforced on standard tables.

Types that are not on the mapping list

Openflow is explicit that any MySQL type not in its table is mapped to TEXT by default. MySQL spatial types, GEOMETRY, POINT, LINESTRING and POLYGON, appear nowhere in that table, so they take the default with no warning at any point. The neighboring SQL Server connector is at least explicit that its spatial values "are inserted as NULL".

SELECT data_type, count(*) AS columns
FROM information_schema.columns
WHERE table_schema = DATABASE()
GROUP BY data_type
ORDER BY columns;

How to read the result: Read the whole list against the published mapping table above. Anything that does not appear there is arriving as a string. This is a five minute query that routinely finds a spatial or generated column nobody remembered was in the schema.

How do I convert a MySQL TINYINT(1) to a Snowflake boolean?

Query the distinct values first, then cast deliberately in the target DDL. If a column holds only 0, 1 and NULL, a cast to BOOLEAN is safe and you should make it, because leaving booleans as integers pushes the ambiguity into every query written afterwards. If the column holds anything else, you have to decide what that value means before you choose a cast, and the decision is a business one rather than a technical one.

The reason this matters more than it sounds is that MySQL and Snowflake genuinely disagree. MySQL's manual is explicit that BOOL and BOOLEAN are synonyms for TINYINT(1), that zero is false and non-zero is true, and that the constants TRUE and FALSE are aliases for exactly 1 and 0. So in MySQL, 2 = TRUE is false. Snowflake's conversion rules say that zero converts to FALSE and any non-zero value converts to TRUE. So in Snowflake, that same row is true. A filter that excluded it now includes it, on data that migrated perfectly, with no error on either side.

There is a related detail worth knowing while you are in there. MySQL has deprecated the display width attribute on integer types outright, and its manual states that the width is unrelated to the range of values a column can store. TINYINT(1) has never meant a one-bit column. It has always been a full byte, and any value from -128 to 127 is legal in it, which is exactly how the value 2 got there in the first place.

Does Snowflake use MySQL?

No. Snowflake is a columnar analytic warehouse with its own storage engine and its own SQL dialect. It is not a MySQL fork and it is not MySQL compatible in the way that, say, MariaDB is. Ordinary queries move across largely unchanged because both follow the SQL standard for the common cases, and that is precisely what makes the differences dangerous: the parts that break are loud and get fixed, while the parts that quietly mean something else are the type system, the boolean handling and constraint enforcement.

The constraint point deserves its own sentence. Snowflake accepts PRIMARY KEY, UNIQUE and FOREIGN KEY definitions on standard tables, records them as metadata the optimizer can use, and does not enforce them. Only NOT NULL and CHECK are always enforced. Coming from MySQL with InnoDB foreign keys switched on, that is a bigger change than the type mapping, and it shows up first as double-counted revenue after a pipeline replays a batch. Land every ingestion path in a staging table and MERGE on the business key rather than appending.

Why do my row counts match but my numbers are wrong?

Because every failure mode on this route preserves row counts. A DECIMAL that lost precision is still one row. A flag that became a hex string is still one row. A spatial column that arrived as text is still one row. Row-count reconciliation is the check almost everyone runs and it is the check that cannot detect anything described on this page.

Reconcile on values instead. Per table, compare the row count, the SUM of every numeric column, MIN and MAX of every date, a count grouped by day, and a count grouped by each boolean flag. That last one takes a minute to write and is the check that catches the TINYINT problem, because it is the only comparison where the MySQL answer and the Snowflake answer differ on data that copied perfectly.

Should I use DATETIME or TIMESTAMP before migrating to Snowflake?

DATETIME, for almost everything, and standardize before you migrate rather than after. Snowflake's connector maps DATETIME to TIMESTAMP_NTZ and TIMESTAMP to TIMESTAMP_TZ, and both of those are correct: MySQL documents that it converts TIMESTAMP values to UTC for storage and back on retrieval, and that this does not happen for DATETIME. The mapping is faithfully reproducing a real difference.

The trouble is that the difference is invisible in application code. Two columns in the same table, both holding what a developer thinks of as a date and time, land in two Snowflake types with different time zone semantics, and any query that compares them has to reconcile that. MySQL TIMESTAMP also stops at 2038-01-19, which bites today rather than in twelve years, because contract and subscription end dates are stored ahead of time. Picking one type per table during the migration costs an afternoon and removes a class of bug permanently.

What MySQL data types does Snowflake not support?

There is no formal unsupported list, which is a different and slightly worse situation than a vendor publishing one. Snowflake states that any MySQL type not in its mapping table is mapped to TEXT by default. The types that fall through that gap in practice are the spatial family, GEOMETRY, POINT, LINESTRING and POLYGON, along with anything exotic a long-lived schema accumulated. They arrive as strings, every spatial function you were using stops existing, and nothing warns you.

Two further cases behave like unsupported types even though they map. MySQL TIME legally holds a duration from -838 to 838 hours as well as a clock time, and only the clock time survives. And MySQL's SET is stored as a bitmask but arrives as a comma-separated string in column declaration order, so anything that treated it as a set is now parsing a string whose ordering was decided by DDL somebody wrote years ago.

What this costs if you get it wrong

Not much in tooling, and quite a lot in reloads. Every mistake in this article is found after a load and fixed by running the load again, and a migration backfill is the most expensive thing you will ever ask a warehouse to do. On row or event based pipeline pricing it is the largest month you will have; on Snowflake credits it is a sustained spike in warehouse time. Budget for at least three full loads and you will probably use two. Teams that watch cloud and SaaS spend as it accrues notice the third reload while it is running rather than on the invoice five weeks later, which is usually the difference between a conversation and an incident.

The eight queries in the table above are the cheapest insurance available against all of it. They run against a database you already have, they need no new tooling, and they take an afternoon. Every one of them turns a question you would otherwise answer after go-live into a decision you make while it is still free to change your mind.

The full tool comparison for this route, including what each vendor bills by and the binlog prerequisite that rules Snowflake's own connector out on some managed hosts, is on our MySQL to Snowflake migration tools page, and the step by step cutover sequence is in the MySQL to Snowflake migration guide. The equivalent treatment for the other routes out of MySQL is in convert MySQL data types to PostgreSQL, and the neighboring connector this page compares against is covered in convert SQL Server data types to Snowflake. If MySQL is staying alive alongside the warehouse, the ongoing case is in Snowflake ETL tools, and the budget model behind either program is in what a data migration really costs.

Once the data is in Snowflake, keep it in agreement with everything else

Map the fields once, run the backfill, then let the same mapping run incrementally with retries, alerts and per-record logs. From $49 a month, never metered by rows.

Try the live demo

No credit card required.