{"id":33422194,"url":"https://github.com/ocean/ecto_libsql","last_synced_at":"2026-01-20T17:02:56.814Z","repository":{"id":324117673,"uuid":"1096026090","full_name":"ocean/ecto_libsql","owner":"ocean","description":"Elixir Ecto database adapter for libSQL/Turso","archived":false,"fork":false,"pushed_at":"2026-01-19T04:09:09.000Z","size":19249,"stargazers_count":14,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-19T13:32:24.933Z","etag":null,"topics":["ecto","ecto-adapter","elixir-library","libsql","libsql-client","turso","turso-db"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"danawanb/libsqlex","license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ocean.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-13T20:56:15.000Z","updated_at":"2026-01-19T11:45:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ocean/ecto_libsql","commit_stats":null,"previous_names":["ocean/libsqlex","ocean/ecto_libsql"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/ocean/ecto_libsql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocean%2Fecto_libsql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocean%2Fecto_libsql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocean%2Fecto_libsql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocean%2Fecto_libsql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ocean","download_url":"https://codeload.github.com/ocean/ecto_libsql/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ocean%2Fecto_libsql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28607624,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T16:10:39.856Z","status":"ssl_error","status_checked_at":"2026-01-20T16:10:39.493Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ecto","ecto-adapter","elixir-library","libsql","libsql-client","turso","turso-db"],"created_at":"2025-11-24T02:02:45.123Z","updated_at":"2026-01-20T17:02:56.807Z","avatar_url":"https://github.com/ocean.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EctoLibSql\n\n[![GitHub Actions CI](https://github.com/ocean/ecto_libsql/actions/workflows/ci.yml/badge.svg)](https://github.com/ocean/ecto_libsql/actions/workflows/ci.yml)\n\n`ecto_libsql` is an (unofficial) Elixir [Ecto](https://github.com/elixir-ecto/ecto) database adapter for [LibSQL](https://github.com/tursodatabase/libsql) database files, and databases hosted on [Turso](https://turso.tech/), built with Rust NIFs. It supports local libSQL/SQLite files, remote replica with synchronisation, and remote only Turso databases.\n\n## Installation\n\nAdd `ecto_libsql` to your dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:ecto_libsql, \"~\u003e 0.8.0\"}\n  ]\nend\n```\n\n## Quick Start\n\n### With Ecto (Recommended)\n\n```elixir\n# Configure your repo\nconfig :my_app, MyApp.Repo,\n  adapter: Ecto.Adapters.LibSql,\n  database: \"my_app.db\"\n\n# Define your repo\ndefmodule MyApp.Repo do\n  use Ecto.Repo,\n    otp_app: :my_app,\n    adapter: Ecto.Adapters.LibSql\nend\n\n# Use Ecto as normal\ndefmodule MyApp.User do\n  use Ecto.Schema\n\n  schema \"users\" do\n    field :name, :string\n    field :email, :string\n    timestamps()\n  end\nend\n\n# CRUD operations\n{:ok, user} = MyApp.Repo.insert(%MyApp.User{name: \"Alice\", email: \"alice@example.com\"})\nusers = MyApp.Repo.all(MyApp.User)\n```\n\n### With DBConnection (Advanced)\n\nFor lower-level control, you can use the DBConnection interface directly:\n\n```elixir\n# Local database\n{:ok, conn} = DBConnection.start_link(EctoLibSql, database: \"local.db\")\n\n# Remote Turso database\n{:ok, conn} = DBConnection.start_link(EctoLibSql,\n  uri: \"libsql://your-db.turso.io\",\n  auth_token: \"your-token\"\n)\n\n# Embedded replica (local database synced with remote)\n{:ok, conn} = DBConnection.start_link(EctoLibSql,\n  database: \"local.db\",\n  uri: \"libsql://your-db.turso.io\",\n  auth_token: \"your-token\",\n  sync: true\n)\n```\n\n## Features\n\n**Connection Modes**\n- Local SQLite files\n- Remote LibSQL/Turso servers\n- Embedded replicas with automatic or manual synchronisation\n\n**Core Functionality**\n- Parameterised queries with safe parameter binding\n- Prepared statements with automatic caching and introspection\n- Transactions with multiple isolation levels (deferred, immediate, exclusive) and savepoints\n- Batch operations (transactional and non-transactional)\n- Metadata access (last insert ID, row counts, etc.)\n- Connection management (busy timeout, reset, interrupt)\n- PRAGMA configuration helpers\n\n**Advanced Features**\n- Vector similarity search\n- R*Tree spatial indexing for multi-dimensional range queries\n- Database encryption (local AES-256-CBC and Turso remote encryption)\n- WebSocket and HTTP protocols\n- Cursor-based streaming for large result sets (via DBConnection interface)\n- Advanced replica synchronisation with frame tracking\n\n**Note:** Ecto `Repo.stream()` is not yet implemented. For streaming large datasets, use the DBConnection cursor interface directly (see examples in USAGE.md).\n\n**Reliability**\n- **Production-ready error handling**: All Rust NIF errors return proper Elixir error tuples instead of crashing the BEAM VM\n- **Graceful degradation**: Invalid operations (bad connection IDs, missing resources) return `{:error, message}` for proper supervision tree handling\n\n## Documentation\n\n- **API Documentation**: [https://hexdocs.pm/ecto_libsql](https://hexdocs.pm/ecto_libsql)\n- **LLM / AGENT Guide**: [USAGE.md](USAGE.md)\n- **Changelog**: [CHANGELOG.md](CHANGELOG.md)\n- **Migration Guide**: [ECTO_MIGRATION_GUIDE.md](ECTO_MIGRATION_GUIDE.md)\n\n## Usage Examples\n\n### Ecto Examples\n\n#### Basic CRUD Operations\n\n```elixir\n# Setup\ndefmodule MyApp.Repo do\n  use Ecto.Repo,\n    otp_app: :my_app,\n    adapter: Ecto.Adapters.LibSql\nend\n\ndefmodule MyApp.User do\n  use Ecto.Schema\n\n  schema \"users\" do\n    field :name, :string\n    field :email, :string\n    field :age, :integer\n    timestamps()\n  end\nend\n\n# Create\n{:ok, user} = MyApp.Repo.insert(%MyApp.User{\n  name: \"Alice\",\n  email: \"alice@example.com\",\n  age: 30\n})\n\n# Read\nuser = MyApp.Repo.get(MyApp.User, 1)\nusers = MyApp.Repo.all(MyApp.User)\n\n# Update\nuser\n|\u003e Ecto.Changeset.change(age: 31)\n|\u003e MyApp.Repo.update()\n\n# Delete\nMyApp.Repo.delete(user)\n```\n\n#### Queries with Ecto.Query\n\n```elixir\nimport Ecto.Query\n\n# Filter and order\nadults = MyApp.User\n  |\u003e where([u], u.age \u003e= 18)\n  |\u003e order_by([u], desc: u.inserted_at)\n  |\u003e MyApp.Repo.all()\n\n# Aggregations\ncount = MyApp.User\n  |\u003e where([u], u.age \u003e= 18)\n  |\u003e MyApp.Repo.aggregate(:count)\n\navg_age = MyApp.Repo.aggregate(MyApp.User, :avg, :age)\n```\n\n#### Transactions\n\n```elixir\nMyApp.Repo.transaction(fn -\u003e\n  {:ok, user1} = MyApp.Repo.insert(%MyApp.User{name: \"Bob\", email: \"bob@example.com\"})\n  {:ok, user2} = MyApp.Repo.insert(%MyApp.User{name: \"Carol\", email: \"carol@example.com\"})\n\n  %{user1: user1, user2: user2}\nend)\n```\n\n### DBConnection Examples (Advanced)\n\nFor lower-level control, use the DBConnection interface:\n\n#### Basic Queries\n\n```elixir\n{:ok, conn} = DBConnection.start_link(EctoLibSql, database: \"test.db\")\n\n# Create table\nDBConnection.execute(conn, %EctoLibSql.Query{\n  statement: \"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)\"\n}, [])\n\n# Insert with parameters\nDBConnection.execute(conn, %EctoLibSql.Query{\n  statement: \"INSERT INTO users (name) VALUES (?)\"\n}, [\"Alice\"])\n\n# Query data\n{:ok, _query, result} = DBConnection.execute(conn, %EctoLibSql.Query{\n  statement: \"SELECT * FROM users WHERE name = ?\"\n}, [\"Alice\"])\n\nIO.inspect(result.rows)  # [[1, \"Alice\"]]\n```\n\n#### Transactions\n\n```elixir\nDBConnection.transaction(conn, fn conn -\u003e\n  DBConnection.execute(conn, %EctoLibSql.Query{\n    statement: \"INSERT INTO users (name) VALUES (?)\"\n  }, [\"Bob\"])\n\n  DBConnection.execute(conn, %EctoLibSql.Query{\n    statement: \"INSERT INTO users (name) VALUES (?)\"\n  }, [\"Carol\"])\nend)\n```\n\n### Prepared Statements\n\nStatements are automatically cached and reused for ~10-15x performance improvement:\n\n```elixir\n# Prepare once, execute many times (statement cached internally)\n{:ok, state} = EctoLibSql.connect(database: \"test.db\")\n\n{:ok, stmt_id} = EctoLibSql.Native.prepare(state,\n  \"SELECT * FROM users WHERE id = ?\")\n\n# Cached statement reused with automatic binding cleanup\n{:ok, result1} = EctoLibSql.Native.query_stmt(state, stmt_id, [1])\n{:ok, result2} = EctoLibSql.Native.query_stmt(state, stmt_id, [2])\n\n# Introspect statement structure\n{:ok, param_count} = EctoLibSql.Native.stmt_parameter_count(state, stmt_id)\n{:ok, col_count} = EctoLibSql.Native.stmt_column_count(state, stmt_id)\n\n:ok = EctoLibSql.Native.close_stmt(stmt_id)\n```\n\n### Batch Operations\n\n```elixir\n{:ok, state} = EctoLibSql.connect(database: \"test.db\")\n\n# Execute multiple statements with parameters\nstatements = [\n  {\"INSERT INTO users (name) VALUES (?)\", [\"Dave\"]},\n  {\"INSERT INTO users (name) VALUES (?)\", [\"Eve\"]},\n  {\"UPDATE users SET name = ? WHERE id = ?\", [\"David\", 1]}\n]\n\n# Non-transactional (each statement independent)\n{:ok, results} = EctoLibSql.Native.batch(state, statements)\n\n# Transactional (all-or-nothing)\n{:ok, results} = EctoLibSql.Native.batch_transactional(state, statements)\n\n# Raw SQL batch (useful for migrations, seeding)\nsql = \"\"\"\nCREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT);\nINSERT INTO posts (title) VALUES ('First Post');\nINSERT INTO posts (title) VALUES ('Second Post');\n\"\"\"\n{:ok, _} = EctoLibSql.Native.execute_batch_sql(state, sql)\n```\n\n### Vector Similarity Search\n\n```elixir\n{:ok, state} = EctoLibSql.connect(database: \"vectors.db\")\n\n# Create table with vector column (3 dimensions, f32 precision)\nvector_type = EctoLibSql.Native.vector_type(3, :f32)\nEctoLibSql.handle_execute(\n  \"CREATE TABLE items (id INTEGER, embedding #{vector_type})\",\n  [], [], state\n)\n\n# Insert vector\nvec = EctoLibSql.Native.vector([1.0, 2.0, 3.0])\nEctoLibSql.handle_execute(\n  \"INSERT INTO items VALUES (?, vector(?))\",\n  [1, vec], [], state\n)\n\n# Find similar vectors (cosine distance)\nquery_vector = [1.5, 2.1, 2.9]\ndistance_fn = EctoLibSql.Native.vector_distance_cos(\"embedding\", query_vector)\n{:ok, _query, results, _} = EctoLibSql.handle_execute(\n  \"SELECT id FROM items ORDER BY #{distance_fn} LIMIT 10\",\n  [], [], state\n)\n```\n\n### Database Encryption\n\nEctoLibSql supports two types of encryption:\n- **Local encryption**: AES-256-CBC encryption for local database files (at-rest encryption)\n- **Remote encryption**: Turso encrypted databases (encryption key sent with each request)\n\n```elixir\n# Encrypted local database (local file encryption)\n{:ok, conn} = DBConnection.start_link(EctoLibSql,\n  database: \"encrypted.db\",\n  encryption_key: \"your-secret-key-must-be-at-least-32-characters\"\n)\n\n# Encrypted remote database (Turso cloud encryption)\n{:ok, conn} = DBConnection.start_link(EctoLibSql,\n  uri: \"libsql://your-encrypted-db.turso.io\",\n  auth_token: \"your-token\",\n  remote_encryption_key: \"base64-encoded-encryption-key\"\n)\n\n# Encrypted embedded replica (both local and remote encryption)\n{:ok, conn} = DBConnection.start_link(EctoLibSql,\n  database: \"encrypted.db\",\n  uri: \"libsql://your-encrypted-db.turso.io\",\n  auth_token: \"your-token\",\n  encryption_key: \"your-local-encryption-key-32-chars-min\",\n  remote_encryption_key: \"base64-encoded-remote-encryption-key\",\n  sync: true\n)\n```\n\n**Notes**:\n- `encryption_key`: Used for local file encryption (local and embedded replica modes)\n- `remote_encryption_key`: Used for Turso encrypted databases (remote and embedded replica modes)\n- Remote encryption keys should be base64-encoded as per Turso's encryption requirements\n- See [Turso Encryption Documentation](https://docs.turso.tech/cloud/encryption) for more details\n\n### Embedded Replica Synchronisation\n\nWhen using embedded replica mode (`sync: true`), the library automatically handles synchronisation between your local database and Turso cloud. However, you can also trigger manual sync when needed.\n\n#### Automatic Sync Behaviour\n\n```elixir\n# Automatic sync is enabled with sync: true\n{:ok, state} = EctoLibSql.connect(\n  database: \"local.db\",\n  uri: \"libsql://your-db.turso.io\",\n  auth_token: \"your-token\",\n  sync: true  # Automatic sync enabled\n)\n\n# Writes and reads work normally - sync happens automatically\nEctoLibSql.handle_execute(\"INSERT INTO users (name) VALUES (?)\", [\"Alice\"], [], state)\nEctoLibSql.handle_execute(\"SELECT * FROM users\", [], [], state)\n```\n\n**How automatic sync works:**\n- Initial sync happens when you first connect\n- Changes are synced automatically in the background\n- You don't need to call `sync/1` in most applications\n\n#### Manual Sync Control\n\nFor specific use cases, you can manually trigger synchronisation:\n\n```elixir\n# Force immediate sync after critical operation\nEctoLibSql.handle_execute(\"INSERT INTO orders (total) VALUES (?)\", [1000.00], [], state)\n{:ok, _} = EctoLibSql.Native.sync(state)  # Ensure synced to cloud immediately\n\n# Before shutdown - ensure all changes are persisted\n{:ok, _} = EctoLibSql.Native.sync(state)\n:ok = EctoLibSql.disconnect([], state)\n\n# Coordinate between multiple replicas\n{:ok, _} = EctoLibSql.Native.sync(replica1)  # Push local changes\n{:ok, _} = EctoLibSql.Native.sync(replica2)  # Pull those changes on another replica\n```\n\n**When to use manual sync:**\n- **Critical operations**: Immediately after writes that must be durable\n- **Before shutdown**: Ensuring all local changes reach the cloud\n- **Coordinating replicas**: When multiple replicas need consistent data immediately\n- **After batch operations**: Following bulk inserts/updates\n\n**When you DON'T need manual sync:**\n- Normal application reads/writes (automatic sync handles this)\n- Most CRUD operations (background sync is sufficient)\n- Development and testing (automatic sync is fine)\n\n#### Disabling Automatic Sync\n\nYou can disable automatic sync and rely entirely on manual control:\n\n```elixir\n# Disable automatic sync\n{:ok, state} = EctoLibSql.connect(\n  database: \"local.db\",\n  uri: \"libsql://your-db.turso.io\",\n  auth_token: \"your-token\",\n  sync: false  # Manual sync only\n)\n\n# Make local changes (not synced yet)\nEctoLibSql.handle_execute(\"INSERT INTO users (name) VALUES (?)\", [\"Alice\"], [], state)\n\n# Manually synchronise when ready\n{:ok, _} = EctoLibSql.Native.sync(state)\n```\n\nThis is useful for offline-first applications or when you want explicit control over when data syncs.\n\n## Configuration Options\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `database` | string | Path to local SQLite database file |\n| `uri` | string | Remote LibSQL server URI (e.g., `libsql://...` or `wss://...`) |\n| `auth_token` | string | Authentication token for remote connections |\n| `sync` | boolean | Enable automatic synchronisation for embedded replicas |\n| `encryption_key` | string | Encryption key (32+ characters) for local database file encryption (AES-256-CBC) |\n| `remote_encryption_key` | string | Base64-encoded encryption key for Turso encrypted databases |\n\n## Connection Modes\n\nThe adapter automatically detects the connection mode based on the options provided:\n\n### Local Mode\nOnly `database` specified - stores data in a local SQLite file:\n\n```elixir\nconfig :my_app, MyApp.Repo,\n  adapter: Ecto.Adapters.LibSql,\n  database: \"my_app.db\"\n```\n\n### Remote Mode\n`uri` and `auth_token` specified - connects directly to Turso cloud:\n\n```elixir\nconfig :my_app, MyApp.Repo,\n  adapter: Ecto.Adapters.LibSql,\n  uri: \"libsql://your-database.turso.io\",\n  auth_token: System.get_env(\"TURSO_AUTH_TOKEN\")\n```\n\n### Embedded Replica Mode (Recommended for Production)\nAll of `database`, `uri`, `auth_token`, and `sync` specified - local file with cloud synchronisation:\n\n```elixir\nconfig :my_app, MyApp.Repo,\n  adapter: Ecto.Adapters.LibSql,\n  database: \"replica.db\",\n  uri: \"libsql://your-database.turso.io\",\n  auth_token: System.get_env(\"TURSO_AUTH_TOKEN\"),\n  sync: true\n```\n\nThis mode provides microsecond read latency (local file) with automatic cloud backup. Synchronisation happens automatically in the background - see the [Embedded Replica Synchronisation](#embedded-replica-synchronisation) section for details on sync behaviour and manual sync control.\n\n## Transaction Behaviours\n\nControl transaction locking behaviour and use savepoints for partial rollback:\n\n```elixir\n# Deferred (default) - locks acquired on first write\n{:ok, state} = EctoLibSql.Native.begin(state, behavior: :deferred)\n\n# Immediate - acquire write lock immediately\n{:ok, state} = EctoLibSql.Native.begin(state, behavior: :immediate)\n\n# Read-only - read lock only\n{:ok, state} = EctoLibSql.Native.begin(state, behavior: :read_only)\n\n# Savepoints for nested transaction-like behaviour\n{:ok, state} = EctoLibSql.Native.create_savepoint(state, \"sp1\")\n# ... operations ...\n{:ok, state} = EctoLibSql.Native.rollback_to_savepoint_by_name(state, \"sp1\")\n# or\n{:ok, state} = EctoLibSql.Native.release_savepoint_by_name(state, \"sp1\")\n```\n\n## Metadata Functions\n\n```elixir\n# Get last inserted row ID\nrowid = EctoLibSql.Native.get_last_insert_rowid(state)\n\n# Get number of rows changed by last statement\nchanges = EctoLibSql.Native.get_changes(state)\n\n# Get total rows changed since connection opened\ntotal = EctoLibSql.Native.get_total_changes(state)\n\n# Check if in autocommit mode (not in transaction)\nautocommit? = EctoLibSql.Native.get_is_autocommit(state)\n```\n\n## License\n\nApache 2.0\n\n## Credits\n\nThis library is a fork of [libsqlex](https://github.com/danawanb/libsqlex) by [danawanb](https://github.com/danawanb), extended from a DBConnection adapter to a full Ecto adapter with additional features including vector similarity search, database encryption, batch operations, prepared statements, and comprehensive documentation.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Focean%2Fecto_libsql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Focean%2Fecto_libsql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Focean%2Fecto_libsql/lists"}