Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ jobs:
run: cargo test --manifest-path native/ecto_libsql/Cargo.toml --all-features

elixir-tests-latest:
name: Elixir 1.18.0 / OTP 27.0 / ${{ matrix.os }}
name: Elixir ${{ matrix.elixir }} / OTP ${{ matrix.otp }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest]
elixir: ['1.18.0']
otp: ['27.0']

steps:
- name: Checkout code
Expand All @@ -87,18 +89,18 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.18.0'
otp-version: '27.0'
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}

- name: Cache Mix dependencies
uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-1.18.0-27.0-${{ hashFiles('**/mix.lock', '**/Cargo.lock') }}
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock', '**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-mix-1.18.0-27.0-
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-

- name: Install Mix dependencies
run: mix deps.get
Expand Down
2 changes: 1 addition & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1516,7 +1516,7 @@ Begins a transaction with specific behaviour.
**Parameters:**
- `state` (EctoLibSql.State): Connection state
- `opts` (keyword list): Options
- `:behavior` - `:deferred`, `:immediate`, `:exclusive`, or `:read_only`
- `:behaviour` - `:deferred`, `:immediate`, `:exclusive`, or `:read_only`

**Returns:** `{:ok, state}` or `{:error, reason}`

Expand Down
73 changes: 64 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,27 +289,82 @@ distance_fn = EctoLibSql.Native.vector_distance_cos("embedding", query_vector)
)
```

### Manual Sync Control
### Embedded Replica Synchronisation

When 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.

#### Automatic Sync Behaviour

```elixir
# Disable automatic sync for embedded replicas
# Automatic sync is enabled with sync: true
{:ok, state} = EctoLibSql.connect(
database: "local.db",
uri: "libsql://your-db.turso.io",
auth_token: "your-token",
sync: false
sync: true # Automatic sync enabled
)

