Skip to content

CORE-1210: optimize insert_rows / render_row_to_sql Jinja path (Fusion artifact upload slowdown)#1041

Open
haritamar wants to merge 3 commits into
masterfrom
CORE-1210-optimize-insert-rows-render-path
Open

CORE-1210: optimize insert_rows / render_row_to_sql Jinja path (Fusion artifact upload slowdown)#1041
haritamar wants to merge 3 commits into
masterfrom
CORE-1210-optimize-insert-rows-render-path

Conversation

@haritamar

@haritamar haritamar commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Artifact upload (DBT_COLUMNS, ~65k rows) spends minutes in Jinja building INSERT … VALUES, dramatically worse under dbt Fusion (MiniJinja FFI overhead per macro/dict boundary). Customer trace: ~8m15s in render_row_to_sql across 65,126 rows while the warehouse inserts took only ~45s.

This reworks the hot path in macros/utils/table_operations/insert_rows.sql so the per-row loop does only a dict lookup + value render, moving all repeated work out of the loop. No change to the emitted SQL shape (still INSERT … VALUES) or to insert semantics across adapters.

What changed

(1) Remove per-row duration instrumentation. render_row_to_sql no longer calls begin/end_duration_measure_context (was ~65k stack push/pop + utcnow() + long name concat per row). Measurement stays at get_insert_rows_queries / insert_rows level.

(2) Hoist column-level work out of the loop into a new get_row_render_metadata(columns, sample_row), computed once per insert:

column_meta[i] = {
  is_created_at:  column.name|lower == "created_at",
  row_key:        <actual dict key resolved once from the first row>,
  normalized_type: normalize_data_type(column.dtype),   # was called per cell
}
created_at_sql = edr_current_timestamp()                 # was called per created_at cell

This removes ~900k normalize_data_type → data_type_list adapter.dispatch calls for the customer case.

(3) Drop insensitive_get_dict_value in this path. row_key is resolved once (exact / lower / upper against the first row — flatten emits consistent keys), then the loop does a direct row.get(row_key).

(4) Resolve adapter.dispatch once per insert. render_value and escape_special_chars impls are resolved once and reused. render_value / default__render_value gained an optional escaper param so the pre-resolved escaper is threaded through instead of re-dispatching per string cell (default behavior unchanged when escaper is omitted, for other callers).

(5) Fix quadratic chunk assembly. Instead of current_query.data + "," + row_sql on a growing multi-MB string, each chunk accumulates row SQL in a list and joins once:

base_insert_query + (chunk.rows | join(","))

Length tracking is done incrementally (namespace counter) so no giant string is rebuilt to check query_max_size. Existing chunking semantics preserved: query_max_size, chunk_size, and on_query_exceed oversized-row handling.

Correctness / tests

  • Existing integration_tests/tests/test_dbt_artifacts suite passes on Postgres (46 passed, 4 skipped), including test_artifacts_collection_in_multiple_row_batches (forces small query_max_size → multiple chunks).
  • Added test_insert_rows.py + test_render_insert_rows_queries macro covering the rendered VALUES output: single-quote escaping, numbers, nulls, nested mapping/sequence via tojson, and chunk splitting.
  • The full warehouse matrix (incl. Fusion on Snowflake/BigQuery/Databricks) runs in CI on this PR.

Link to Devin session: https://app.devin.ai/sessions/11d70ed2cdfe4ae182c112e4135de8d3
Requested by: @haritamar

Summary by CodeRabbit

  • New Features

    • Enhanced SQL insert generation to reliably handle mixed data types, including strings, numbers, nulls, and nested/JSON-like values.
    • Improved configurable chunking so large inserts split into multiple queries more consistently.
  • Bug Fixes

    • Strengthened adapter-aware escaping and literal rendering (including special characters) for safer, more consistent inserts.
    • Kept correct timestamp handling and maintained oversized-row insert behavior.
  • Tests

    • Added dbt macro coverage and pytest assertions for rendered values, escaping, null handling, nested serialization, and chunking behavior.

Hoist per-column work out of the per-row loop, drop per-row duration
measuring and case-insensitive dict lookups, resolve adapter.dispatch
once per insert, and build chunk queries via list join instead of
growing-string concat.

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
@haritamar haritamar self-assigned this Jul 23, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@linear

