-
Notifications
You must be signed in to change notification settings - Fork 1
fix: oban lifeline plugin errors #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughQuery result normalisation is adjusted across transactional and non-transactional paths to ensure consistent column and row handling. Parameter encoding is enhanced to transform Elixir types into SQLite-compatible representations, whilst result decoding incorporates normalisation logic for improved ecto_sql compatibility. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/ecto_libsql/native.ex (1)
800-815: Fixexecute_with_transactionto returnrows: []for 0-row writes, notrows: nil.The execute path inconsistently returns
rows: nilfor all INSERT/UPDATE/DELETE operations without RETURNING, even whennum_rows == 0. The documented design rule (inQuery.decode, line 56) specifies thatrowsshould be nil only for write commands "that affected rows" (i.e., whennum_rows > 0). The query path correctly normalisesrows: niltorows: []for 0-row writes; the execute path should do the same.Proposed fix
else # Use execute_with_transaction for INSERT/UPDATE/DELETE without RETURNING - # columns MUST be [] (not nil), rows should be nil + # columns MUST be [] (not nil); rows should be nil only when rows were affected case execute_with_transaction(trx_id, conn_id, statement, args_for_execution) do num_rows when is_integer(num_rows) -> result = %EctoLibSql.Result{ command: command, columns: [], - rows: nil, + rows: if(num_rows > 0, do: nil, else: []), num_rows: num_rows }
🤖 Fix all issues with AI agents
In @lib/ecto_libsql/query.ex:
- Around line 53-83: The decode/3 function can let non-list :rows pass through
and later crash; update decode/3 (the clauses handling columns, cmd, rows,
num_rows) to coerce rows to a list when it's non-nil and not a list (e.g. treat
any non-list rows as [rows] or convert via List.wrap semantics) before applying
the existing cond that sets nil for write commands with no RETURNING, and also
change the comment header text from "Normalize results" to British/Australian
spelling "Normalise results" to match guidelines.
- Around line 41-44: The encode/3 clause currently assumes params is a list and
calls Enum.map, which crashes for nil or non-list params; add a guarded clause
so def encode(_query, params, _opts) when is_list(params) do Enum.map(params,
&encode_param/1) end and provide a fallback def encode(_query, params, _opts)
when not is_list(params) do params end (or otherwise return the unmodified
params) to avoid raising before the NIF; reference the encode/3 function and
encode_param/1 helper.
🧹 Nitpick comments (2)
lib/ecto_libsql/native.ex (2)
707-718: Normalisation logic looks right, but fix en-AU spelling (“Normalise”).
This block aligns with the goal (columns always list; rows nil only for successful writes without RETURNING). The only change I’d request here is comment spelling per guidelines.
774-785: Same: behaviour is good; consider extracting a shared normaliser.
The query-path normalisation matches the non-transactional path; to avoid future drift, consider a small private helper (even just within this module) used by both blocks.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
lib/ecto_libsql/native.exlib/ecto_libsql/query.ex
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ex,exs}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ex,exs}: Use British/Australian English for all Elixir code comments and documentation (except SQL keywords and compatibility requirements)
Always format Elixir code before committing usingmix format --check-formattedUse camelCase for function names in Elixir (e.g.,
defmodule,def handle_execute)
Files:
lib/ecto_libsql/native.exlib/ecto_libsql/query.ex
lib/ecto_libsql/native.ex
📄 CodeRabbit inference engine (CLAUDE.md)
Provide NIF stubs and safe wrapper functions in the Native module for all Rust NIF functions
Files:
lib/ecto_libsql/native.ex
**/*.{ex,exs,rs,md}
📄 CodeRabbit inference engine (CLAUDE.md)
When ending a work session, complete the mandatory workflow: file issues for remaining work, run quality gates (tests, linters, builds), update issue status, commit changes with
git commit, clean up stashes, and provide context for next session
Files:
lib/ecto_libsql/native.exlib/ecto_libsql/query.ex
**/*.ex
📄 CodeRabbit inference engine (AGENTS.md)
**/*.ex: Use camelCase for variable names in Elixir modules
All database connection errors and query failures must be handled gracefully with pattern matching on{:ok, ...}and{:error, ...}tuples - never use bare unwrap operations
Use prepared statements with automatic statement caching for repeated queries to achieve ~10-15x performance improvement
Use batch operations (batch_transactional or execute_batch_sql) for bulk inserts/updates instead of individual queries
Named parameters (:name,@name,$namesyntax) are preferred over positional parameters (?) for readability and maintainability in complex queries
Use savepoints for partial rollback within transactions to enable error recovery patterns in batch operations
Use cursor streaming for large result sets to avoid loading entire datasets into memory
JSON functions (extract, type, set, replace, insert, remove, patch) should be used for efficient JSON/JSONB manipulation in LibSQL 3.45.1+
Enable foreign key constraints withEctoLibSql.Pragma.enable_foreign_keys/1in production environments
Use WAL (Write-Ahead Logging) mode for better concurrency in production:EctoLibSql.Pragma.set_journal_mode(state, :wal)
Files:
lib/ecto_libsql/native.exlib/ecto_libsql/query.ex
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to lib/ecto/adapters/libsql/connection.ex : Add SQLite function support by defining expression handlers for native SQLite functions
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to native/ecto_libsql/src/**/*.rs : Validate transaction ownership by checking that `entry.conn_id == conn_id` before performing transaction operations
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to lib/ecto/adapters/libsql.ex : Implement type dumpers for converting Ecto types to SQLite values using `dumpers/2` function
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to lib/ecto/adapters/libsql.ex : Implement type loaders for converting SQLite values to Ecto types using `loaders/2` function
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-08T04:56:27.393Z
Learning: Applies to priv/repo/migrations/*.exs : Use STRICT tables in migrations for type safety when database schema stability is critical
📚 Learning: 2026-01-06T06:36:01.585Z
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to native/ecto_libsql/src/**/*.rs : Validate transaction ownership by checking that `entry.conn_id == conn_id` before performing transaction operations
Applied to files:
lib/ecto_libsql/native.ex
📚 Learning: 2026-01-06T06:36:01.585Z
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to lib/ecto/adapters/libsql/connection.ex : Add SQLite function support by defining expression handlers for native SQLite functions
Applied to files:
lib/ecto_libsql/query.ex
📚 Learning: 2026-01-06T06:36:01.585Z
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to lib/ecto/adapters/libsql.ex : Implement type dumpers for converting Ecto types to SQLite values using `dumpers/2` function
Applied to files:
lib/ecto_libsql/query.ex
📚 Learning: 2026-01-06T06:36:01.585Z
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to lib/ecto_libsql/native.ex : Provide NIF stubs and safe wrapper functions in the Native module for all Rust NIF functions
Applied to files:
lib/ecto_libsql/query.ex
📚 Learning: 2026-01-06T06:36:01.585Z
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T06:36:01.585Z
Learning: Applies to lib/ecto/adapters/libsql.ex : Implement type loaders for converting SQLite values to Ecto types using `loaders/2` function
Applied to files:
lib/ecto_libsql/query.ex
📚 Learning: 2026-01-08T04:56:27.394Z
Learnt from: CR
Repo: ocean/ecto_libsql PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-08T04:56:27.394Z
Learning: Applies to **/*.ex : All database connection errors and query failures must be handled gracefully with pattern matching on `{:ok, ...}` and `{:error, ...}` tuples - never use bare unwrap operations
Applied to files:
lib/ecto_libsql/query.ex
🧬 Code graph analysis (1)
lib/ecto_libsql/native.ex (1)
native/ecto_libsql/src/transaction.rs (1)
execute_with_transaction(283-307)
| # Convert Elixir types to SQLite-compatible values before sending to NIF | ||
| def encode(_query, params, _opts) do | ||
| Enum.map(params, &encode_param/1) | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file structure and locate the query.ex file
find . -name "query.ex" -type fRepository: ocean/ecto_libsql
Length of output: 87
🏁 Script executed:
# Let's read the file to see the encode function and surrounding context
cat -n lib/ecto_libsql/query.ex | head -100Repository: ocean/ecto_libsql
Length of output: 3588
🏁 Script executed:
# Let's also check if there are any tests or documentation about the encode function
rg "def encode" -A 5 lib/Repository: ocean/ecto_libsql
Length of output: 448
🏁 Script executed:
# Search for DBConnection documentation or any comments about the encode contract
rg "DBConnection\|encode.*params" lib/ecto_libsql/ -B 2 -A 2Repository: ocean/ecto_libsql
Length of output: 43
🏁 Script executed:
# Check if there are any other database adapters or examples that show how params are handled
rg "Enum\.map.*params" lib/ -B 3 -A 3Repository: ocean/ecto_libsql
Length of output: 1327
🏁 Script executed:
# Search for DBConnection.Query protocol definition or documentation
rg "DBConnection\.Query" lib/ --type elixir -B 2 -A 5Repository: ocean/ecto_libsql
Length of output: 897
🏁 Script executed:
# Look for test files that might show how encode is called
find . -name "*test*.exs" -type f | head -5Repository: ocean/ecto_libsql
Length of output: 210
🏁 Script executed:
# Check if there's any documentation about DBConnection contract
rg "encode.*_query.*params" . --type elixir -B 3 -A 3 | head -50Repository: ocean/ecto_libsql
Length of output: 459
🏁 Script executed:
# Search for how params are passed to encode in this adapter
rg "DBConnection\." lib/ecto_libsql/ --type elixir -B 2 -A 2Repository: ocean/ecto_libsql
Length of output: 990
🏁 Script executed:
# Look for the defimpl DBConnection.Query usage and see if there are type specs
ast-grep --pattern 'defimpl DBConnection.Query do
$$$
end'Repository: ocean/ecto_libsql
Length of output: 3323
🌐 Web query:
DBConnection.Query encode/3 callback signature params contract
💡 Result:
Signature/contract:
- Callback: encode(query, params, opts) :: any(). This is called before execute to encode the params for the query. If it returns encoded params, those are used; if it raises DBConnection.EncodeError the query will be prepared again. [1][2][3]
Notes / common expectations:
- Types are left as "any()" by DBConnection — the concrete encoded shape is adapter-specific (e.g. Postgres/Postgrex or MyXQL encode routines). Implementations typically return the params transformed into the adapter's expected format (or raise DBConnection.EncodeError on invalid input). [2][3]
Sources:
- DBConnection.Query (encode/3) docs. [hexdocs.pm/db_connection/DBConnection.Query.html]. [1]
- DBConnection EncodeError and callback discussion (behaviour examples). [hexdocs.pm/db_connection/DBConnection.EncodeError.html; blog/example]. [2]
- Adapter examples (Postgrex/MyXQL) showing encode usage/expectations. [postgrex docs; MyXQL example]. [3]
🌐 Web query:
Elixir DBConnection.Query protocol documentation encode
💡 Result:
The DBConnection.Query protocol defines encode/3 as:
- Spec: encode(query, params, opts) :: any().
- Purpose: encode parameters for a query before execution. If encode/3 raises DBConnection.EncodeError the query will be prepared again.
- Related functions in the protocol: parse/2, describe/2, decode/3.
Reference documentation: DBConnection.Query (hexdocs). [1]
(DBConnection shows encode is invoked when preparing/executing and handles EncodeError by re-preparing.) [2]
Sources:
[1] DBConnection.Query — db_connection (protocol docs).
[2] DBConnection implementation showing encode usage and EncodeError handling (lib/db_connection.ex).
🏁 Script executed:
# Check if there are any guards used elsewhere in encode implementations
rg "def encode.*when" lib/Repository: ocean/ecto_libsql
Length of output: 43
🏁 Script executed:
# Look for how params are used in the codebase - is it always passed as a list?
rg "encode\(" lib/ --type elixir -B 2 -A 2 | head -40Repository: ocean/ecto_libsql
Length of output: 1395
🏁 Script executed:
# Check test files to see how encode is called
head -100 test/ecto_adapter_test.exsRepository: ocean/ecto_libsql
Length of output: 3127
🏁 Script executed:
# Check how params flow through the system - search for places where encode is called or params are prepared
rg "execute\|prepare" lib/ecto_libsql/ --type elixir -B 2 -A 2 | head -60Repository: ocean/ecto_libsql
Length of output: 43
🏁 Script executed:
# Look for DBConnection usage patterns to understand param handling
rg "DBConnection\." lib/ecto_libsql/connection.ex --type elixir -B 3 -A 3 2>/dev/null | head -80Repository: ocean/ecto_libsql
Length of output: 43
🏁 Script executed:
# Check what Ecto adapters typically do - look at existing implementations
rg "def execute" lib/ --type elixir -B 2 -A 5 | head -60Repository: ocean/ecto_libsql
Length of output: 3014
🏁 Script executed:
# Check the main connection execution flow to see how params reach encode
cat -n lib/ecto_libsql.ex | head -150Repository: ocean/ecto_libsql
Length of output: 6261
🏁 Script executed:
# Search for where encode is invoked via DBConnection
rg "cast_params\|prepared\|DBConnection\." lib/ecto_libsql.ex --type elixir -B 2 -A 3 | head -80Repository: ocean/ecto_libsql
Length of output: 43
🏁 Script executed:
# Look at similar Ecto adapters to understand if params guard is common practice
web_search: "Ecto adapter SQLite encode params guard DBConnection"Repository: ocean/ecto_libsql
Length of output: 112
🏁 Script executed:
# Check if there are any existing nil or non-list handling patterns in the codebase
rg "is_list" lib/ --type elixir -B 1 -A 1Repository: ocean/ecto_libsql
Length of output: 4534
🏁 Script executed:
# Examine how query execution happens - trace the flow from Ecto to encode
cat -n lib/ecto_libsql.ex | grep -A 20 "def execute"Repository: ocean/ecto_libsql
Length of output: 43
🏁 Script executed:
# Let me trace through handle_execute to see how params flow to encode
cat -n lib/ecto_libsql.ex | sed -n '148,200p'Repository: ocean/ecto_libsql
Length of output: 2210
🏁 Script executed:
# Check if DBConnection.Query protocol is documented in the repo or if there are type specs
rg "@spec encode" lib/ --type elixir -B 2 -A 2Repository: ocean/ecto_libsql
Length of output: 43
🏁 Script executed:
# Check the complete flow - does DBConnection guarantee args is a list before calling encode?
rg "handle_execute\|def encode" lib/ecto_libsql.ex --type elixir -B 1 -A 10 | head -80Repository: ocean/ecto_libsql
Length of output: 43
🏁 Script executed:
# Verify: are there any error cases or edge paths where encode could receive non-list?
rg "encode\(" lib/ecto_libsql.ex --type elixir -B 3 -A 3Repository: ocean/ecto_libsql
Length of output: 43
🌐 Web query:
DBConnection Elixir encode callback receives list params or generic?
💡 Result:
Short answer: generic — the encode callback takes params() :: any(), not specifically a list. DBConnection passes whatever your adapter/query uses (lists, maps, tuples, etc.), and the DBConnection.Query.encode implementation for your adapter defines how they’re turned into iodata. [1][2]
🏁 Script executed:
# Final verification: confirm that the spelling issue exists at line 53
sed -n '53p' lib/ecto_libsql/query.exRepository: ocean/ecto_libsql
Length of output: 112
🏁 Script executed:
# Verify the proposed fix aligns with codebase patterns by checking similar encode-like functions
rg "def.*(_.*params.*_opts)" lib/ --type elixir -A 2 | head -40Repository: ocean/ecto_libsql
Length of output: 224
Add guard to encode/3 to prevent crashes on non-list params.
The DBConnection.Query protocol specifies encode/3 receives params as any() type, not guaranteed to be a list. Enum.map/2 will raise if params is nil or a non-list, causing failures before the NIF layer. The codebase already uses is_list() guards elsewhere (e.g., normalise_args_for_query/2), confirming this edge case was anticipated.
Proposed fix
- def encode(_query, params, _opts) do
- Enum.map(params, &encode_param/1)
- end
+ def encode(_query, params, _opts) when is_list(params) do
+ Enum.map(params, &encode_param/1)
+ end
+
+ def encode(_query, params, _opts), do: params📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Convert Elixir types to SQLite-compatible values before sending to NIF | |
| def encode(_query, params, _opts) do | |
| Enum.map(params, &encode_param/1) | |
| end | |
| # Convert Elixir types to SQLite-compatible values before sending to NIF | |
| def encode(_query, params, _opts) when is_list(params) do | |
| Enum.map(params, &encode_param/1) | |
| end | |
| def encode(_query, params, _opts), do: params |
🤖 Prompt for AI Agents
In @lib/ecto_libsql/query.ex around lines 41 - 44, The encode/3 clause currently
assumes params is a list and calls Enum.map, which crashes for nil or non-list
params; add a guarded clause so def encode(_query, params, _opts) when
is_list(params) do Enum.map(params, &encode_param/1) end and provide a fallback
def encode(_query, params, _opts) when not is_list(params) do params end (or
otherwise return the unmodified params) to avoid raising before the NIF;
reference the encode/3 function and encode_param/1 helper.
| # Normalize results for ecto_sql compatibility. | ||
| # Rules: | ||
| # 1. columns MUST ALWAYS be a list (even empty []), NEVER nil | ||
| # 2. rows should be nil only for write commands without RETURNING that affected rows | ||
| # 3. For all other cases (SELECT, RETURNING queries), rows must be a list | ||
| def decode(_query, result, _opts) when is_map(result) do | ||
| columns = case Map.get(result, :columns) do | ||
| nil -> [] | ||
| cols when is_list(cols) -> cols | ||
| _ -> [] | ||
| end | ||
|
|
||
| cmd = Map.get(result, :command) | ||
| rows = Map.get(result, :rows) | ||
| num_rows = Map.get(result, :num_rows, 0) | ||
|
|
||
| rows = cond do | ||
| # Write commands that affected rows but have no RETURNING -> rows should be nil | ||
| cmd in [:insert, :update, :delete] and rows == [] and num_rows > 0 and columns == [] -> | ||
| nil | ||
| # All other cases: rows must be a list | ||
| rows == nil -> | ||
| [] | ||
| true -> | ||
| rows | ||
| end | ||
|
|
||
| result | ||
| |> Map.put(:columns, columns) | ||
| |> Map.put(:rows, rows) | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tighten decode/3 row normalisation + fix en-AU spelling in comments.
Two points:
- If
:rowscomes back as a non-list (unexpected but possible given NIF/map boundaries), it currently passes through and may explode later when enumerated. - Comments use “Normalize”; guidelines ask for British/Australian English (“Normalise”).
Proposed fix
- # Normalize results for ecto_sql compatibility.
+ # Normalise results for ecto_sql compatibility.
# Rules:
# 1. columns MUST ALWAYS be a list (even empty []), NEVER nil
# 2. rows should be nil only for write commands without RETURNING that affected rows
# 3. For all other cases (SELECT, RETURNING queries), rows must be a list
def decode(_query, result, _opts) when is_map(result) do
columns = case Map.get(result, :columns) do
nil -> []
cols when is_list(cols) -> cols
_ -> []
end
cmd = Map.get(result, :command)
rows = Map.get(result, :rows)
num_rows = Map.get(result, :num_rows, 0)
rows = cond do
# Write commands that affected rows but have no RETURNING -> rows should be nil
cmd in [:insert, :update, :delete] and rows == [] and num_rows > 0 and columns == [] ->
nil
# All other cases: rows must be a list
- rows == nil ->
+ rows == nil ->
[]
+ not is_list(rows) ->
+ []
true ->
rows
end🤖 Prompt for AI Agents
In @lib/ecto_libsql/query.ex around lines 53 - 83, The decode/3 function can let
non-list :rows pass through and later crash; update decode/3 (the clauses
handling columns, cmd, rows, num_rows) to coerce rows to a list when it's
non-nil and not a list (e.g. treat any non-list rows as [rows] or convert via
List.wrap semantics) before applying the existing cond that sets nil for write
commands with no RETURNING, and also change the comment header text from
"Normalize results" to British/Australian spelling "Normalise results" to match
guidelines.
I'm not well versed enough in elixir to make this a proper PR, so if you feel like it, I'm happy to learn how to make this proper.
In case you don't want it, feel free to throw it away.
I added Oban to a small project and ran into a few issues:
Ecto migrations:
Datetime handling:
And Oban's Lifeline plugin:
As best I could understand:
columnswas set tonilfor write commands, but Ecto always expects a listrowswasnilfor UPDATE...RETURNING with 0 matching rows, but Ecto expects a listWonderful AI quick fixes were, (sorry if they're trash 😅):
encode/3: Convert DateTime/NaiveDateTime/Date/Time/Decimal to ISO8601/string (NIF can't serialize these)decode/3:columnsis always[],rowsisnilfor successful writes without RETURNING, otherwise[]do_query/7anddo_execute_with_trx/6:columnsis always[],rowsisnilfor successful writes without RETURNING, otherwise[]Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.