Skip to content

Commit c744cd5

Browse files
committed
feat: support attached databases
- This PR plumbs the feature through (TS -> commands -> wrapper) - Introducing a new builder pattern to chain the attach() fn - The attach() fn can be chained with any query fn - This prevents introducing optional attached db params everywhere - Unfortunately we never set up eslint in this repo so this PR adds that - This has resulted in a lot of changes to the TS files - However this is a good time to do it because the builder pattern meant significant changes to the TS anyway - Also doubled back to add tests for the new attached.rs API which was added in the last PR
1 parent dabf445 commit c744cd5

18 files changed

Lines changed: 5234 additions & 551 deletions

README.md

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,67 @@ To rollback instead of committing:
289289
await tx.rollback()
290290
```
291291

292+
### Cross-Database Queries
293+
294+
Attach other SQLite databases to run queries across multiple database files.
295+
Each attached database gets a schema name that acts as a namespace for its
296+
tables.
297+
298+
**Builder Pattern:** All query methods (`execute`, `executeTransaction`,
299+
`fetchAll`, `fetchOne`) return builders that support `.attach()` for
300+
cross-database operations.
301+
302+
```typescript
303+
// Join data from multiple databases
304+
const results = await db.fetchAll(
305+
'SELECT u.name, o.total FROM users u JOIN orders.orders o ON u.id = o.user_id',
306+
[]
307+
).attach([
308+
{
309+
databasePath: 'orders.db',
310+
schemaName: 'orders',
311+
mode: 'readOnly'
312+
}
313+
])
314+
315+
// Update main database using data from attached database
316+
await db.execute(
317+
'UPDATE todos SET status = $1 WHERE id IN (SELECT todo_id FROM archive.completed)',
318+
['archived']
319+
).attach([
320+
{
321+
databasePath: 'archive.db',
322+
schemaName: 'archive',
323+
mode: 'readOnly'
324+
}
325+
])
326+
327+
// Atomic writes across multiple databases
328+
await db.executeTransaction([
329+
['INSERT INTO main.orders (user_id, total) VALUES ($1, $2)', [userId, total]],
330+
['UPDATE stats.order_count SET count = count + 1', []]
331+
]).attach([
332+
{
333+
databasePath: 'stats.db',
334+
schemaName: 'stats',
335+
mode: 'readWrite'
336+
}
337+
])
338+
```
339+
340+
**Attachment Modes:**
341+
342+
* `readOnly` - Read-only access (can be used with read or write operations)
343+
* `readWrite` - Read-write access (requires write operation, holds write
344+
lock)
345+
346+
**Important:**
347+
348+
* Attached databases are automatically detached after query completion
349+
* Read-write attachments acquire write locks on all involved databases
350+
* Attachments are connection-scoped and don't persist across queries
351+
* Main database is always accessible without a schema prefix
352+
292353
### Error Handling
293354

294355
```typescript
@@ -315,9 +376,9 @@ Common error codes:
315376
### Closing and Removing
316377

317378
```typescript
318-
await db.close() // Close this connection
319-
await Database.closeAll() // Close all connections
320-
await db.remove() // Close and DELETE database file(s) - irreversible!
379+
await db.close() // Close this connection
380+
await Database.close_all() // Close all connections
381+
await db.remove() // Close and DELETE database file(s) - irreversible!
321382
```
322383

323384
## API Reference
@@ -328,7 +389,7 @@ await db.remove() // Close and DELETE database file(s) - irreversible!
328389
| ------ | ----------- |
329390
| `Database.load(path, config?)` | Connect and return Database instance (or existing) |
330391
| `Database.get(path)` | Get instance without connecting (lazy init) |
331-
| `Database.closeAll()` | Close all database connections |
392+
| `Database.close_all()` | Close all database connections |
332393

333394
### Instance Methods
334395

@@ -342,6 +403,16 @@ await db.remove() // Close and DELETE database file(s) - irreversible!
342403
| `close()` | Close connection, returns `true` if was loaded |
343404
| `remove()` | Close and delete database file(s), returns `true` if was loaded |
344405

406+
### Builder Methods
407+
408+
All query methods (`execute`, `executeTransaction`, `fetchAll`, `fetchOne`)
409+
return builders that are directly awaitable and support method chaining:
410+
411+
| Method | Description |
412+
| ------ | ----------- |
413+
| `attach(specs)` | Attach databases for cross-database queries, returns `this` |
414+
| `await builder` | Execute the query (builders implement `PromiseLike`) |
415+
345416
### InterruptibleTransaction Methods
346417

347418
| Method | Description |
@@ -364,6 +435,12 @@ interface CustomConfig {
364435
idleTimeoutSecs?: number // default: 30
365436
}
366437

438+
interface AttachedDatabaseSpec {
439+
databasePath: string // Path relative to app config directory
440+
schemaName: string // Schema name for accessing tables (e.g., 'orders')
441+
mode: 'readOnly' | 'readWrite'
442+
}
443+
367444
interface SqliteError {
368445
code: string
369446
message: string

api-iife.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)