linear Bot commented Jul 23, 2026

Copy link
Copy Markdown

CORE-1210

@github-actions

Copy link
Copy Markdown
Contributor

👋 @haritamar
Thank you for raising your pull request.
Please make sure to add tests and document all user-facing changes.
You can do this by editing the docs files in the elementary repository.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f89bd145-b2b6-4558-b491-ee2390f6dcfb

📥 Commits

Reviewing files that changed from the base of the PR and between 11be565 and d84117a.

📒 Files selected for processing (1)
  • macros/utils/table_operations/insert_rows.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • macros/utils/table_operations/insert_rows.sql

📝 Walkthrough

Walkthrough

The insert-row query generator now caches rendering metadata and dependencies, renders rows through helper macros, and assembles chunked SQL statements. Integration tests cover SQL literal formatting, JSON serialization, combined rows, and row-level chunking.

Changes

Insert row rendering

Layer / File(s) Summary
Rendering pipeline
macros/utils/table_operations/insert_rows.sql
get_insert_rows_queries precomputes rendering state, uses row-rendering helpers, supports oversized rows, and joins rendered tuples into query chunks. render_value accepts a reusable escaper for string and JSON literals.
Integration validation
integration_tests/dbt_project/macros/test_render_insert_rows_queries.sql, integration_tests/tests/test_dbt_artifacts/test_insert_rows.py
Adds a dbt operation macro and tests for mixed-type literal formatting, nested values, combined rows, and chunk_size=1 output.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant dbt_operation
  participant get_insert_rows_queries
  participant render_row_to_sql
  participant render_value
  dbt_operation->>get_insert_rows_queries: provide columns, rows, and chunk settings
  get_insert_rows_queries->>render_row_to_sql: render each row with cached metadata
  render_row_to_sql->>render_value: convert column values to SQL literals
  get_insert_rows_queries-->>dbt_operation: return chunked insert queries
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: optimizing the insert_rows/render_row_to_sql Jinja path to address Fusion artifact upload slowdown.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch CORE-1210-optimize-insert-rows-render-path

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@integration_tests/tests/test_dbt_artifacts/test_insert_rows.py`:
- Line 22: Update the assertions in the test around the query checks at lines 22
and 39 to derive the expected escaped O’Brien value using the active adapter’s
escaping behavior, rather than hardcoding SQL quote-doubling. Ensure the
expectation matches elementary.escape_special_chars for each warehouse in the
matrix.

In `@macros/utils/table_operations/insert_rows.sql`:
- Around line 215-239: Update get_row_render_metadata and its callers so row_key
resolution is performed for each row rather than derived once from rows[0] and
reused across the batch. Preserve the existing exact, lowercase, and uppercase
key matching for every row, ensuring mixed key casing does not produce null
values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4185b170-4b46-4600-b449-ceaaa53d6168

📥 Commits

Reviewing files that changed from the base of the PR and between ccb0de3 and f12de01.

📒 Files selected for processing (3)
  • integration_tests/dbt_project/macros/test_render_insert_rows_queries.sql
  • integration_tests/tests/test_dbt_artifacts/test_insert_rows.py
  • macros/utils/table_operations/insert_rows.sql

Comment thread integration_tests/tests/test_dbt_artifacts/test_insert_rows.py Outdated
Comment on lines +215 to +239
{% macro get_row_render_metadata(columns, sample_row) %}
{% set column_meta = [] %}
{% for column in columns %}
{% set is_created_at = column.name | lower == "created_at" %}
{% set row_key = none %}
{% if not is_created_at and sample_row is not none %}
{% if column.name in sample_row %} {% set row_key = column.name %}
{% elif column.name | lower in sample_row %}
{% set row_key = column.name | lower %}
{% elif column.name | upper in sample_row %}
{% set row_key = column.name | upper %}
{% endif %}
{% endif %}
{% do column_meta.append(
{
"is_created_at": is_created_at,
"row_key": row_key,
"normalized_type": elementary.normalize_data_type(
column.dtype
),
}
) %}
{% endfor %}
{% do return(column_meta) %}
{% endmacro %}

@coderabbitai coderabbitai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether row-key casing is guaranteed consistent by all row-producing
# (flatten) macros that feed into insert_rows/get_insert_rows_queries.
rg -n 'insensitive_get_dict_value' --type sql -C2
rg -n 'def flatten|macro flatten' --type sql -C3
rg -n 'get_insert_rows_queries|insert_rows\(' --type sql -g '!macros/utils/table_operations/insert_rows.sql'

Repository: elementary-data/dbt-data-reliability

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- insert_rows.sql outline ---'
ast-grep outline macros/utils/table_operations/insert_rows.sql --view expanded || true

echo
echo '--- relevant search ---'
rg -n --hidden --glob '!**/.git/**' \
  'insensitive_get_dict_value|get_row_render_metadata|render_row_to_sql|get_insert_rows_queries|flatten' \
  macros . || true

Repository: elementary-data/dbt-data-reliability

Length of output: 50392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file size ---'
wc -l macros/utils/table_operations/insert_rows.sql

echo
echo '--- around referenced lines ---'
sed -n '1,320p' macros/utils/table_operations/insert_rows.sql | nl -ba | sed -n '180,280p'

echo
echo '--- callers and helpers ---'
rg -n --hidden --glob '!**/.git/**' \
  'get_insert_rows_queries|render_row_to_sql|insensitive_get_dict_value|sample_row|row_key|flatten' \
  macros

Repository: elementary-data/dbt-data-reliability

Length of output: 315


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- insert_rows.sql (lines 101-280) ---'
sed -n '101,280p' macros/utils/table_operations/insert_rows.sql

echo
echo '--- insensitive_get_dict_value.sql ---'
cat -n macros/utils/dict_utils/insensitive_get_dict_value.sql

Repository: elementary-data/dbt-data-reliability

Length of output: 7997


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- callers of get_insert_rows_queries / insert_rows ---'
rg -n --hidden --glob '!**/.git/**' 'get_insert_rows_queries\(|insert_rows\(' macros

echo
echo '--- top of insert_rows.sql ---'
sed -n '1,120p' macros/utils/table_operations/insert_rows.sql

echo
echo '--- flatten_* macros that return list/dict rows ---'
rg -n --hidden --glob '!**/.git/**' 'return\(.*flatten|do return\(flatten|append\(flat|extend\(flatten' macros/edr macros/utils

Repository: elementary-data/dbt-data-reliability

Length of output: 7730


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- upload_artifacts_to_table.sql ---'
sed -n '1,140p' macros/edr/dbt_artifacts/upload_artifacts_to_table.sql

echo
echo '--- upload_dbt_invocation.sql ---'
sed -n '1,140p' macros/edr/dbt_artifacts/upload_dbt_invocation.sql

echo
echo '--- handle_tests_results.sql ---'
sed -n '1,120p' macros/edr/tests/on_run_end/handle_tests_results.sql

echo
echo '--- create_model_baseline_table.sql ---'
sed -n '1,120p' macros/edr/tests/test_utils/create_model_baseline_table.sql

echo
echo '--- replace_table_data.sql ---'
sed -n '1,220p' macros/utils/table_operations/replace_table_data.sql

Repository: elementary-data/dbt-data-reliability

Length of output: 22458


Keep key lookup independent of the first row. row_key is derived from rows[0] and reused for every row, so any batch that mixes key casing will silently write null for later rows. Normalize per row or make the casing invariant explicit at the call sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@macros/utils/table_operations/insert_rows.sql` around lines 215 - 239, Update
get_row_render_metadata and its callers so row_key resolution is performed for
each row rather than derived once from rows[0] and reused across the batch.
Preserve the existing exact, lowercase, and uppercase key matching for every
row, ensuring mixed key casing does not produce null values.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the one-time key map here intentionally. Every caller of get_insert_rows_queries feeds rows produced by the flatten_* artifact macros (e.g. flatten_model, flatten_column, flatten_test), which emit a fixed, literal set of keys — the same key casing for every row in a batch. Because the keys are consistent across rows, resolving row_key once from the first row is equivalent to the old per-row insensitive_get_dict_value, and resolving it once is exactly what CORE-1210 asks for (the per-cell case-insensitive lookup was a large part of the Fusion slowdown). Reintroducing a per-row/per-cell 3-way casing check would re-add the FFI cost the ticket is trying to remove. If a future caller ever passes rows with heterogeneous key casing this would need revisiting, but that isn't possible with the current flatten-based inputs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside - CodeRabbit - does the above response make sense?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@haritamar, yes—the response makes sense for the current call paths and the stated CORE-1210 performance goal.