# Make local changes
EctoLibSql.handle_execute(
"INSERT INTO users (name) VALUES (?)",
["Alice"], [], state
# Writes and reads work normally - sync happens automatically
EctoLibSql.handle_execute("INSERT INTO users (name) VALUES (?)", ["Alice"], [], state)
EctoLibSql.handle_execute("SELECT * FROM users", [], [], state)
```

**How automatic sync works:**
- Initial sync happens when you first connect
- Changes are synced automatically in the background
- You don't need to call `sync/1` in most applications

#### Manual Sync Control

For specific use cases, you can manually trigger synchronisation:

```elixir
# Force immediate sync after critical operation
EctoLibSql.handle_execute("INSERT INTO orders (total) VALUES (?)", [1000.00], [], state)
{:ok, _} = EctoLibSql.Native.sync(state) # Ensure synced to cloud immediately

# Before shutdown - ensure all changes are persisted
{:ok, _} = EctoLibSql.Native.sync(state)
:ok = EctoLibSql.disconnect([], state)

# Coordinate between multiple replicas
{:ok, _} = EctoLibSql.Native.sync(replica1) # Push local changes
{:ok, _} = EctoLibSql.Native.sync(replica2) # Pull those changes on another replica
```

**When to use manual sync:**
- **Critical operations**: Immediately after writes that must be durable
- **Before shutdown**: Ensuring all local changes reach the cloud
- **Coordinating replicas**: When multiple replicas need consistent data immediately
- **After batch operations**: Following bulk inserts/updates

**When you DON'T need manual sync:**
- Normal application reads/writes (automatic sync handles this)
- Most CRUD operations (background sync is sufficient)
- Development and testing (automatic sync is fine)

#### Disabling Automatic Sync

You can disable automatic sync and rely entirely on manual control:

```elixir
# Disable automatic sync
{:ok, state} = EctoLibSql.connect(
database: "local.db",
uri: "libsql://your-db.turso.io",
auth_token: "your-token",
sync: false # Manual sync only
)

# Make local changes (not synced yet)
EctoLibSql.handle_execute("INSERT INTO users (name) VALUES (?)", ["Alice"], [], state)

# Manually synchronise when ready
{:ok, "success sync"} = EctoLibSql.Native.sync(state)
{:ok, _} = EctoLibSql.Native.sync(state)
```

This is useful for offline-first applications or when you want explicit control over when data syncs.

## Configuration Options

| Option | Type | Description |
Expand Down Expand Up @@ -355,7 +410,7 @@ config :my_app, MyApp.Repo,
sync: true
```

This mode provides microsecond read latency (local file) with automatic cloud backup.
This 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.

## Transaction Behaviours

Expand Down
2 changes: 1 addition & 1 deletion lib/ecto/adapters/libsql/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Ecto.Adapters.LibSql.Connection do
Implementation of Ecto.Adapters.SQL.Connection for LibSQL.

This module handles SQL query generation and DDL operations for LibSQL/SQLite.
It implements the `Ecto.Adapters.SQL.Connection` behavior, translating Ecto's
It implements the `Ecto.Adapters.SQL.Connection` behaviour, translating Ecto's
query structures into SQLite-compatible SQL.

## Key Responsibilities
Expand Down
2 changes: 1 addition & 1 deletion lib/ecto_libsql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ defmodule EctoLibSql do
@doc """
Begins a new database transaction.

The transaction behavior (deferred/immediate/exclusive) can be controlled
The transaction behaviour (deferred/immediate/exclusive) can be controlled
via options passed to the Native module.
"""
def handle_begin(_opts, state) do
Expand Down
46 changes: 40 additions & 6 deletions lib/ecto_libsql/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,47 @@ defmodule EctoLibSql.Native do
Manually trigger a sync for embedded replicas.

For connections in `:remote_replica` mode, this function forces a
synchronization with the remote Turso database.
synchronisation with the remote Turso database, pulling down any changes
from the remote and pushing local changes up.

## When to Use

In most cases, you don't need to call this manually - automatic sync happens
when you connect with `sync: true`. However, manual sync is useful for:

- **Critical reads after remote writes**: When you need to immediately read
data that was just written to the remote database
- **Before shutdown**: Ensuring all local changes are synced before closing
the connection
- **After batch operations**: Forcing sync after bulk inserts/updates to
ensure data is persisted remotely
- **Coordinating between replicas**: When multiple replicas need to see
consistent data immediately

## Parameters
- state: The connection state
- state: The connection state (must be in `:remote_replica` mode)

## Example
## Returns
- `{:ok, "success sync"}` on successful sync
- `{:error, reason}` if sync fails

## Examples

# Force sync after critical write
{:ok, state} = EctoLibSql.connect(database: "local.db", uri: turso_uri, auth_token: token, sync: true)
{:ok, _, _, state} = EctoLibSql.handle_execute("INSERT INTO users ...", [], [], state)
{:ok, "success sync"} = EctoLibSql.Native.sync(state)

# Ensure sync before shutdown
{:ok, _} = EctoLibSql.Native.sync(state)
:ok = EctoLibSql.disconnect([], state)

## Notes

- Sync is only applicable for `:remote_replica` mode connections
- For `:local` mode, this is a no-op
- For `:remote` mode, data is already on the remote server
- Sync happens synchronously and may take time depending on data size

"""
def sync(%EctoLibSql.State{conn_id: conn_id, mode: mode} = _state) do
Expand Down Expand Up @@ -243,14 +277,14 @@ defmodule EctoLibSql.Native do
end

@doc """
Begin a new transaction with optional behavior control.
Begin a new transaction with optional behaviour control.

## Parameters
- state: The connection state
- opts: Options keyword list
- `:behavior` - Transaction behavior (`:deferred`, `:immediate`, or `:exclusive`), defaults to `:deferred`
- `:behavior` - Transaction behaviour (`:deferred`, `:immediate`, or `:exclusive`), defaults to `:deferred`

## Transaction Behaviors
## Transaction Behaviours

- `:deferred` - Default. Locks are acquired on first write operation
- `:immediate` - Acquires write lock immediately when transaction begins
Expand Down
2 changes: 1 addition & 1 deletion test/ecto_libsql_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ defmodule EctoLibSqlTest do
assert balance == 100.50
end

test "transaction behaviors - deferred and read_only", state do
test "transaction behaviours - deferred and read_only", state do
{:ok, state} = EctoLibSql.connect(state[:opts])

# Test DEFERRED (default)
Expand Down
Loading