Skip to content

Fix upsert ON CONFLICT: missing :identifier expr + numbered parameters#95

Open
AlanMcCann wants to merge 1 commit intoocean:mainfrom
AlanMcCann:fix/upsert-on-conflict-identifier
Open

Fix upsert ON CONFLICT: missing :identifier expr + numbered parameters#95
AlanMcCann wants to merge 1 commit intoocean:mainfrom
AlanMcCann:fix/upsert-on-conflict-identifier

Conversation

@AlanMcCann
Copy link
Copy Markdown

@AlanMcCann AlanMcCann commented Apr 27, 2026

Two bugs in SQL generation that cause "near ?: syntax error" on any INSERT ... ON CONFLICT DO UPDATE query:

  1. Missing :identifier expression handler. Ecto generates ON CONFLICT UPDATE clauses using fragment expressions containing {:identifier, _, ["column_name"]} tuples. Without a matching expr/3 clause, these fall through to the catch-all which returns bare "?", producing invalid SQL like: SET "col" = EXCLUDED.?

  2. Bare ? parameter placeholders. SQLite requires numbered positional parameters (?1, ?2, ...) when a statement contains multiple parameter groups (INSERT values + ON CONFLICT UPDATE). The ecto_sqlite3 adapter already uses this pattern.

Fixes all upsert operations including Ash Framework's upsert? actions.

Summary by CodeRabbit

Bug Fixes

  • Enhanced INSERT statement SQL generation for improved parameter handling and consistency across standard inserts and conflict-resolution operations, ensuring more reliable database insert behaviour.

Two bugs in SQL generation that cause "near ?: syntax error" on any
INSERT ... ON CONFLICT DO UPDATE query:

1. Missing :identifier expression handler. Ecto generates ON CONFLICT
   UPDATE clauses using fragment expressions containing {:identifier, _,
   ["column_name"]} tuples. Without a matching expr/3 clause, these fall
   through to the catch-all which returns bare "?", producing invalid SQL
   like: SET "col" = EXCLUDED.?

2. Bare ? parameter placeholders. SQLite requires numbered positional
   parameters (?1, ?2, ...) when a statement contains multiple parameter
   groups (INSERT values + ON CONFLICT UPDATE). The ecto_sqlite3 adapter
   already uses this pattern.

Fixes all upsert operations including Ash Framework's upsert? actions.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Walkthrough

The LibSQL adapter's INSERT SQL generation has been refactored to use numbered positional bind parameters (?1, ?2, etc.) instead of unnumbered placeholders. A new encode_insert_values/2 function renumbers placeholders per row within the VALUES clause, whilst parameter placeholder expressions and the identifier printer have been updated accordingly for consistency.

Changes

Cohort / File(s) Summary
LibSQL Insert Parameter Binding
lib/ecto/adapters/libsql/connection.ex
Updated insert/7 to calculate offset based on existing placeholder count and introduced encode_insert_values/2 to generate numbered bind parameters per row. Enhanced parameter placeholder expressions to emit numbered placeholders. Extended expression printer to support identifier node quoting for fragment and EXCLUDED cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Numbered parameters hop with glee,
Through VALUES clauses, wild and free,
Identifiers quoted, conflicts resolved,
Off CONFLICT schemas now well evolved! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: fixing upsert ON CONFLICT issues by addressing missing :identifier expression handling and implementing numbered parameters for SQLite compatibility.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ecto/adapters/libsql/connection.ex`:
- Around line 1184-1189: The IN with a bound list branch still emits bare "?"
placeholders; update the clause handling the pattern {:in, _, [left, {:^, _, [_,
length]}]} inside expr/3 to emit numbered placeholders like the generic {:^, [],
[ix]} clause does: generate a sequence of "?" + Integer.to_string(start_index)
through "?" + Integer.to_string(start_index + length - 1) (matching the same
numbering scheme used by expr({:^,[],[ix]},...)), join them with ", " and return
the full parenthesized placeholder list so SQLite gets ?1, ?2, ... for IN ^list.
Ensure you reference and reuse the same index base/offset logic used by
expr({:^,...}) so numbering remains consistent across other parameter groups.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f519f1b-e1b3-479f-8e81-0e161b5058ae

📥 Commits

Reviewing files that changed from the base of the PR and between b9d0647 and 6ba8838.

📒 Files selected for processing (1)
  • lib/ecto/adapters/libsql/connection.ex

Comment on lines +1184 to 1189
# Parameter placeholder - use numbered parameters (?1, ?2, ...).
# SQLite requires numbered parameters when a statement has multiple
# parameter groups (e.g., INSERT values + ON CONFLICT UPDATE).
defp expr({:^, [], [ix]}, _sources, _query) do
[?? | Integer.to_string(ix + 1)]
end
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.

⚠️ Potential issue | 🟠 Major

Finish the numbered-placeholder pass for IN ^list.

This change only renumbers the generic {:^, ...} case. The dedicated {:in, _, [left, {:^, _, [_, length]}]} branch below still expands to bare ? placeholders, so queries that use IN ^list can still hit SQLite's numbered-parameter requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ecto/adapters/libsql/connection.ex` around lines 1184 - 1189, The IN with
a bound list branch still emits bare "?" placeholders; update the clause
handling the pattern {:in, _, [left, {:^, _, [_, length]}]} inside expr/3 to
emit numbered placeholders like the generic {:^, [], [ix]} clause does: generate
a sequence of "?" + Integer.to_string(start_index) through "?" +
Integer.to_string(start_index + length - 1) (matching the same numbering scheme
used by expr({:^,[],[ix]},...)), join them with ", " and return the full
parenthesized placeholder list so SQLite gets ?1, ?2, ... for IN ^list. Ensure
you reference and reuse the same index base/offset logic used by expr({:^,...})
so numbering remains consistent across other parameter groups.

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