Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.7.0]
Added
Prepared Statement Caching with Reset
- Implemented true statement caching: statements are prepared once and reused with
.reset()for binding cleanup - Changed
STMT_REGISTRYfrom storing SQL text to caching actualArc<Mutex<Statement>>objects prepare_statement/2now immediately prepares statements (catches SQL errors early)query_prepared/5uses cached statement withstmt.reset()callexecute_prepared/6uses cached statement withstmt.reset()call- Statement introspection functions optimized to use cached statements directly
- Eliminates 30-50% performance overhead from repeated statement re-preparation
- Impact: Significant performance improvement for prepared statement workloads (~10-15x faster for cached queries)
- Backward compatible: API unchanged, behavior improved (eager validation better than deferred)
- Implemented true statement caching: statements are prepared once and reused with
Statement Caching Benchmark Test
- Added
test/stmt_caching_benchmark_test.exswith comprehensive caching tests - Verified 100 cached executions complete in ~33ms (~330µs per execution)
- Confirmed bindings clear correctly between executions
- Tested multiple independent cached statements
- Demonstrated consistent performance across multiple prepared statements
- Added
Full Transaction Ownership & Savepoint Connection Context
- Implemented complete transaction-to-connection mapping with
TransactionEntrystruct TXN_REGISTRYnow tracksconn_idfor each transaction, enabling ownership validation- Updated
begin_transaction/1andbegin_transaction_with_behavior/2to store connection owner with transaction - Updated
savepoint/2NIF signature tosavepoint/3with requiredconn_idparameter - All savepoint functions (
savepoint,release_savepoint,rollback_to_savepoint) now validate transaction ownership - Updated
commit_or_rollback_transaction/5to validate ownership before commit/rollback - Updated
declare_cursor_with_context/6to work with transaction ownership tracking - Prevents cross-connection transaction manipulation by enforcing strict ownership validation
- Returns clear error: "Transaction does not belong to this connection" on ownership violation
- All 289 tests passing (including 18 savepoint-specific tests, 5 transaction isolation tests)
- Security: Now validates actual transaction ownership, not just ID existence
- Implemented complete transaction-to-connection mapping with
Connection Management Features
busy_timeout/2- Configure database busy timeout to handle locked databases (default: 5000ms)reset/1- Reset connection state without closing the connectioninterrupt/1- Interrupt long-running queries on a connection- All features include comprehensive tests and integration with connection lifecycle
PRAGMA Support (Complete SQLite Configuration)
- New
EctoLibSql.Pragmamodule with comprehensive PRAGMA helpers (396 lines) query/2- Execute arbitrary PRAGMA statements- Foreign Keys:
enable_foreign_keys/1,disable_foreign_keys/1,foreign_keys/1 - Journal Mode:
set_journal_mode/2,journal_mode/1(supports :delete, :wal, :memory, :persist, :truncate, :off) - Synchronous Level:
set_synchronous/2,synchronous/1(supports :off, :normal, :full, :extra) - Cache Size:
set_cache_size/2,cache_size/1(in pages or KB with negative values) - Table Introspection:
table_info/2,table_list/1 - User Version:
user_version/1,set_user_version/2(for schema versioning) - Added 19 comprehensive tests covering all PRAGMA operations
- New
Native Batch Execution
execute_batch_sql/2- Execute multiple SQL statements in a single call (non-transactional)execute_transactional_batch_sql/2- Execute multiple SQL statements atomically in a transaction- Improved performance for bulk operations (migrations, seeding, etc.)
- Added 3 comprehensive tests including atomic rollback verification
Advanced Replica Sync Control
get_frame_number(conn_id)NIF - Monitor replication framesync_until(conn_id, frame_no)NIF - Wait for specific frame with 30-second timeoutflush_replicator(conn_id)NIF - Push pending writes with 30-second timeout- Elixir wrappers:
get_frame_number_for_replica(),sync_until_frame(),flush_and_get_frame() - All with proper error handling, explicit None handling, and network timeouts
- Improved error messages for timeout and non-replica scenarios
Max Write Replication Index
max_write_replication_index/1- Track highest frame number from write operations- Enables read-your-writes consistency across replicas
- Synchronous NIF wrapper around
db.max_write_replication_index() - Use case: Ensure replica syncs to at least your write frame before reading
Prepared Statement Introspection
stmt_column_count/2- Get number of columns in a prepared statement result setstmt_column_name/3- Get column name by index (0-based)stmt_parameter_count/2- Get number of parameters (?) in a prepared statement- Enables dynamic schema discovery and parameter binding validation
- Added 21 comprehensive tests in
test/prepared_statement_test.exs
Savepoint Support (Nested Transactions)
create_savepoint/2- Create a named savepoint within a transactionrelease_savepoint_by_name/2- Commit a savepoint's changesrollback_to_savepoint_by_name/2- Rollback to a savepoint, keeping transaction active- Enables nested transaction-like behaviour within a single transaction
- Perfect for error recovery and partial rollback patterns
- Added 18 comprehensive tests in
test/savepoint_test.exs
Test Suite Reorganisation
- Restructured tests from "missing vs implemented" to feature-based organisation
- New feature-focused test files:
test/connection_features_test.exs(6 tests) - busy_timeout, reset, interrupttest/batch_features_test.exs(6 tests) - batch executiontest/pragma_test.exs(19 tests) - PRAGMA operationstest/statement_features_test.exs(11 tests) - prepared statement features (mostly skipped, awaiting implementation)test/advanced_features_test.exs(13 tests) - MVCC, cacheflush, replication, extensions, hooks (all skipped, awaiting implementation)
- All unimplemented features properly marked with
@describetag :skipfor easy enabling as features are added
Comprehensive Documentation Suite
TURSO_COMPREHENSIVE_GAP_ANALYSIS.md- Consolidated analysis of all Turso/LibSQL featuresIMPLEMENTATION_ROADMAP_FOCUSED.md- Detailed implementation roadmap with prioritised phasesLIBSQL_FEATURE_MATRIX_FINAL.md- Complete feature compatibility matrixTESTING_PLAN_COMPREHENSIVE.md- Comprehensive testing strategy and coverage plan- Merged multiple gap analysis documents into consolidated, authoritative sources
- Complete source code references and Ecto integration details
Changed
LibSQL 0.9.29 API Verification
- Verified all replication NIFs use correct libsql 0.9.29 APIs
get_frame_number/1confirmed usingdb.replication_index()(not legacy methods)sync_until/2confirmed usingdb.sync_until()flush_replicator/1confirmed usingdb.flush_replicator()- All implementations verified correct
Test Suite Improvements
- Removed duplicated tests to improve maintainability
- Standardized test database naming conventions across all test files
- Improved test assertions for better clarity and debugging
- Added explicit disconnect calls to match test patterns
- Enabled previously skipped tests for now-implemented features
- Fixed test setup issues and race conditions
- Performance test adjustments for slower CI machines
Transaction Ownership Helper Functions
- Added
verify_transaction_ownership/2helper function to reduce code duplication - Simplified ownership validation logic across all transaction operations
- Consolidated lock scope handling for transaction registry operations
- Improved code maintainability and consistency
- Added
Replica Function API Improvements
- Added state-accepting overloads for replica functions for better ergonomics
- Fixed inconsistent
flush_replicatorbehavior to always use 30-second timeout - Improved error messages for replica operations
Fixed
Security: SQL Injection Prevention
- Fixed potential SQL injection vulnerability in savepoint name validation
- Added strict alphanumeric validation for savepoint identifiers
- Prevents malicious SQL in nested transaction operations
Security: Prepared Statement Validation
- Fixed security issue in prepared statement parameter validation
- Enhanced parameter binding checks to prevent malformed queries
Remote Test Stability
- Fixed vector operations test to properly drop existing tables before recreation
- Removed
IF NOT EXISTSfrom table creation to ensure correct schema - Prevents "table has no column named X" errors from stale test data
Upsert Operations with ON CONFLICT (Issue #25)
- Implemented full support for
on_conflictoptions in INSERT operations - Added support for
on_conflict: :nothingwith conflict targets (single and composite unique indexes) - Added support for
on_conflict: :replace_allfor upsert operations - Added support for custom field replacement with
on_conflict: {fields, _, targets} - Fixed composite unique index constraint handling - now correctly generates
ON CONFLICT (col1, col2) DO NOTHING/UPDATE - Improved constraint name extraction from SQLite error messages to handle composite constraints
- Added 8 comprehensive connection tests for various upsert scenarios
- Added 154 lines of integration tests demonstrating real-world usage with composite unique indexes
- Fixed test setup to ensure clean database state by dropping and recreating tables before each test
- Implemented full support for
Binary ID Type System (Issue #23) - Complete Resolution
- Fixed
autogenerate(:binary_id)to generate string UUIDs instead of binary UUIDs - Fixed
loaders(:binary_id)to pass through string UUIDs from TEXT columns (was expecting binary) - Fixed
dumpers(:binary_id)to pass through string UUIDs (was converting to binary) - Fixed Rust NIF to handle Elixir
Binarytype for BLOB data (was only checkingVec<u8>) - LibSQL stores
:binary_idas TEXT, not BLOB, so string UUIDs are required throughout the pipeline - Prevents "Unsupported argument type" errors when inserting records with binary_id or binary fields
- Removed unnecessary
blob_encodewrapper - binary data now passes through directly - Fixed INSERT without RETURNING clause - Now correctly returns
rows: nilinstead ofrows: []when no RETURNING clause is present, preventing CaseClauseError in Ecto.Adapters.SQL - Fixed BLOB encoding in Rust NIF - Binary data now returns as Elixir binaries (
<<...>>) instead of lists ([...]), properly encoding BLOBs usingOwnedBinary
- Fixed
Test Coverage Improvements
- Added comprehensive integration tests for
binary_idautogeneration (6 new tests, all passing) - Added end-to-end tests for
:binary(BLOB) field CRUD operations - Added tests for
binary_idwith associations and foreign keys - Total: 194 lines of new integration tests in
test/ecto_integration_test.exs
- Added comprehensive integration tests for
[0.6.0] - 2025-11-30
Fixed
Remote Sync Performance & Reliability
- Removed redundant manual
.sync()calls after write operations for embedded replicas - LibSQL automatically handles sync to remote primary database - manual syncs were causing double-sync overhead
- Added 30-second timeout to connection establishment to prevent indefinite hangs
- All Turso remote tests now pass reliably (previously 4 tests timed out)
- Test suite execution time improved significantly (~107s vs timing out at 60s+)
- Removed redundant manual
Ecto Migrations Compatibility (Issue #20)
- Fixed DDL function grouping that was causing compilation errors
- Added comprehensive migration test suite (759 lines) covering all SQLite ALTER TABLE operations
- Improved handling of SQLite's limited ALTER TABLE support
- Added tests for column operations, constraint management, and index creation
Prepared Statement Execution
- Fixed panic in prepared statement execution that could crash the BEAM VM
- Added proper error handling for prepared statement operations
- Improved error messages for prepared statement failures
Extended LibSQL DDL Support
- Added support for additional ALTER TABLE operations compatible with LibSQL
- Improved DDL operation grouping and execution order
- Better handling of SQLite dialect quirks
Added
Cursor Streaming Support
- Implemented cursor-based streaming for large result sets
- Added
handle_declare/4,handle_fetch/4, andhandle_deallocate/4DBConnection callbacks - Memory-efficient processing of large queries
- Rust NIF functions:
declare_cursor/3,fetch_cursor/2, cursor registry management
Comprehensive Test Coverage
- Added 138 new DDL generation tests in
test/ecto_connection_test.exs - Added 759 lines of migration tests in
test/ecto_migration_test.exs - Improved error handling test coverage
- All 162 tests passing (0 failures)
- Added 138 new DDL generation tests in
Changed
Sync Behaviour for Embedded Replicas
- Automatic sync after writes has been removed (LibSQL handles this natively)
- Manual
sync()viaEctoLibSql.Native.sync/1still available for explicit control - Improved sync timeout handling with configurable
DEFAULT_SYNC_TIMEOUT_SECS(30s) - Added connection timeout to prevent hangs during initial replica sync
Documentation Updates
- Updated all documentation to reflect sync behaviour changes
- Added clarification about when manual sync is needed vs automatic
- Improved Turso/LibSQL compatibility documentation references
Technical Details
Sync Performance Before:
- Manual
.sync()called after every write operation - Double sync overhead (LibSQL auto-sync + manual sync)
- 120-second timeout causing long test hangs
- 4 tests timing out after 60+ seconds each
Sync Performance After:
- LibSQL's native auto-sync used correctly
- No redundant manual sync calls
- 30-second connection timeout for fast failure
- All tests passing in ~107 seconds
Key Insight: According to Turso documentation: "Writes are sent to the remote primary database by default, then the local database updates automatically once the remote write succeeds." Manual sync is only needed when explicitly pulling down changes from remote (e.g., after reconnecting to an existing replica).
Migration Notes
This is a non-breaking change for normal usage. However, if you were relying on automatic sync behaviour after writes in embedded replica mode, you may now need to explicitly call EctoLibSql.Native.sync/1 when you need to ensure remote data is pulled down (e.g., after reconnecting to an existing local database).
Recommended Actions:
- Review code that uses embedded replicas with
sync: true - Add explicit
sync()calls after reconnecting to existing local databases if you need to pull down remote changes - Remove any redundant manual
sync()calls after write operations
[0.5.0] - 2025-11-27
Changed
- Rust NIF Error Handling (BREAKING for direct NIF users)
- Eliminated all 146
unwrap()calls from production Rust code - Added
safe_lock()andsafe_lock_arc()helper functions for safe mutex locking - All NIF errors now return
{:error, message}tuples to Elixir instead of panicking - Mutex poisoning errors are handled gracefully with descriptive context
- Invalid connection/transaction/statement/cursor IDs return proper errors
- Eliminated all 146
Fixed
- VM Stability - NIF errors no longer crash the entire BEAM VM
- Invalid operations (bad connection IDs, missing resources) now return error tuples
- Processes survive NIF errors, allowing supervision trees to work properly
- Error messages include descriptive context for easier debugging
Added
- Comprehensive Error Handling Tests
- Added
test/error_demo_test.exswith 7 tests demonstrating graceful error handling - Added
test/error_handling_test.exswith 14 comprehensive error coverage tests - All tests verify that NIF errors return proper error tuples instead of crashing the BEAM VM
- Added
Technical Details
Before 0.5.0:
- 146
unwrap()calls in Rust production code - Mutex/registry errors → panic → entire BEAM VM crash
- Invalid IDs → panic → VM crash
- Supervision trees ineffective for NIF errors
After 0.5.0:
- 0
unwrap()calls in Rust production code (100% eliminated) - All errors return
{:error, "descriptive message"}tuples - Processes can handle errors and recover
- Supervision trees work as expected
Migration Guide
This is a non-breaking change for normal Ecto usage. Your existing code will continue to work exactly as before, but is now significantly more stable.
What Changed:
- NIF functions that previously panicked now return
{:error, reason}tuples - Your existing error handling code will now catch errors that previously crashed the VM
Recommended Actions:
- Review error handling in code that uses
EctoLibSql.Nativefunctions directly - Ensure supervision strategies are in place for database operations
- Consider adding retry logic for transient errors (connection timeouts, etc.)
Notes
This release represents a major stability improvement for production deployments. The refactoring ensures that ecto_libsql handles errors the "Elixir way" - returning error tuples that can be supervised, rather than panicking at the Rust level and crashing the VM.
[0.4.0] - 2025-11-19
Changed
- Library Renamed from LibSqlEx to EctoLibSql
- Package name changed from
:libsqlexto:ecto_libsql - Main module renamed from
LibSqlExtoEctoLibSql - Adapter module renamed from
Ecto.Adapters.LibSqlExtoEcto.Adapters.LibSql - Connection module renamed from
Ecto.Adapters.LibSqlEx.ConnectiontoEcto.Adapters.LibSql.Connection - Native module renamed from
LibSqlEx.NativetoEctoLibSql.Native - All supporting modules updated (Query, Result, State, Error)
- Rust crate renamed from
libsqlextoecto_libsql
- Package name changed from
Migration Guide
To upgrade from 0.3.0 to 0.4.0, update your dependencies and module references:
# mix.exs - Update dependency
def deps do
[
# Old: {:libsqlex, "~> 0.3.0"}
{:ecto_libsql, "~> 0.4.0"}
]
end
# config/config.exs - Update adapter reference
config :my_app, MyApp.Repo,
# Old: adapter: Ecto.Adapters.LibSqlEx
adapter: Ecto.Adapters.LibSql
# Code - Update module references
# Old: alias LibSqlEx.{Query, Result}
alias EctoLibSql.{Query, Result}
# Old: LibSqlEx.Native.vector_type(128, :f32)
EctoLibSql.Native.vector_type(128, :f32)All functionality remains identical; only names have changed.
[0.3.0] - 2025-11-17
Added
Full Ecto Adapter Support - LibSqlEx now provides a complete Ecto adapter implementation
Ecto.Adapters.LibSqlEx- Main adapter module implementing Ecto.Adapter.Storage and Ecto.Adapter.StructureEcto.Adapters.LibSqlEx.Connection- SQL query generation and DDL support for SQLite/libSQL- Full support for Ecto schemas, changesets, and migrations
- Phoenix integration support
- Type loaders and dumpers for proper Ecto type conversion
- Storage operations (create, drop, status)
- Structure operations (dump, load) using sqlite3
- Migration support with standard Ecto.Migration features:
- CREATE/DROP TABLE with IF (NOT) EXISTS
- ALTER TABLE for adding columns and renaming
- CREATE/DROP INDEX with UNIQUE and partial index support
- Proper constraint conversion (UNIQUE, FOREIGN KEY, CHECK)
- Comprehensive test suite for adapter and connection modules
Documentation and Examples
examples/ecto_example.exs- Complete Ecto usage examplesECTO_MIGRATION_GUIDE.md- Comprehensive migration guide from PostgreSQL/MySQL- Updated README with extensive Ecto integration documentation
- Phoenix integration guide
- Production deployment best practices
Changed
- Updated
mix.exsto includeectoandecto_sqldependencies - Bumped version from 0.2.0 to 0.3.0 to reflect major feature addition
Notes
This release makes LibSqlEx a full-featured Ecto adapter, bringing it on par with other database adapters in the Elixir ecosystem. Users can now:
- Use LibSqlEx in Phoenix applications
- Define Ecto schemas and run migrations
- Leverage all Ecto.Query features
- Benefit from Turso's remote replica mode with Ecto
- Migrate existing applications from PostgreSQL/MySQL to LibSqlEx
The adapter supports all three connection modes:
- Local SQLite databases
- Remote-only Turso connections
- Remote replica mode (local + cloud sync)
Breaking Changes
None - this is purely additive functionality.
Known Limitations
SQLite/libSQL has some limitations compared to PostgreSQL:
- No ALTER COLUMN support (column type modifications require table recreation)
- No DROP COLUMN on older SQLite versions (< 3.35.0)
- No native array types (use JSON or separate tables)
- No native UUID type (stored as TEXT, works with Ecto.UUID)
These are SQLite limitations, not LibSqlEx limitations, and are well-documented in the migration guide.
[0.2.0] - Previous Release
Added
- DBConnection protocol implementation
- Local, Remote, and Remote Replica modes
- Transaction support with multiple isolation levels
- Prepared statements
- Batch operations
- Cursor support for large result sets
- Vector search support
- Encryption support (AES-256-CBC)
- WebSocket protocol support
- Metadata methods (last_insert_rowid, changes, etc.)
[0.1.0] - Initial Release
Added
- Basic LibSQL/Turso connection support
- Rust NIF implementation
- Query execution
- Basic transaction support