Code Conversion - Druid Functional Differences¶
This page provides a reference for the functional differences reported when Apache Druid SQL and native queries are translated to Snowflake equivalents. Each entry describes the behavior that changes, shows the generated code, and lists the checks to perform on the converted query.
SSC-FDM-DR0001¶
EARLIEST/LATEST order by the Druid ‘__time’ column. Verify the ‘__time’ column exists in Snowflake and preserves the intended ordering.
Description¶
Druid’s EARLIEST and LATEST return the value associated with the smallest or largest __time value, using that column implicitly. They are translated to Snowflake MIN_BY and MAX_BY with __time supplied as an explicit ordering argument, so the result depends on that column being migrated and ordering the rows as it did in Druid.
The optional second argument of EARLIEST and LATEST is Druid’s maxBytesPerValue storage hint and is dropped during translation.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Confirm that
__timewas migrated with the same name, and that its Snowflake type preserves the precision the ordering depends on. - Check for ties on
__time. When several rows share a timestamp,MIN_BYandMAX_BYmay return any of them, so add a tie-breaker if the source relied on a stable pick.
SSC-FDM-DR0002¶
Druid STRING_AGG does not guarantee element order; a WITHIN GROUP (ORDER BY) clause was added for deterministic output, which may change the concatenation order.
Description¶
Druid’s STRING_AGG concatenates values in an unspecified order. It is translated to Snowflake LISTAGG with a WITHIN GROUP (ORDER BY expression) clause added so the output is deterministic. That ordering is derived from the aggregated expression, so the resulting sequence can differ from what the Druid query happened to produce. A DISTINCT quantifier in the source is carried through to the LISTAGG argument list.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Replace the generated ordering expression when the business logic expects a specific sequence, such as chronological order by
__time. - Treat downstream comparisons of the concatenated string as suspect. Snapshot tests that pinned a Druid-produced order will need to be rebaselined.
SSC-FDM-DR0003¶
Druid ARRAY_CONTAINS with an array argument tests subset containment; the translated Snowflake ARRAY_CONTAINS tests element equality, so results may differ.
Description¶
When its second argument is an array, Druid’s ARRAY_CONTAINS (and the equivalent MV_CONTAINS) tests whether every element of that array is present in the first argument. The call is translated to Snowflake ARRAY_CONTAINS with the arguments flipped, but Snowflake tests whether a single value is an element of an array rather than performing subset containment, so the two can disagree.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Restore subset semantics explicitly, for example by requiring
ARRAY_CONTAINSto hold for each searched element, or by comparingARRAY_SIZE(ARRAY_INTERSECTION(...))against the size of the searched array. - Single-value
ARRAY_CONTAINScalls are unaffected; only the array-argument form needs review.
SSC-FDM-DR0004¶
Druid ARRAY_SLICE returns NULL for an out-of-bounds start; the translated Snowflake ARRAY_SLICE returns an empty array for a positive start past the end and counts a negative start from the end, so results may differ.
Description¶
Druid allows ARRAY_SLICE (and MV_SLICE) to omit the end index, which slices to the end of the array. Snowflake ARRAY_SLICE requires the end, so ARRAY_SIZE(array) is supplied. The two engines then diverge on out-of-bounds starts: Druid returns NULL, while Snowflake returns an empty array for a positive start past the end and interprets a negative start as an offset from the end.
This difference is reported when the start is a literal that can be shown to be out of bounds. A start given as a column cannot be range-checked at conversion time, so no marker is emitted in that case.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Guard the call when downstream logic distinguishes
NULLfrom an empty array, for example with aCASEthat reproduces Druid’sNULLresult for out-of-range starts. - Review column-driven start expressions too. They are not flagged, but the same divergence applies whenever the runtime value falls outside the array bounds.
SSC-FDM-DR0005¶
Druid REGEXP_LIKE performs a partial match; the pattern was wrapped to emulate this under Snowflake’s full-string match. Verify anchored patterns, newlines, and Java-vs-Snowflake regex differences.
Description¶
Druid’s REGEXP_LIKE succeeds when the pattern matches anywhere in the input, whereas Snowflake REGEXP_LIKE requires the pattern to match the entire string. The pattern is therefore wrapped as .*(<pattern>).* — folded into a single literal when the pattern is a literal, or built with concatenation when it is not.
The wrapper reproduces the partial-match behavior, but it does not reconcile the underlying regex dialects: Druid uses Java regular expressions, and anchors, newline handling, and Java-specific constructs can still behave differently.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Re-examine patterns that already contained
^or$. Combining them with the wrapper changes what the expression accepts. - Test against multiline values. The
.metacharacter does not match newlines by default, so a pattern that matched in Druid may fail on multiline input.
SSC-FDM-DR0006¶
Druid PARSE_LONG returns NULL for non-integer text; Snowflake TRY_TO_NUMBER may instead round decimals, trim whitespace, or parse scientific notation. Verify inputs that are not plain integers.
Description¶
PARSE_LONG with no radix, or with an explicit radix of 10, is translated to Snowflake TRY_TO_NUMBER. Druid returns NULL for any text that is not a plain integer, whereas TRY_TO_NUMBER is more permissive: it can round decimal values, tolerate surrounding whitespace, and parse scientific notation.
The radix-16 path uses a hexadecimal format model and does not exhibit this leniency, so it carries no marker.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Add an integer-only validation, such as a
REGEXP_LIKEguard, when the source logic depended onNULLbeing returned for malformed values. - Pay particular attention to rows counted or filtered by
IS NULL. Values that Druid rejected may now parse successfully and change those counts.
SSC-FDM-DR0007¶
Druid TIME_CEIL returns the input unchanged when it is already on a unit boundary; Snowflake TIME_SLICE(…, ‘END’) always advances to the next boundary.
Description¶
Snowflake has no DATE_CEIL function, so Druid’s TIME_CEIL is translated to TIME_SLICE with the END boundary. The two agree for timestamps inside a bucket, but differ exactly on a boundary: Druid returns the input unchanged, while TIME_SLICE(..., 'END') advances to the start of the next bucket.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Add an exact-boundary check when the difference matters, for example returning the input unchanged when it already equals its floored value.
- Assess how often boundary values actually occur. Data captured at whole hours or midnight hits this case frequently; irregular event timestamps rarely do.
SSC-FDM-DR0008¶
ARRAY_INTERSECTION returns distinct elements, so duplicates and order may not be preserved.
Description¶
Druid’s MV_FILTER_ONLY keeps the elements of an array that appear in a supplied list, preserving duplicates and position. It is translated to Snowflake ARRAY_INTERSECTION, which retains the same value set but returns distinct elements, so multiplicity and ordering can change.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Review consumers that read by index or count elements, since both depend on the duplicates and positions that are not preserved.
- Use a
FILTERlambda that tests membership per element when the original multiplicity and order must be kept.
SSC-FDM-DR0009¶
MV_FILTER_REGEX uses Java regex syntax; the translated Snowflake REGEXP_INSTR uses a different regex dialect, so some patterns may match differently.
Description¶
MV_FILTER_REGEX keeps the array elements matching a Java regular expression. It is translated to a Snowflake FILTER lambda that tests each element with REGEXP_INSTR. The filtering structure is equivalent, but the regex dialects are not identical, so individual patterns can match differently.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Look for Java-specific constructs such as possessive quantifiers,
\\p{...}character classes, or named groups, which need rewriting for Snowflake. - Validate the translated pattern against a sample of real array values rather than only against the literal used in the query.
SSC-FDM-DR0010¶
Druid CAST(x AS CHAR(n)) was rewritten to RPAD(CAST(x AS VARCHAR), n, ‘ ‘). Druid pads short strings with spaces to length n; Snowflake CHAR(n) does not pad. The rewrite preserves padding semantics.
Description¶
Druid pads a CHAR(n) result with trailing spaces up to length n, while Snowflake CHAR(n) performs no padding. To preserve the source behavior, the cast is rewritten as RPAD over a VARCHAR cast. The rewrite applies to every explicit length, including CHAR(1).
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Check equality comparisons, joins, and hashes on the result, because the retained trailing spaces are significant in Snowflake.
- Drop the
RPADwrapper where the padding was incidental rather than required by a downstream fixed-width consumer.
SSC-FDM-DR0011¶
Druid bare CAST(x AS CHAR) (no length) was rewritten to CAST(x AS VARCHAR). Snowflake’s bare CHAR is CHAR(1) and rejects multi-character input; Druid’s bare CHAR is unbounded.
Description¶
A bare CHAR in Druid is an unbounded string type, whereas Snowflake treats bare CHAR as CHAR(1), which rejects multi-character input. The cast is therefore rewritten to VARCHAR so that longer values continue to convert successfully.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Accept the rewrite in most cases; it is what keeps multi-character values from failing the cast.
- Add an explicit
VARCHAR(n)only when a downstream table or interface enforces a maximum length.
SSC-FDM-DR0012¶
Druid CAST(x AS BOOLEAN) returns a LONG value (0/1); Snowflake returns a native BOOLEAN (TRUE/FALSE). Logical truth is preserved, but code that compares to 1/0 or concatenates the result will see different values.
Description¶
The cast syntax passes through unchanged, but the result type does not: Druid produces a numeric LONG of 0 or 1, while Snowflake produces a native BOOLEAN. Predicates keep working because the logical truth value is the same, but expressions that treat the result as a number or a string will see TRUE and FALSE instead of 1 and 0.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Replace comparisons such as
= 1or= 0with direct boolean predicates. - Cast explicitly where the numeric or string form is required, for example
CAST(... AS BOOLEAN)::INTfor arithmetic, and check reports or exports that displayed 1 and 0.
SSC-FDM-DR0013¶
Druid CAST(x AS DECIMAL/NUMERIC) was rewritten to CAST(x AS FLOAT)
Description¶
Druid treats DECIMAL and NUMERIC as approximate types backed by a double, so the cast is rewritten to Snowflake FLOAT to match that runtime behavior. Any declared precision and scale is dropped, and the result is subject to floating-point rounding rather than exact decimal arithmetic. The rewrite also applies when the operand is NULL.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Replace the cast with
NUMBER(precision, scale)for monetary or otherwise exact calculations, where floating-point rounding is not acceptable. - Review equality comparisons and sums over many rows, which are the places where accumulated floating-point error becomes visible.
SSC-FDM-DR0014¶
Druid PARTITIONED BY <granularity> specifies ingestion-time segment bucketing; the translated Snowflake INSERT omits the clause and relies on micro-partitions, so time-bucketed segmentation is not preserved.
Description¶
In Druid ingestion DML, PARTITIONED BY <granularity> buckets the ingested rows into time-based segments. Snowflake organizes table storage into micro-partitions automatically, so the clause is dropped from the generated INSERT and this difference is reported.
PARTITIONED BY ALL (and its ALL TIME synonym) is the Druid sentinel for “do not time-partition”, so it is inert and produces no marker.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Rely on automatic micro-partitioning first. Snowflake prunes on the time column without any explicit partitioning clause.
- Consider clustering on the time column only if query profiles show poor pruning on a large table.
SSC-FDM-DR0015¶
Druid CLUSTERED BY pre-sorts rows within each ingested segment by the listed keys; the translated Snowflake INSERT omits the clause and makes no within-segment ordering guarantee.
Description¶
CLUSTERED BY pre-sorts rows inside each Druid segment by the listed keys. The generated Snowflake INSERT omits the clause and makes no guarantee about the physical ordering of the inserted rows, so this difference is reported. The clause is dropped whether the keys are plain columns or expressions, and a statement using both PARTITIONED BY <granularity> and CLUSTERED BY carries this marker alongside SSC-FDM-DR0014.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Add an explicit
ORDER BYto any query whose output order previously depended on the ingestion sort. - Evaluate a Snowflake clustering key on the same columns when the goal was scan pruning rather than result ordering.
SSC-FDM-DR0017¶
Druid applies the granularity timeZone to bucket boundaries, but the translated Snowflake DATE_TRUNC/TIME_SLICE aligns to calendar/epoch boundaries, so bucket edges may differ.
Description¶
A native query granularity may carry a timeZone, which Druid uses to place the bucket boundaries. The translation converts the timestamp with CONVERT_TIMEZONE before applying DATE_TRUNC or TIME_SLICE, but those functions align to calendar or epoch boundaries, so the resulting bucket edges can differ from Druid’s. The difference is reported as a leading comment on the generated query.
Code Example¶
Input Code:¶
Druid¶
Output Code:¶
Snowflake¶
Best Practices¶
- Compare bucket totals around daylight-saving transitions, where the boundary placement is most likely to diverge.
- Check granularities that do not divide evenly into a calendar unit, since epoch-aligned slicing and Druid’s period boundaries drift apart fastest there.