Skip to content

Commit 4a43dec

Browse files
authored
Merge pull request #249 from tursodatabase/migrate
Add migrate function
2 parents b7a5475 + 5e02103 commit 4a43dec

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)