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
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ Or specify individual permissions:
"sqlite:allow-load",
"sqlite:allow-fetch-one",
"sqlite:allow-fetch-all",
// etc.
]
}
```
Expand Down Expand Up @@ -187,14 +186,14 @@ All query methods use `$1`, `$2`, etc. syntax with `SqlValue` types:
type SqlValue = string | number | boolean | null | Uint8Array
```

| SQLite Type | TypeScript Type | Notes |
| ----------- | --------------- | ----- |
| TEXT | `string` | Also for DATE, TIME, DATETIME |
| INTEGER | `number` | Integers preserved up to i64 range |
| REAL | `number` | Floating point |
| BOOLEAN | `boolean` | |
| NULL | `null` | |
| BLOB | `Uint8Array` | Binary data |
| SQLite Type | TypeScript Type | Notes |
| ----------- | --------------- | ----------------------------------- |
| TEXT | `string` | Also for DATE, TIME, DATETIME |
| INTEGER | `number` | Integers preserved up to i64 range |
| REAL | `number` | Floating point |
| BOOLEAN | `boolean` | |
| NULL | `null` | |
| BLOB | `Uint8Array` | Binary data |

> **Note:** JavaScript safely represents integers up to ±2^53 - 1. The plugin binds
> integers as SQLite's INTEGER type (i64), maintaining full precision within that range.
Expand Down Expand Up @@ -255,7 +254,7 @@ generated ID or other computed values, then using that data in subsequent writes

```typescript
// Begin transaction with initial insert
const tx = await db.executeInterruptibleTransaction([
let tx = await db.executeInterruptibleTransaction([
['INSERT INTO orders (user_id, total) VALUES ($1, $2)', [userId, 0]]
])

Expand All @@ -267,13 +266,13 @@ const orders = await tx.read<Array<{ id: number }>>(
const orderId = orders[0].id

// Continue transaction with the order ID
const tx2 = await tx.continue([
tx = await tx.continue([
['INSERT INTO order_items (order_id, product_id) VALUES ($1, $2)', [orderId, productId]],
['UPDATE orders SET total = $1 WHERE id = $2', [itemTotal, orderId]]
])

// Commit the transaction
await tx2.commit()
await tx.commit()
```

**Important:**
Expand Down Expand Up @@ -390,6 +389,7 @@ fn init_tracing() {
use tracing_subscriber::{fmt, EnvFilter};
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("trace"));

fmt().with_env_filter(filter).compact().init();
}

Expand Down
71 changes: 71 additions & 0 deletions crates/sqlx-sqlite-conn-mgr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,67 @@ times is safe (already-applied migrations are skipped).
> `Builder::add_migrations()`. The plugin starts migrations at setup and waits for
> completion when `load()` is called.

### Attached Databases

Attach other SQLite databases to enable cross-database queries. Attachments are
connection-scoped and automatically detached when the guard is dropped.

```rust
use sqlx_sqlite_conn_mgr::{SqliteDatabase, AttachedSpec, AttachedMode, acquire_reader_with_attached};
use sqlx::query;
use std::sync::Arc;

async fn example() -> Result<(), sqlx_sqlite_conn_mgr::Error> {
// You must first connect to the main database and any database(s) you intend to
// attach.
let main_db = SqliteDatabase::connect("main.db", None).await?;
let orders_db = SqliteDatabase::connect("orders.db", None).await?;

// Attach orders database for read-only access
let specs = vec![AttachedSpec {
database: orders_db,
schema_name: "orders".to_string(),
mode: AttachedMode::ReadOnly,
}];

let mut conn = acquire_reader_with_attached(&main_db, specs).await?;

// Cross-database query
let rows = query(
"SELECT u.name, o.total
FROM main.users u
JOIN orders.orders o ON u.id = o.user_id"
)
.fetch_all(&mut *conn)
.await?;

// Attached database automatically detached when conn is dropped
Ok(())
}
```

#### Attached Modes

* **`AttachedMode::ReadOnly`**: Attach for read access only. Can be used with
both reader and writer connections.
* **`AttachedMode::ReadWrite`**: Attach for write access. Can only be used with
writer connections. Acquires the attached database's writer lock to ensure
exclusive access.

#### Safety Guarantees

1. **Lock ordering**: Multiple attachments are acquired in alphabetical order by
schema name to prevent deadlocks
2. **Mode validation**: Read-only connections cannot attach databases in
read-write mode (returns `CannotAttachReadWriteToReader` error)
3. **Automatic cleanup**: SQLite automatically detaches databases when connections
close; no manual cleanup required

> **Caution:** Do not bypass this API by executing raw
> `ATTACH DATABASE '/path/to/db.db' AS alias` SQL commands directly. Doing so
> circumvents the connection manager's policies and will result in
> unpredictable behavior, including potential deadlocks.

## API Reference

### `SqliteDatabase`
Expand All @@ -110,6 +171,16 @@ times is safe (already-applied migrations are skipped).
RAII guard for exclusive write access. Derefs to `SqliteConnection`. Connection
returned to pool on drop.

### Attached Database Functions

| Function | Description |
| -------- | ----------- |
| `acquire_reader_with_attached(db, specs)` | Acquire read connection with attached databases |
| `acquire_writer_with_attached(db, specs)` | Acquire writer connection with attached databases |

Returns `AttachedConnection` or `AttachedWriteGuard` respectively. Both guards
deref to `SqliteConnection` and automatically detach databases on drop.

## Design Details

### Read-Only Pool
Expand Down
Loading