If all inputs are produced by the flatten_* macros and those macros guarantee the same literal key casing for every row in a batch, then deriving row_key from the first row is behaviorally equivalent to the previous per-cell case-insensitive lookup. Reintroducing that lookup in the hot loop would undermine the optimization this PR is specifically targeting.

The only caveat is API scope: get_insert_rows_queries itself does not enforce that invariant, so a future caller passing heterogeneous key casing could still get null values. That is an acceptable trade-off if the macro is intentionally internal / constrained to flatten-generated rows. A short contract comment—as already present—helps make that assumption explicit.

So I would treat the original finding as not actionable for this PR, rather than requiring a code change.


✏️ Learnings added
Learnt from: haritamar
Repo: elementary-data/dbt-data-reliability PR: 1041
File: macros/utils/table_operations/insert_rows.sql:215-239
Timestamp: 2026-07-23T17:39:04.799Z
Learning: In `macros/utils/table_operations/insert_rows.sql`, `elementary.get_insert_rows_queries` currently receives rows generated by `flatten_*` artifact macros, whose dictionaries use a fixed, consistent key casing within each batch. Therefore, `elementary.get_row_render_metadata` intentionally resolves each column key once from the first row to avoid per-cell case-insensitive lookup overhead in the CORE-1210 performance-sensitive insert-rendering path.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

