Releases: ocean/ecto_libsql
0.9.1 - more SQL fixes, and an upstream libSQL announcement
[0.9.1] - 2026-05-07
Fixed
- Upsert
ON CONFLICT DO UPDATE SQLGeneration - Fixed two bugs that caused"near ?: syntax error"on anyINSERT ... ON CONFLICT DO UPDATEquery, breaking all upsert operations including Ash Framework'supsert?actions. (Thanks @AlanMcCann for PR #95!)- Missing
:identifierexpression handler: Ecto generatesON CONFLICT UPDATEclauses using{:identifier, _, ["column_name"]}fragment expressions. Without a matchingexpr/3clause these fell through to the catch-all, producing invalid SQL such asSET "col" = EXCLUDED.?. - Bare
?parameter placeholders: SQLite requires numbered positional parameters (?1,?2, …) when a statement contains multiple parameter groups (INSERT values + ON CONFLICT UPDATE). The adapter now uses numbered parameters throughout, consistent with theecto_sqlite3adapter. INclause with bound list parameters was also left using bare?placeholders. TheIN (?, ?, ?)handler now generatesIN (?1, ?2, ?3)with correct start-index offsets, matching the numbering scheme used everywhere else.- Empty
INlist edge case (where: field in ^[]) is handled explicitly withIN (SELECT NULL WHERE 1=0)since SQLite rejectsIN ().
- Missing
- PRAGMA Statement Routing - PRAGMA statements are now correctly routed through the
query()path rather than the execute path, fixing incorrect behaviour when reading PRAGMA values (e.g.PRAGMA journal_mode,PRAGMA synchronous) via the Ecto adapter.
Changed
- Upstream Status Notice - Added a note to the README and Hex documentation about Turso’s transition away from libSQL toward their new Turso library (a full SQLite rewrite in Rust).
ecto_libsqlwill continue to receive bug fixes and security updates; see the README for more context. - Dependency Updates - Bumped
db_connection2.9.0 → 2.10.0,ecto3.13.5 → 3.13.6,jason1.4.4 → 1.4.5,rustler0.37.3 → 0.37.4,libsqlcrates 0.9.29 → 0.9.30, plus various transitive Rust dependency updates. - CI Toolchain - Replaced
erlef/setup-beamwithmisefor Elixir/OTP version management; updated to current Elixir/OTP versions; appliedzizmorGitHub Actions security hardening.
Security
- Acknowledged three additional
rustls-webpki0.102.x advisories (RUSTSEC-2026-0049, RUSTSEC-2026-0098, RUSTSEC-2026-0099, RUSTSEC-2026-0104) incargo deny- all are transitive via libsql’s pinnedrustls0.22.x dependency and cannot be resolved until libsql updates upstream.
New Contributors
- @AlanMcCann made their first contribution in #95
Full Changelog: 0.9.0...0.9.1
0.9.0 - Fixes, and now precompiled NIFs!
[0.9.0] - 2026-02-02
Added
- Precompiled NIF Binaries - Users no longer need Rust, cmake, or pkg-config installed. First compile drops from 5-10 minutes to seconds using prebuilt NIF downloads for 6 targets (2 macOS, 4 Linux). Force local compilation if required with
ECTO_LIBSQL_BUILD=true. (Thanks @ricardo-valero for PR #70!) - GitHub Actions Release Workflow - Automated NIF builds on tag push for all supported targets using
philss/rustler-precompiled-action, with version validation againstmix.exs
Fixed
Repo.exists?Generates Valid SQL - Fixed empty SELECT clause generating invalid SQL (SELECT FROM "users"instead ofSELECT 1 FROM "users"), causing syntax errors. (Thanks @ricardo-valero for PR #69!)- NIF Cross-Compilation Workflow - Fixed multiple issues preventing successful cross-compilation in GitHub Actions:
- Fixed Cargo workspace target directory mismatch — build output goes to the workspace root
target/directory, not the crate subdirectory - Moved
.cargo/config.tomlto workspace root so musl-crt-staticrustflags are found when building from workspace root - Added
Cross.tomlforRUSTLER_NIF_VERSIONenvironment passthrough to cross containers - Consolidated macOS runners to macos-15 (Apple Silicon) for both architectures
- Fixed Cargo workspace target directory mismatch — build output goes to the workspace root
Changed
- Dependency Updates - Bumped
actions/checkoutto v6,actions/upload-artifactto v6, updated Cargo and Credo dependencies
New Contributors
- @ricardo-valero made their first contribution in #69
Full Changelog: 0.8.9...0.9.0
0.8.9 - Fixes are IN
0.8.9 - 2026-01-28
Fixed
- IN Clause with Ecto.Query.Tagged Structs - Fixed issue #63 where
~w()sigil word lists in IN clauses returned zero results due to Tagged struct wrapping. Now properly extracts list values fromEcto.Query.Taggedstructs before generating IN clauses, enabling these patterns to work correctly. - SubQuery Support in IN Expressions - Fixed SubQuery expressions being incorrectly wrapped in
JSON_EACH(), causing invalid SQL. Now properly generates inline subqueries likeWHERE id IN (SELECT s0.id FROM table AS s0 WHERE ...). Fixes compatibility with libraries like Oban that use subqueries in UPDATE...WHERE patterns. (Thanks @nadilas for PR #66 !) - Ecto.Query.Tagged Expression Handling - Fixed type-cast fragments (e.g.
type(fragment(...), :integer)) falling through to catch-all expression handler and generating incorrect parameter placeholders. Now properly handles%Ecto.Query.Tagged{}structs that Ecto's query planner creates from{:type, _, [expr, type]}AST nodes. Fixes parameter count mismatches with Hrana/Turso. (Thanks @nadilas for PR #67 !)
New Contributors
Full Changelog: 0.8.8...0.8.9
0.8.8 - Bug fixes: IN clause, RETURNING clause, not Santa Claus
Fixed
- IN Clause Datatype Mismatch - Fixed issue #63 where IN clauses with parameterised lists caused datatype mismatch errors due to automatic JSON encoding of lists
- SQL Comment Query Detection - Fixed Protocol.UndefinedError when queries start with SQL comments (both
--and/* */styles) by properly skipping comments before detecting query type - RETURNING Clause for update_all/delete_all - Added RETURNING clause generation when using update_all/delete_all with select clauses, fixing Protocol.UndefinedError with Oban job fetching
Changed
- Removed Unsupported Replication Tests - Removed replication integration tests that were testing unsupported features
Full Changelog: 0.8.7...0.8.8
0.8.7 - R*Tree spatial indexing, CHECK constraints, better DEFAULT and type handling
Added
- CHECK Constraint Support - Column-level CHECK constraints in migrations
- R*Tree Spatial Indexing - Full support for SQLite R*Tree virtual tables with 1D-5D indexing, validation, and comprehensive test coverage
- ecto_sqlite3 Compatibility Test Suite - Comprehensive tests ensuring feature parity with ecto_sqlite3
- Type Encoding Improvements - Automatic JSON encoding for plain maps, DateTime/Decimal parameter encoding, improved type coercion
- Comprehensive Type Loader/Dumper Support - Full support for encoding/decoding temporal types (DateTime, NaiveDateTime, Date, Time), Decimal, and special nil values with proper ISO 8601 formatting
- Default Value Type Handling - Support for Decimal, DateTime, NaiveDateTime, Date, Time, and
:nullas default values in migrations with warning logging for unsupported types - Connection Recovery Testing - Test suite for connection failure scenarios and recovery patterns
- Query Encoding Improvements - Explicit test coverage for query parameter encoding with various data types and edge cases
Fixed
- DateTime Microsecond Type Loading - Fixed
:utc_datetime_usec,:naive_datetime_usec, and:time_usecloading from ISO 8601 strings with microsecond precision - Parameter Encoding - Automatic map-to-JSON conversion, DateTime/Decimal encoding for compatibility with Oban and other libraries
- Migration Robustness - Handle
:serial/:bigserialtypes, improved default value handling with warnings for unsupported types - JSON and RETURNING Clauses - Fixed JSON encoding in RETURNING queries and datetime function calls
- Test Isolation - Comprehensive database cleanup across all test suites, per-test table clearing, improved resource management
- DateTime Type Handling - Fixed datetime_decode to handle timezone-aware ISO 8601 strings and nil value encoding for date/time/bool types
- Decimal Type Handling - Updated assertions to accept both numeric and string representations of decimal values in database queries
- Datetime Roundtrip Preservation - Strengthened microsecond precision preservation in datetime round-trip tests
Changed
- Test Suite Consolidation - Streamlined and improved test organisation with better coverage of edge cases, error handling, and concurrent operations
- Documentation - Updated documentation with SQLite-specific query limitations, compatibility testing results, and guidance for type encoding edge cases
Full Changelog: 0.8.6...0.8.7
0.8.6 - JSON, UPSERT, EXPLAIN, and Rust fixes
Release v0.8.6
Major Features
Named Parameters Execution Support
Full support for SQLite named parameter syntax (:name, @name, $name) in prepared statements and direct execution. Parameters are transparently converted from maps to positional arguments internally.
# Use named parameters in queries
{:ok, _, result, state} = EctoLibSql.handle_execute(
"SELECT * FROM users WHERE email = :email AND status = :status",
%{"email" => "alice@example.com", "status" => "active"},
[],
state
)Works seamlessly with prepared statements, transactions, batch operations, and cursor streaming.
EXPLAIN QUERY PLAN Support
Full support for SQLite's EXPLAIN QUERY PLAN via Ecto's Repo.explain/2 and Repo.explain/3. Returns structured query plans for optimisation and debugging.
{:ok, plan} = Repo.explain(:all, from(u in User, where: u.active == true))
# Returns: [%{"id" => 2, "parent" => 0, "notused" => 0, "detail" => "SCAN users"}]CTE (Common Table Expression) Support
Full support for SQL WITH clauses including recursive CTEs. Enables complex hierarchical queries and improved query organisation.
query = "hierarchy"
|> with_cte("hierarchy", as: ^base_query)
|> recursive_ctes(true)
|> select([h], h.name)
Repo.all(query)Query-Based UPSERT Support
Extended on_conflict support to handle query-based updates with keyword list syntax for dynamic operations.
Repo.insert(changeset,
on_conflict: [set: [name: "updated", updated_at: DateTime.utc_now()]],
conflict_target: [:email]
)STRICT Table Option
Added support for SQLite's STRICT table option for stronger type enforcement at INSERT/UPDATE time.
create table(:users, options: [strict: true]) do
add :name, :string
add :age, :integer
endSecurity Enhancements
CVE-2025-47736 Protection
Defence-in-depth measures against SQL injection via named parameters:
- Comprehensive parameter validation to prevent atom table exhaustion
- Improved parameter extraction to avoid malicious input exploitation
- Validates all named parameters against statement introspection
- Proper error handling for invalid or malicious parameter names
See SECURITY.md for full details.
Bug Fixes & Improvements
Statement Caching
- Replaced unbounded
persistent_termcache with bounded ETS LRU cache - Prevents memory leaks from unlimited prepared statement caching
- Configurable cache size with automatic eviction
Error Handling
- Propagate parameter introspection errors instead of silently falling back
- Descriptive errors for invalid argument types
- Improved error messages throughout
Code Quality
- Fixed all Credo warnings
- Improved test reliability and coverage
- Better state threading and error handling
- Removed redundant UTF-8 validation code
Documentation
- Added generated/computed columns documentation
- Enhanced JSON/JSONB function documentation
- Comprehensive test coverage for all new features
- Cross-connection security test suite
🔗 Resources
- CHANGELOG - Full changelog
- AGENTS.md - API reference
- SECURITY.md - Security policy
Full Changelog: 0.8.3...0.8.6
0.8.3 - SQLite extensions & fuzz testing
v0.8.3 Release Notes
New Features
RANDOM ROWID Support (libSQL Extension)
- Generate pseudorandom row IDs instead of sequential integers for security/privacy
- Prevents ID enumeration attacks and leaking business metrics
- Usage: create table(:sessions, options: [random_rowid: true])
SQLite Extension Loading
- Load SQLite extensions dynamically via enable_extensions/2 and load_ext/3
- Supports FTS5, JSON1, R-Tree, PCRE, and custom extensions
- Security-first: disabled by default, must be explicitly enabled
Enhanced Statement Introspection
- stmt_parameter_name/3 - Get named parameter names (:name, @name, $name)
- reset_stmt/2 - Explicitly reset statements for efficient reuse
- get_stmt_columns/2 - Get full column metadata (name, origin, declared type)
Remote Encryption Support
- New remote_encryption_key option for Turso encrypted databases
- Works alongside existing local encryption_key for end-to-end encryption
Quality & Testing
- Added Credo, Dialyxir, and Sobelow for comprehensive Elixir code analysis
- Property-based fuzz testing with StreamData (SQL injection, transactions, edge cases)
- Rust fuzz testing infrastructure with cargo-fuzz
- Ported key tests from Ecto.Adapters.SQL for compatibility verification
- Modernised Rust code: std::sync::LazyLock, stricter Clippy lints
Fixes
- SQL injection prevention in Pragma module table name validation
- Dialyzer type error in disconnect/2 spec
- Improved fuzz test stability for savepoints and binary data
Changelog: https://github.com/ocean/ecto_libsql/blob/main/CHANGELOG.md
Full Changelog: 0.8.1...0.8.3
0.8.1 - Constraints bug fix
Fixed
- Constraint Error Handling: Index Name Reconstruction (Issue #34)
- Improved constraint name extraction to reconstruct full index names from SQLite error messages
- Now follows Ecto's naming convention: table_column1_column2_index
- Single-column constraints: "UNIQUE constraint failed: users.email" → "users_email_index" (previously just "email")
- Multi-column constraints: "UNIQUE constraint failed: users.slug, users.parent_slug" → "users_slug_parent_slug_index"
- Backtick handling: Properly strips trailing backticks appended by libSQL to error messages
- Enhanced error messages: Preserves custom index names from enhanced format (index: custom_index_name)
- NOT NULL constraints: Reconstructs index names following same convention
- Enables accurate unique_constraint/3 and check_constraint/3 matching with custom index names in Ecto changesets
Full Changelog: 0.8.0...0.8.1
0.8.0 - Rust refactor
Overview
Major code refactoring and critical thread safety fixes with zero breaking changes.
Key Changes
Code Refactoring
- Modularised Rust codebase: Split 2,302-line monolithic
lib.rsinto 13 focused modules (connection, query, batch, statement, transaction, savepoint, cursor, replication, metadata, utils, constants, models, decode) - Reorganised test suite: Refactored 1,194-line tests.rs into structured modules (integration, constants, utils)
- Improved maintainability: Better code navigation and contributor onboarding with clearer separation of concerns
- Zero behaviour changes: All APIs and functionality preserved
Thread Safety & Performance Fixes
- Registry lock management: Fixed all functions to drop registry locks before async operations (prevents deadlocks)
- Scheduler annotations: Added
#[rustler::nif(schedule = "DirtyIo")]to blocking NIFs - Atom naming consistency: Fixed remote_primary → remote atom mismatch
- Runtime optimisation: Use shared global TOKIO_RUNTIME instead of creating per-connection (prevents resource exhaustion)
- Replication performance: Eliminated unnecessary async overhead for synchronous operations
Bug Fixes
- Fixed prepared statement column introspection tests (enabled previously skipped tests)
- Enhanced constraint error message handling with index name support
- Improved remote Turso test stability
- Better error handling for allocation failures
- Proper SQL identifier quoting in PRAGMA queries
Note: This release is fully backward compatible. The refactoring is purely organisational with performance and stability improvements under the hood.
Full Changelog: 0.7.5...0.8.0
0.7.5 - query routing bug fix & performance improvements
Release 0.7.5 - Query Routing & Performance Improvements
This release focuses on critical bug fixes for batch operations and significant performance optimisations.
Fixed
Query/Execute Routing for Batch Operations
- Implemented proper query() vs execute() routing in batch operations based on statement type
- execute_batch() now automatically detects SELECT and RETURNING clauses to use the correct LibSQL method
- execute_transactional_batch() applies the same intelligent routing logic for atomic operations
- execute_batch_native() and execute_transactional_batch_native() now properly route SQL batch execution
- Eliminates "Statement does not return data" errors for operations that should return rows
- All operations with RETURNING clauses now correctly use the query() method
Performance: Batch Operation Optimisations
- Eliminated per-statement argument clones in batch operations for better memory efficiency
- Changed batch_stmts.iter() to batch_stmts.into_iter() to consume vectors by value
- Removed unnecessary args.clone() calls in both transactional and non-transactional batches
- Reduces memory allocations during batch execution for improved throughput
Lock Coupling Reduction
- Dropped outer LibSQLConn mutex guard earlier in batch operations to reduce contention
- Extract inner Arc<Mutexlibsql::Connection> before entering async blocks
- Only hold inner connection lock during actual I/O operations
- Applied to all four batch operation variants:
- execute_batch()
- execute_transactional_batch()
- execute_batch_native()
- execute_transactional_batch_native()
- Reduces contention and deadlock surface area by following the established pattern from query_args()
Test Coverage & Documentation
- Enhanced should_use_query() test coverage for block comment handling
- Added explicit assertion documenting known limitation: RETURNING in block comments detected as false positive (safe behaviour)
- Documented CTE and EXPLAIN detection limitations with clear scope notes
- Added comprehensive future improvement recommendations with priority levels and implementation sketches
- Added performance budget notes for optimisation efforts
Impact
- Correctness: Batch operations with RETURNING clauses now work correctly
- Performance: Reduced memory allocations and lock contention in batch operations
- Reliability: Lower deadlock risk through improved lock coupling patterns
- Maintainability: Better test coverage and documentation for edge cases
Full Changelog: 0.7.0...0.7.5