Skip to content

Commit 5e02103

Browse files
committed
Add migrate function
This new function can be used to do migrations on both schema databases and regular databases. It is mostly dedicated to schema migration tools. Signed-off-by: Piotr Jastrzebski <piotr@chiselstrike.com>
1 parent b7a5475 commit 5e02103

6 files changed

Lines changed: 159 additions & 0 deletions

File tree

packages/libsql-client-wasm/src/wasm.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,33 @@ export class Sqlite3Client implements Client {
171171
}
172172
}
173173

174+
async migrate(
175+
stmts: Array<InStatement>,
176+
): Promise<Array<ResultSet>> {
177+
this.#checkNotClosed();
178+
const db = this.#getDb();
179+
try {
180+
executeStmt(db, "PRAGMA foreign_keys=off", this.#intMode);
181+
executeStmt(db, transactionModeToBegin("deferred"), this.#intMode);
182+
const resultSets = stmts.map((stmt) => {
183+
if (!inTransaction(db)) {
184+
throw new LibsqlError(
185+
"The transaction has been rolled back",
186+
"TRANSACTION_CLOSED",
187+
);
188+
}
189+
return executeStmt(db, stmt, this.#intMode);
190+
});
191+
executeStmt(db, "COMMIT", this.#intMode);
192+
return resultSets;
193+
} finally {
194+
if (inTransaction(db)) {
195+
executeStmt(db, "ROLLBACK", this.#intMode);
196+
}
197+
executeStmt(db, "PRAGMA foreign_keys=on", this.#intMode);
198+
}
199+
}
200+
174201
async transaction(mode: TransactionMode = "write"): Promise<Transaction> {
175202
const db = this.#getDb();
176203
executeStmt(db, transactionModeToBegin(mode), this.#intMode);

packages/libsql-client/src/hrana.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,11 @@ export async function executeHranaBatch(
252252
version: hrana.ProtocolVersion,
253253
batch: hrana.Batch,
254254
hranaStmts: Array<hrana.Stmt>,
255+
disableForeignKeys: boolean = false,
255256
): Promise<Array<ResultSet>> {
257+
if (disableForeignKeys) {
258+
batch.step().run("PRAGMA foreign_keys=off")
259+
}
256260
const beginStep = batch.step();
257261
const beginPromise = beginStep.run(transactionModeToBegin(mode));
258262

@@ -282,6 +286,9 @@ export async function executeHranaBatch(
282286
.step()
283287
.condition(hrana.BatchCond.not(hrana.BatchCond.ok(commitStep)));
284288
rollbackStep.run("ROLLBACK").catch((_) => undefined);
289+
if (disableForeignKeys) {
290+
batch.step().run("PRAGMA foreign_keys=on")
291+
}
285292

286293
await batch.execute();
287294

packages/libsql-client/src/http.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,40 @@ export class HttpClient implements Client {
180180
});
181181
}
182182

183+
async migrate(
184+
stmts: Array<InStatement>,
185+
): Promise<Array<ResultSet>> {
186+
return this.limit<Array<ResultSet>>(async () => {
187+
try {
188+
const hranaStmts = stmts.map(stmtToHrana);
189+
const version = await this.#client.getVersion();
190+
191+
// Pipeline all operations, so `hrana.HttpClient` can open the stream, execute the batch and
192+
// close the stream in a single HTTP request.
193+
let resultsPromise: Promise<Array<ResultSet>>;
194+
const stream = this.#client.openStream();
195+
try {
196+
const batch = stream.batch(false);
197+
resultsPromise = executeHranaBatch(
198+
"deferred",
199+
version,
200+
batch,
201+
hranaStmts,
202+
true,
203+
);
204+
} finally {
205+
stream.closeGracefully();
206+
}
207+
208+
const results = await resultsPromise;
209+
210+
return results;
211+
} catch (e) {
212+
throw mapHranaError(e);
213+
}
214+
});
215+
}
216+
183217
async transaction(
184218
mode: TransactionMode = "write",
185219
): Promise<HttpTransaction> {

packages/libsql-client/src/sqlite3.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,33 @@ export class Sqlite3Client implements Client {
167167
}
168168
}
169169

170+
async migrate(
171+
stmts: Array<InStatement>,
172+
): Promise<Array<ResultSet>> {
173+
this.#checkNotClosed();
174+
const db = this.#getDb();
175+
try {
176+
executeStmt(db, "PRAGMA foreign_keys=off", this.#intMode);
177+
executeStmt(db, transactionModeToBegin("deferred"), this.#intMode);
178+
const resultSets = stmts.map((stmt) => {
179+
if (!db.inTransaction) {
180+
throw new LibsqlError(
181+
"The transaction has been rolled back",
182+
"TRANSACTION_CLOSED",
183+
);
184+
}
185+
return executeStmt(db, stmt, this.#intMode);
186+
});
187+
executeStmt(db, "COMMIT", this.#intMode);
188+
return resultSets;
189+
} finally {
190+
if (db.inTransaction) {
191+
executeStmt(db, "ROLLBACK", this.#intMode);
192+
}
193+
executeStmt(db, "PRAGMA foreign_keys=on", this.#intMode);
194+
}
195+
}
196+
170197
async transaction(mode: TransactionMode = "write"): Promise<Transaction> {
171198
const db = this.#getDb();
172199
executeStmt(db, transactionModeToBegin(mode), this.#intMode);

packages/libsql-client/src/ws.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,37 @@ export class WsClient implements Client {
226226
});
227227
}
228228

229+
async migrate(
230+
stmts: Array<InStatement>,
231+
): Promise<Array<ResultSet>> {
232+
return this.limit<Array<ResultSet>>(async () => {
233+
const streamState = await this.#openStream();
234+
try {
235+
const hranaStmts = stmts.map(stmtToHrana);
236+
const version = await streamState.conn.client.getVersion();
237+
238+
// Schedule all operations synchronously, so they will be pipelined and executed in a single
239+
// network roundtrip.
240+
const batch = streamState.stream.batch(version >= 3);
241+
const resultsPromise = executeHranaBatch(
242+
"deferred",
243+
version,
244+
batch,
245+
hranaStmts,
246+
true,
247+
);
248+
249+
const results = await resultsPromise;
250+
251+
return results;
252+
} catch (e) {
253+
throw mapHranaError(e);
254+
} finally {
255+
this._closeStream(streamState);
256+
}
257+
});
258+
}
259+
229260
async transaction(mode: TransactionMode = "write"): Promise<WsTransaction> {
230261
return this.limit<WsTransaction>(async () => {
231262
const streamState = await this.#openStream();

packages/libsql-core/src/api.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,39 @@ export interface Client {
126126
mode?: TransactionMode,
127127
): Promise<Array<ResultSet>>;
128128

129+
/** Execute a batch of SQL statements in a transaction with PRAGMA foreign_keys=off; before and PRAGMA foreign_keys=on; after.
130+
*
131+
* The batch is executed in its own logical database connection and the statements are wrapped in a
132+
* transaction. This ensures that the batch is applied atomically: either all or no changes are applied.
133+
*
134+
* The transaction mode is `"deferred"`.
135+
*
136+
* If any of the statements in the batch fails with an error, the batch is aborted, the transaction is
137+
* rolled back and the returned promise is rejected.
138+
*
139+
* ```javascript
140+
* const rss = await client.migrate([
141+
* // statement without arguments
142+
* "CREATE TABLE test (a INT)",
143+
*
144+
* // statement with positional arguments
145+
* {
146+
* sql: "INSERT INTO books (name, author, published_at) VALUES (?, ?, ?)",
147+
* args: ["First Impressions", "Jane Austen", 1813],
148+
* },
149+
*
150+
* // statement with named arguments
151+
* {
152+
* sql: "UPDATE books SET name = $new WHERE name = $old",
153+
* args: {old: "First Impressions", new: "Pride and Prejudice"},
154+
* },
155+
* ]);
156+
* ```
157+
*/
158+
migrate(
159+
stmts: Array<InStatement>,
160+
): Promise<Array<ResultSet>>;
161+
129162
/** Start an interactive transaction.
130163
*
131164
* Interactive transactions allow you to interleave execution of SQL statements with your application

0 commit comments

Comments
 (0)