escape_special_chars differs per warehouse ('' vs backslash), so derive
the expected quoted literal from the adapter instead of hardcoding the
Postgres-style doubled quote.

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
loop only does a dict lookup + value render. `sample_row` (the first row) is
used to resolve the actual dict key for each column, replacing the per-cell
case-insensitive lookup - flatten macros emit consistent keys across rows. #}
{% macro get_row_render_metadata(columns, sample_row) %}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be get_columns_metadata?
also I think the comment can be removed - it's pretty clear what this macro does.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to get_columns_metadata and removed the comment. Done in d84117a.

{{- adapter.dispatch("render_value", "elementary")(value, data_type) -}}
{# `escaper` lets callers pass a pre-resolved escape_special_chars implementation
so the hot insert path avoids an adapter.dispatch per rendered cell. When it's
not provided (other callers) it is resolved once here, matching prior behavior. #}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove "matching prior behavior", don't document past code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped "matching prior behavior" in d84117a.

{# Accumulate each chunk's rendered rows in a list and join once when the
chunk is finalized, instead of repeatedly concatenating a growing query
string (which is O(n^2)). A namespace is required so reassignments made
inside the loop persist across iterations. #}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in d84117a.

{% set current_chunk_size = namespace(data=0) %}
{# Precompute per-column rendering metadata and adapter implementations once,
hoisted out of the per-row loop (dtypes / created_at handling / dispatch
results are identical for every row). #}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in d84117a.

…ments

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>

{% set current_query = namespace(data=base_insert_query) %}
{% set current_chunk_size = namespace(data=0) %}
{% set column_meta = elementary.get_columns_metadata(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside - getting column metadata once, instead of re-computing it over and over on every row

{% endfor %}
{% set row_sql = "({})".format(rendered_column_values | join(",")) %}

{% do elementary.end_duration_measure_context("render_row_to_sql") %}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside - These begin/end measurements were very expensive by themselves, so we removed them

{% if column.is_created_at %}
{% do rendered_column_values.append(created_at_sql) %}
{% else %}
{% set column_value = elementary.insensitive_get_dict_value(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside - insensitive_get_dict_value replaced with a simple get, as the flattened rows already lower all keys

) %}
{% set created_at_sql = elementary.edr_current_timestamp() %}
{% set render_value_impl = adapter.dispatch("render_value", "elementary") %}
{% set escaper = adapter.dispatch("escape_special_chars", "elementary") %}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside - smaller optimization - pre-compute once and then pass adapter.dispatch to avoid doing this logic on every row.

{% for row in rows %}
{% set row_sql = elementary.render_row_to_sql(row, columns) %}
{% set query_with_row = (
current_query.data + ("," if not loop.first else "") + row_sql

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside - replaced this with collecting an array of all the rows in the chunk and joining once (makes this O(n) instead of O(n^2))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant