The Hidden Trap in Snowflake's INFER_SCHEMA
And How SQL Functions Save the Day

Search for a command to run...
And How SQL Functions Save the Day

No comments yet. Be the first to comment.
I've always liked the idea of giving workshop participants an AI coding assistant. Not because it makes the workshop easier to run, but because "learn to work with AI" is a useful skill on its own. Bu

Using a highly experimental dbt package

Most teams I talk to already have a decent dbt project and are now looking at Semantic Views and Cortex Analyst. The question is usually: “How do I reuse what we have in dbt instead of building a sec

In Part 1 I connected OpenClaw to Snowflake Cortex as the LLM backend. Enterprise-grade security, unified billing, data stays in Snowflake. So far so good. But after running it for a while I noticed something: most of the tokens OpenClaw burns throug...

I've been playing around with OpenClaw - an open-source AI assistant framework - and wanted to hook it up to Snowflake's Cortex LLM API. The idea: use enterprise-grade models like Claude Sonnet 4.5 through Snowflake's infrastructure while keeping my ...

I've always been a fan of Snowflake's INFER_SCHEMA() function. Not only because it saves you from manually typing out column definitions but the whole idea of letting Snowflake figure out your schema from actual data feels like the right level of automation. But recently I had to learn its limitations the hard way.
Here's what happened: I loaded a sample file with IDs like 1, 2, 3 — single-digit integers. INFER_SCHEMA() happily inferred NUMBER(1,0) for the column. Makes sense, right? Minimal precision for single digits.
Then production data arrived with IDs in the millions.
Numeric value '1234567' is out of range
What a bummer.
The issue is that INFER_SCHEMA() acts too precise. It looks at your sample data and picks the tightest type that fits. A file with values like 1.5 and 2.3 gets inferred as NUMBER(2,1) — which explodes when 123.456 shows up later.
Here's the thing: when you create a table using USING TEMPLATE, you're not locked into the raw output of INFER_SCHEMA(). The template is just SQL — and you can transform it however you want.
Most tutorials show you this basic pattern:
CREATE TABLE my_table
USING TEMPLATE (
SELECT ARRAY_AGG(OBJECT_CONSTRUCT(*))
FROM TABLE(INFER_SCHEMA(...))
);
So far so good. But that OBJECT_CONSTRUCT(*) is where the magic happens — or doesn't happen if you're just passing everything through unchanged.
You can use any SQL function inside the template to transform column names, types, or other properties. Let me show you what I mean.
Uppercasing column names:
CREATE TABLE my_table
USING TEMPLATE (
SELECT ARRAY_AGG(
OBJECT_CONSTRUCT(
'COLUMN_NAME', UPPER(COLUMN_NAME), -- Force uppercase
'TYPE', TYPE,
'NULLABLE', NULLABLE,
'ORDER_ID', ORDER_ID
)
)
WITHIN GROUP (ORDER BY ORDER_ID)
FROM TABLE(INFER_SCHEMA(...))
);
Why would you want this? Schema evolution creates columns in UPPERCASE. If your initial columns are lowercase from the CSV header but new columns come in as uppercase, you end up with case mismatches. Normalizing upfront avoids the headache.
Broadening numeric types:
Here's the pattern I use now for every INFER_SCHEMA workflow:
CREATE TABLE my_table
USING TEMPLATE (
SELECT ARRAY_AGG(
OBJECT_CONSTRUCT(
'COLUMN_NAME', UPPER(COLUMN_NAME),
'TYPE', CASE
WHEN REGEXP_LIKE(TYPE, 'NUMBER\\([0-9]+,\\s?0\\)') THEN 'NUMBER(38, 0)'
WHEN STARTSWITH(TYPE, 'NUMBER(') THEN 'DOUBLE'
ELSE TYPE
END,
'NULLABLE', NULLABLE,
'ORDER_ID', ORDER_ID
)
)
WITHIN GROUP (ORDER BY ORDER_ID)
FROM TABLE(INFER_SCHEMA(...))
);
Let me break down that CASE statement:
| Pattern | Transformation | Why |
NUMBER(X, 0) | NUMBER(38, 0) | Integers get minimal precision — NUMBER(1,0) for single digits. Broadening to NUMBER(38,0) handles any integer. |
NUMBER(X, Y) | DOUBLE | Decimals like NUMBER(3,2) overflow with larger values. DOUBLE provides flexibility for real-world data. |
| Everything else | Keep as-is | Strings, timestamps, booleans don't have this problem. |
The regex NUMBER\\([0-9]+,\\s?0\\) matches integer types (scale of 0), while STARTSWITH(TYPE, 'NUMBER(') catches any remaining numeric types that have decimal places.
Here's the complete pattern I use for CSV files:
-- Create file format that reads headers
CREATE OR REPLACE FILE FORMAT my_csv_format
TYPE = 'CSV'
PARSE_HEADER = TRUE
ERROR_ON_COLUMN_COUNT_MISMATCH = FALSE;
-- Create table with broadened types
CREATE OR REPLACE TABLE my_table
USING TEMPLATE (
SELECT ARRAY_AGG(
OBJECT_CONSTRUCT(
'COLUMN_NAME', UPPER(COLUMN_NAME),
'TYPE', CASE
WHEN REGEXP_LIKE(TYPE, 'NUMBER\\([0-9]+,\\s?0\\)') THEN 'NUMBER(38, 0)'
WHEN STARTSWITH(TYPE, 'NUMBER(') THEN 'DOUBLE'
ELSE TYPE
END,
'NULLABLE', NULLABLE,
'ORDER_ID', ORDER_ID
)
)
WITHIN GROUP (ORDER BY ORDER_ID)
FROM TABLE(
INFER_SCHEMA(
LOCATION => '@my_stage/data/',
FILE_FORMAT => 'my_csv_format'
)
)
)
ENABLE_SCHEMA_EVOLUTION = TRUE;
The ENABLE_SCHEMA_EVOLUTION = TRUE is critical if you want new columns to be added automatically — but that's a story for the next post.
Good news: Parquet files have type information embedded in their metadata, so the inferred types are much more accurate. You typically don't need the numeric broadening trick.
But you should still uppercase the column names for consistency with schema evolution:
CREATE TABLE my_table
USING TEMPLATE (
SELECT ARRAY_AGG(
OBJECT_CONSTRUCT(
'COLUMN_NAME', UPPER(COLUMN_NAME), -- Still important!
'TYPE', TYPE,
'NULLABLE', NULLABLE,
'ORDER_ID', ORDER_ID
)
)
WITHIN GROUP (ORDER BY ORDER_ID)
FROM TABLE(INFER_SCHEMA(...))
);
INFER_SCHEMA() is fantastic for prototyping and development. But for production, don't trust it blindly — post-process the template using SQL functions to broaden numeric types and normalize column names. Your future self (and your pipelines) will thank you.
Next up: How to combine this pattern with Snowpipe for automatic schema evolution. Stay tuned.