mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-20 16:44:19 +01:00
Merge 'Javascript API improvements' from Pekka Enberg
Various improvements: - Make promise API the default for `@tursodatabase/turso` - Add a compatibility test suite for database, serverless, and better- sqlite3 - Improve serverless driver a bit to be more compatible. Closes #2322
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import avaTest from "ava";
|
||||
import turso from "../wrapper.js";
|
||||
import turso from "../sync.js";
|
||||
import sqlite from "better-sqlite3";
|
||||
|
||||
class DualTest {
|
||||
|
||||
@@ -6,12 +6,17 @@
|
||||
"url": "https://github.com/tursodatabase/turso"
|
||||
},
|
||||
"description": "The Turso database library",
|
||||
"main": "wrapper.js",
|
||||
"main": "promise.js",
|
||||
"exports": {
|
||||
".": "./promise.js",
|
||||
"./sync": "./sync.js"
|
||||
},
|
||||
"files": [
|
||||
"browser.js",
|
||||
"index.js",
|
||||
"promise.js",
|
||||
"sqlite-error.js",
|
||||
"wrapper.js"
|
||||
"sync.js"
|
||||
],
|
||||
"types": "index.d.ts",
|
||||
"napi": {
|
||||
@@ -46,4 +51,4 @@
|
||||
"version": "napi version"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
315
bindings/javascript/sync.js
Normal file
315
bindings/javascript/sync.js
Normal file
@@ -0,0 +1,315 @@
|
||||
"use strict";
|
||||
|
||||
const { Database: NativeDB } = require("./index.js");
|
||||
|
||||
const SqliteError = require("./sqlite-error.js");
|
||||
|
||||
const convertibleErrorTypes = { TypeError };
|
||||
const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]";
|
||||
|
||||
function convertError(err) {
|
||||
if ((err.code ?? "").startsWith(CONVERTIBLE_ERROR_PREFIX)) {
|
||||
return createErrorByName(
|
||||
err.code.substring(CONVERTIBLE_ERROR_PREFIX.length),
|
||||
err.message,
|
||||
);
|
||||
}
|
||||
|
||||
return new SqliteError(err.message, err.code, err.rawCode);
|
||||
}
|
||||
|
||||
function createErrorByName(name, message) {
|
||||
const ErrorConstructor = convertibleErrorTypes[name];
|
||||
if (!ErrorConstructor) {
|
||||
throw new Error(`unknown error type ${name} from Turso`);
|
||||
}
|
||||
|
||||
return new ErrorConstructor(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Database represents a connection that can prepare and execute SQL statements.
|
||||
*/
|
||||
class Database {
|
||||
/**
|
||||
* Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} path - Path to the database file.
|
||||
* @param {Object} opts - Options for database behavior.
|
||||
* @param {boolean} [opts.readonly=false] - Open the database in read-only mode.
|
||||
* @param {boolean} [opts.fileMustExist=false] - If true, throws if database file does not exist.
|
||||
* @param {number} [opts.timeout=0] - Timeout duration in milliseconds for database operations. Defaults to 0 (no timeout).
|
||||
*/
|
||||
constructor(path, opts = {}) {
|
||||
opts.readonly = opts.readonly === undefined ? false : opts.readonly;
|
||||
opts.fileMustExist =
|
||||
opts.fileMustExist === undefined ? false : opts.fileMustExist;
|
||||
opts.timeout = opts.timeout === undefined ? 0 : opts.timeout;
|
||||
|
||||
this.db = new NativeDB(path, opts);
|
||||
this.memory = this.db.memory;
|
||||
const db = this.db;
|
||||
|
||||
Object.defineProperties(this, {
|
||||
inTransaction: {
|
||||
get() {
|
||||
return db.inTransaction();
|
||||
},
|
||||
},
|
||||
name: {
|
||||
get() {
|
||||
return path;
|
||||
},
|
||||
},
|
||||
readonly: {
|
||||
get() {
|
||||
return opts.readonly;
|
||||
},
|
||||
},
|
||||
open: {
|
||||
get() {
|
||||
return this.db.open;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a SQL statement for execution.
|
||||
*
|
||||
* @param {string} sql - The SQL statement string to prepare.
|
||||
*/
|
||||
prepare(sql) {
|
||||
if (!sql) {
|
||||
throw new RangeError("The supplied SQL string contains no statements");
|
||||
}
|
||||
|
||||
try {
|
||||
return new Statement(this.db.prepare(sql), this);
|
||||
} catch (err) {
|
||||
throw convertError(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that executes the given function in a transaction.
|
||||
*
|
||||
* @param {function} fn - The function to wrap in a transaction.
|
||||
*/
|
||||
transaction(fn) {
|
||||
if (typeof fn !== "function")
|
||||
throw new TypeError("Expected first argument to be a function");
|
||||
|
||||
const db = this;
|
||||
const wrapTxn = (mode) => {
|
||||
return (...bindParameters) => {
|
||||
db.exec("BEGIN " + mode);
|
||||
try {
|
||||
const result = fn(...bindParameters);
|
||||
db.exec("COMMIT");
|
||||
return result;
|
||||
} catch (err) {
|
||||
db.exec("ROLLBACK");
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
};
|
||||
const properties = {
|
||||
default: { value: wrapTxn("") },
|
||||
deferred: { value: wrapTxn("DEFERRED") },
|
||||
immediate: { value: wrapTxn("IMMEDIATE") },
|
||||
exclusive: { value: wrapTxn("EXCLUSIVE") },
|
||||
database: { value: this, enumerable: true },
|
||||
};
|
||||
Object.defineProperties(properties.default.value, properties);
|
||||
Object.defineProperties(properties.deferred.value, properties);
|
||||
Object.defineProperties(properties.immediate.value, properties);
|
||||
Object.defineProperties(properties.exclusive.value, properties);
|
||||
return properties.default.value;
|
||||
}
|
||||
|
||||
pragma(source, options) {
|
||||
if (options == null) options = {};
|
||||
|
||||
if (typeof source !== "string")
|
||||
throw new TypeError("Expected first argument to be a string");
|
||||
|
||||
if (typeof options !== "object")
|
||||
throw new TypeError("Expected second argument to be an options object");
|
||||
|
||||
const simple = options["simple"];
|
||||
const pragma = `PRAGMA ${source}`;
|
||||
|
||||
return simple
|
||||
? this.db.pragma(source, { simple: true })
|
||||
: this.db.pragma(source);
|
||||
}
|
||||
|
||||
backup(filename, options) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
serialize(options) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
function(name, options, fn) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
aggregate(name, options) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
table(name, factory) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
loadExtension(path) {
|
||||
this.db.loadExtension(path);
|
||||
}
|
||||
|
||||
maxWriteReplicationIndex() {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a SQL statement.
|
||||
*
|
||||
* @param {string} sql - The SQL statement string to execute.
|
||||
*/
|
||||
exec(sql) {
|
||||
try {
|
||||
this.db.exec(sql);
|
||||
} catch (err) {
|
||||
throw convertError(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrupts the database connection.
|
||||
*/
|
||||
interrupt() {
|
||||
this.db.interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the database connection.
|
||||
*/
|
||||
close() {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statement represents a prepared SQL statement that can be executed.
|
||||
*/
|
||||
class Statement {
|
||||
constructor(stmt, database) {
|
||||
this.stmt = stmt;
|
||||
this.db = database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle raw mode.
|
||||
*
|
||||
* @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled.
|
||||
*/
|
||||
raw(raw) {
|
||||
this.stmt.raw(raw);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle pluck mode.
|
||||
*
|
||||
* @param pluckMode Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled.
|
||||
*/
|
||||
pluck(pluckMode) {
|
||||
this.stmt.pluck(pluckMode);
|
||||
return this;
|
||||
}
|
||||
|
||||
get source() {
|
||||
return this.stmt.source;
|
||||
}
|
||||
|
||||
get reader() {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
get source() {
|
||||
return this.stmt.source;
|
||||
}
|
||||
|
||||
get database() {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the SQL statement and returns an info object.
|
||||
*/
|
||||
run(...bindParameters) {
|
||||
return this.stmt.run(bindParameters.flat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the SQL statement and returns the first row.
|
||||
*
|
||||
* @param bindParameters - The bind parameters for executing the statement.
|
||||
*/
|
||||
get(...bindParameters) {
|
||||
return this.stmt.get(bindParameters.flat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the SQL statement and returns an iterator to the resulting rows.
|
||||
*
|
||||
* @param bindParameters - The bind parameters for executing the statement.
|
||||
*/
|
||||
*iterate(...bindParameters) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the SQL statement and returns an array of the resulting rows.
|
||||
*
|
||||
* @param bindParameters - The bind parameters for executing the statement.
|
||||
*/
|
||||
all(...bindParameters) {
|
||||
return this.stmt.all(bindParameters.flat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrupts the statement.
|
||||
*/
|
||||
interrupt() {
|
||||
this.stmt.interrupt();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the columns in the result set returned by this prepared statement.
|
||||
*/
|
||||
columns() {
|
||||
return this.stmt.columns();
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the given parameters to the statement _permanently_
|
||||
*
|
||||
* @param bindParameters - The bind parameters for binding the statement.
|
||||
* @returns this - Statement with binded parameters
|
||||
*/
|
||||
bind(...bindParameters) {
|
||||
try {
|
||||
return new Statement(this.stmt.bind(bindParameters.flat()), this.db);
|
||||
} catch (err) {
|
||||
throw convertError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Database;
|
||||
module.exports.SqliteError = SqliteError;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Connection, connect, type Config as TursoConfig } from './connection.js';
|
||||
import { Session, type SessionConfig } from './session.js';
|
||||
import { DatabaseError } from './error.js';
|
||||
|
||||
/**
|
||||
@@ -127,17 +127,17 @@ export interface Client {
|
||||
}
|
||||
|
||||
class LibSQLClient implements Client {
|
||||
private connection: Connection;
|
||||
private session: Session;
|
||||
private _closed = false;
|
||||
|
||||
constructor(config: Config) {
|
||||
this.validateConfig(config);
|
||||
|
||||
const tursoConfig: TursoConfig = {
|
||||
const sessionConfig: SessionConfig = {
|
||||
url: config.url,
|
||||
authToken: config.authToken || ''
|
||||
};
|
||||
this.connection = connect(tursoConfig);
|
||||
this.session = new Session(sessionConfig);
|
||||
}
|
||||
|
||||
private validateConfig(config: Config): void {
|
||||
@@ -246,8 +246,15 @@ class LibSQLClient implements Client {
|
||||
normalizedStmt = this.normalizeStatement(stmtOrSql);
|
||||
}
|
||||
|
||||
const result = await this.connection.execute(normalizedStmt.sql, normalizedStmt.args);
|
||||
return this.convertResult(result);
|
||||
await this.session.sequence(normalizedStmt.sql);
|
||||
// Return empty result set for sequence execution
|
||||
return this.convertResult({
|
||||
columns: [],
|
||||
columnTypes: [],
|
||||
rows: [],
|
||||
rowsAffected: 0,
|
||||
lastInsertRowid: undefined
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new LibsqlError(error.message, "EXECUTE_ERROR");
|
||||
}
|
||||
@@ -264,7 +271,7 @@ class LibSQLClient implements Client {
|
||||
return normalized.sql; // For now, ignore args in batch
|
||||
});
|
||||
|
||||
const result = await this.connection.batch(sqlStatements, mode);
|
||||
const result = await this.session.batch(sqlStatements);
|
||||
|
||||
// Return array of result sets (simplified - actual implementation would be more complex)
|
||||
return [this.convertResult(result)];
|
||||
@@ -283,7 +290,15 @@ class LibSQLClient implements Client {
|
||||
}
|
||||
|
||||
async executeMultiple(sql: string): Promise<void> {
|
||||
throw new LibsqlError("Execute multiple not implemented", "NOT_IMPLEMENTED");
|
||||
try {
|
||||
if (this._closed) {
|
||||
throw new LibsqlError("Client is closed", "CLIENT_CLOSED");
|
||||
}
|
||||
|
||||
await this.session.sequence(sql);
|
||||
} catch (error: any) {
|
||||
throw new LibsqlError(error.message, "EXECUTE_MULTIPLE_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async sync(): Promise<any> {
|
||||
|
||||
@@ -44,7 +44,6 @@ export class Connection {
|
||||
* Execute a SQL statement and return all results.
|
||||
*
|
||||
* @param sql - The SQL statement to execute
|
||||
* @param args - Optional array of parameter values
|
||||
* @returns Promise resolving to the complete result set
|
||||
*
|
||||
* @example
|
||||
@@ -53,8 +52,8 @@ export class Connection {
|
||||
* console.log(result.rows);
|
||||
* ```
|
||||
*/
|
||||
async execute(sql: string, args: any[] = []): Promise<any> {
|
||||
return this.session.execute(sql, args);
|
||||
async exec(sql: string): Promise<any> {
|
||||
return this.session.sequence(sql);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +76,17 @@ export class Connection {
|
||||
async batch(statements: string[], mode?: string): Promise<any> {
|
||||
return this.session.batch(statements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a pragma.
|
||||
*
|
||||
* @param pragma - The pragma to execute
|
||||
* @returns Promise resolving to the result of the pragma
|
||||
*/
|
||||
async pragma(pragma: string): Promise<any> {
|
||||
const sql = `PRAGMA ${pragma}`;
|
||||
return this.session.execute(sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,9 +47,14 @@ export interface BatchRequest {
|
||||
};
|
||||
}
|
||||
|
||||
export interface SequenceRequest {
|
||||
type: 'sequence';
|
||||
sql: string;
|
||||
}
|
||||
|
||||
export interface PipelineRequest {
|
||||
baton: string | null;
|
||||
requests: (ExecuteRequest | BatchRequest)[];
|
||||
requests: (ExecuteRequest | BatchRequest | SequenceRequest)[];
|
||||
}
|
||||
|
||||
export interface PipelineResponse {
|
||||
@@ -58,8 +63,8 @@ export interface PipelineResponse {
|
||||
results: Array<{
|
||||
type: 'ok' | 'error';
|
||||
response?: {
|
||||
type: 'execute' | 'batch';
|
||||
result: ExecuteResult;
|
||||
type: 'execute' | 'batch' | 'sequence';
|
||||
result?: ExecuteResult;
|
||||
};
|
||||
error?: {
|
||||
message: string;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
executeCursor,
|
||||
executePipeline,
|
||||
encodeValue,
|
||||
decodeValue,
|
||||
type CursorRequest,
|
||||
type CursorResponse,
|
||||
type CursorEntry
|
||||
type CursorEntry,
|
||||
type PipelineRequest,
|
||||
type SequenceRequest
|
||||
} from './protocol.js';
|
||||
import { DatabaseError } from './error.js';
|
||||
|
||||
@@ -214,4 +217,35 @@ export class Session {
|
||||
lastInsertRowid
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a sequence of SQL statements separated by semicolons.
|
||||
*
|
||||
* @param sql - SQL string containing multiple statements separated by semicolons
|
||||
* @returns Promise resolving when all statements are executed
|
||||
*/
|
||||
async sequence(sql: string): Promise<void> {
|
||||
const request: PipelineRequest = {
|
||||
baton: this.baton,
|
||||
requests: [{
|
||||
type: "sequence",
|
||||
sql: sql
|
||||
} as SequenceRequest]
|
||||
};
|
||||
|
||||
const response = await executePipeline(this.baseUrl, this.config.authToken, request);
|
||||
|
||||
this.baton = response.baton;
|
||||
if (response.base_url) {
|
||||
this.baseUrl = response.base_url;
|
||||
}
|
||||
|
||||
// Check for errors in the response
|
||||
if (response.results && response.results[0]) {
|
||||
const result = response.results[0];
|
||||
if (result.type === "error") {
|
||||
throw new DatabaseError(result.error?.message || 'Sequence execution failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
428
testing/javascript/__test__/async.test.js
Normal file
428
testing/javascript/__test__/async.test.js
Normal file
@@ -0,0 +1,428 @@
|
||||
import test from "ava";
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
|
||||
|
||||
test.beforeEach(async (t) => {
|
||||
const [db, errorType] = await connect();
|
||||
await db.exec(`
|
||||
DROP TABLE IF EXISTS users;
|
||||
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
|
||||
`);
|
||||
await db.exec(
|
||||
"INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"
|
||||
);
|
||||
await db.exec(
|
||||
"INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')"
|
||||
);
|
||||
t.context = {
|
||||
db,
|
||||
errorType
|
||||
};
|
||||
});
|
||||
|
||||
test.after.always(async (t) => {
|
||||
if (t.context.db != undefined) {
|
||||
t.context.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.serial("Open in-memory database", async (t) => {
|
||||
const [db] = await connect(":memory:");
|
||||
t.is(db.memory, true);
|
||||
});
|
||||
|
||||
test.skip("Statement.prepare() error", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
await t.throwsAsync(async () => {
|
||||
return await db.prepare("SYNTAX ERROR");
|
||||
}, {
|
||||
instanceOf: t.context.errorType,
|
||||
message: 'near "SYNTAX": syntax error'
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("Statement.run() [positional]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = await db.prepare("INSERT INTO users(name, email) VALUES (?, ?)");
|
||||
const info = stmt.run(["Carol", "carol@example.net"]);
|
||||
t.is(info.changes, 1);
|
||||
t.is(info.lastInsertRowid, 3);
|
||||
});
|
||||
|
||||
test.serial("Statement.get() [no parameters]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
var stmt = 0;
|
||||
|
||||
stmt = await db.prepare("SELECT * FROM users");
|
||||
t.is(stmt.get().name, "Alice");
|
||||
t.deepEqual(await stmt.raw().get(), [1, 'Alice', 'alice@example.org']);
|
||||
});
|
||||
|
||||
test.serial("Statement.get() [positional]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
var stmt = 0;
|
||||
|
||||
stmt = await db.prepare("SELECT * FROM users WHERE id = ?");
|
||||
t.is(stmt.get(0), undefined);
|
||||
t.is(stmt.get([0]), undefined);
|
||||
t.is(stmt.get(1).name, "Alice");
|
||||
t.is(stmt.get(2).name, "Bob");
|
||||
|
||||
stmt = await db.prepare("SELECT * FROM users WHERE id = ?1");
|
||||
t.is(stmt.get({1: 0}), undefined);
|
||||
t.is(stmt.get({1: 1}).name, "Alice");
|
||||
t.is(stmt.get({1: 2}).name, "Bob");
|
||||
});
|
||||
|
||||
test.serial("Statement.get() [named]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
var stmt = undefined;
|
||||
|
||||
stmt = await db.prepare("SELECT * FROM users WHERE id = :id");
|
||||
t.is(stmt.get({ id: 0 }), undefined);
|
||||
t.is(stmt.get({ id: 1 }).name, "Alice");
|
||||
t.is(stmt.get({ id: 2 }).name, "Bob");
|
||||
|
||||
stmt = await db.prepare("SELECT * FROM users WHERE id = @id");
|
||||
t.is(stmt.get({ id: 0 }), undefined);
|
||||
t.is(stmt.get({ id: 1 }).name, "Alice");
|
||||
t.is(stmt.get({ id: 2 }).name, "Bob");
|
||||
|
||||
stmt = await db.prepare("SELECT * FROM users WHERE id = $id");
|
||||
t.is(stmt.get({ id: 0 }), undefined);
|
||||
t.is(stmt.get({ id: 1 }).name, "Alice");
|
||||
t.is(stmt.get({ id: 2 }).name, "Bob");
|
||||
});
|
||||
|
||||
|
||||
test.serial("Statement.get() [raw]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = await db.prepare("SELECT * FROM users WHERE id = ?");
|
||||
t.deepEqual(stmt.raw().get(1), [1, "Alice", "alice@example.org"]);
|
||||
});
|
||||
|
||||
test.skip("Statement.iterate() [empty]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = await db.prepare("SELECT * FROM users WHERE id = 0");
|
||||
const it = await stmt.iterate();
|
||||
t.is(it.next().done, true);
|
||||
});
|
||||
|
||||
test.skip("Statement.iterate()", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = await db.prepare("SELECT * FROM users");
|
||||
const expected = [1, 2];
|
||||
var idx = 0;
|
||||
for (const row of await stmt.iterate()) {
|
||||
t.is(row.id, expected[idx++]);
|
||||
}
|
||||
});
|
||||
|
||||
test.skip("Statement.all()", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = await db.prepare("SELECT * FROM users");
|
||||
const expected = [
|
||||
{ id: 1, name: "Alice", email: "alice@example.org" },
|
||||
{ id: 2, name: "Bob", email: "bob@example.com" },
|
||||
];
|
||||
t.deepEqual(await stmt.all(), expected);
|
||||
});
|
||||
|
||||
test.skip("Statement.all() [raw]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = await db.prepare("SELECT * FROM users");
|
||||
const expected = [
|
||||
[1, "Alice", "alice@example.org"],
|
||||
[2, "Bob", "bob@example.com"],
|
||||
];
|
||||
t.deepEqual(await stmt.raw().all(), expected);
|
||||
});
|
||||
|
||||
test.skip("Statement.all() [pluck]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = await db.prepare("SELECT * FROM users");
|
||||
const expected = [
|
||||
1,
|
||||
2,
|
||||
];
|
||||
t.deepEqual(await stmt.pluck().all(), expected);
|
||||
});
|
||||
|
||||
test.skip("Statement.all() [default safe integers]", async (t) => {
|
||||
const db = t.context.db;
|
||||
db.defaultSafeIntegers();
|
||||
const stmt = await db.prepare("SELECT * FROM users");
|
||||
const expected = [
|
||||
[1n, "Alice", "alice@example.org"],
|
||||
[2n, "Bob", "bob@example.com"],
|
||||
];
|
||||
t.deepEqual(await stmt.raw().all(), expected);
|
||||
});
|
||||
|
||||
test.skip("Statement.all() [statement safe integers]", async (t) => {
|
||||
const db = t.context.db;
|
||||
const stmt = await db.prepare("SELECT * FROM users");
|
||||
stmt.safeIntegers();
|
||||
const expected = [
|
||||
[1n, "Alice", "alice@example.org"],
|
||||
[2n, "Bob", "bob@example.com"],
|
||||
];
|
||||
t.deepEqual(await stmt.raw().all(), expected);
|
||||
});
|
||||
|
||||
test.skip("Statement.raw() [failure]", async (t) => {
|
||||
const db = t.context.db;
|
||||
const stmt = await db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)");
|
||||
await t.throws(() => {
|
||||
stmt.raw()
|
||||
}, {
|
||||
message: 'The raw() method is only for statements that return data'
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("Statement.columns()", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
var stmt = undefined;
|
||||
|
||||
stmt = await db.prepare("SELECT 1");
|
||||
t.deepEqual(stmt.columns(), [
|
||||
{
|
||||
column: null,
|
||||
database: null,
|
||||
name: '1',
|
||||
table: null,
|
||||
type: null,
|
||||
},
|
||||
]);
|
||||
|
||||
stmt = await db.prepare("SELECT * FROM users WHERE id = ?");
|
||||
t.deepEqual(stmt.columns(), [
|
||||
{
|
||||
column: "id",
|
||||
database: "main",
|
||||
name: "id",
|
||||
table: "users",
|
||||
type: "INTEGER",
|
||||
},
|
||||
{
|
||||
column: "name",
|
||||
database: "main",
|
||||
name: "name",
|
||||
table: "users",
|
||||
type: "TEXT",
|
||||
},
|
||||
{
|
||||
column: "email",
|
||||
database: "main",
|
||||
name: "email",
|
||||
table: "users",
|
||||
type: "TEXT",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test.skip("Database.transaction()", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const insert = await db.prepare(
|
||||
"INSERT INTO users(name, email) VALUES (:name, :email)"
|
||||
);
|
||||
|
||||
const insertMany = db.transaction((users) => {
|
||||
t.is(db.inTransaction, true);
|
||||
for (const user of users) insert.run(user);
|
||||
});
|
||||
|
||||
t.is(db.inTransaction, false);
|
||||
await insertMany([
|
||||
{ name: "Joey", email: "joey@example.org" },
|
||||
{ name: "Sally", email: "sally@example.org" },
|
||||
{ name: "Junior", email: "junior@example.org" },
|
||||
]);
|
||||
t.is(db.inTransaction, false);
|
||||
|
||||
const stmt = await db.prepare("SELECT * FROM users WHERE id = ?");
|
||||
t.is(stmt.get(3).name, "Joey");
|
||||
t.is(stmt.get(4).name, "Sally");
|
||||
t.is(stmt.get(5).name, "Junior");
|
||||
});
|
||||
|
||||
test.skip("Database.transaction().immediate()", async (t) => {
|
||||
const db = t.context.db;
|
||||
const insert = await db.prepare(
|
||||
"INSERT INTO users(name, email) VALUES (:name, :email)"
|
||||
);
|
||||
const insertMany = db.transaction((users) => {
|
||||
t.is(db.inTransaction, true);
|
||||
for (const user of users) insert.run(user);
|
||||
});
|
||||
t.is(db.inTransaction, false);
|
||||
await insertMany.immediate([
|
||||
{ name: "Joey", email: "joey@example.org" },
|
||||
{ name: "Sally", email: "sally@example.org" },
|
||||
{ name: "Junior", email: "junior@example.org" },
|
||||
]);
|
||||
t.is(db.inTransaction, false);
|
||||
});
|
||||
|
||||
test.serial("Database.pragma()", async (t) => {
|
||||
if (process.env.PROVIDER === "serverless") {
|
||||
t.pass("Skipping pragma test for serverless");
|
||||
return;
|
||||
}
|
||||
const db = t.context.db;
|
||||
await db.pragma("cache_size = 2000");
|
||||
t.deepEqual(await db.pragma("cache_size"), [{ "cache_size": 2000 }]);
|
||||
});
|
||||
|
||||
test.skip("errors", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const syntaxError = await t.throwsAsync(async () => {
|
||||
await db.exec("SYNTAX ERROR");
|
||||
}, {
|
||||
instanceOf: t.context.errorType,
|
||||
message: 'near "SYNTAX": syntax error',
|
||||
code: 'SQLITE_ERROR'
|
||||
});
|
||||
|
||||
t.is(syntaxError.rawCode, 1)
|
||||
const noTableError = await t.throwsAsync(async () => {
|
||||
await db.exec("SELECT * FROM missing_table");
|
||||
}, {
|
||||
instanceOf: t.context.errorType,
|
||||
message: "no such table: missing_table",
|
||||
code: 'SQLITE_ERROR'
|
||||
});
|
||||
t.is(noTableError.rawCode, 1)
|
||||
});
|
||||
|
||||
test.skip("Database.prepare() after close()", async (t) => {
|
||||
const db = t.context.db;
|
||||
await db.close();
|
||||
await t.throwsAsync(async () => {
|
||||
await db.prepare("SELECT 1");
|
||||
}, {
|
||||
instanceOf: TypeError,
|
||||
message: "The database connection is not open"
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("Database.exec() after close()", async (t) => {
|
||||
const db = t.context.db;
|
||||
await db.close();
|
||||
await t.throwsAsync(async () => {
|
||||
await db.exec("SELECT 1");
|
||||
}, {
|
||||
instanceOf: TypeError,
|
||||
message: "The database connection is not open"
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("Database.interrupt()", async (t) => {
|
||||
const db = t.context.db;
|
||||
const stmt = await db.prepare("WITH RECURSIVE infinite_loop(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM infinite_loop) SELECT * FROM infinite_loop;");
|
||||
const fut = stmt.all();
|
||||
db.interrupt();
|
||||
await t.throwsAsync(async () => {
|
||||
await fut;
|
||||
}, {
|
||||
instanceOf: t.context.errorType,
|
||||
message: 'interrupted',
|
||||
code: 'SQLITE_INTERRUPT'
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test.skip("Statement.interrupt()", async (t) => {
|
||||
const db = t.context.db;
|
||||
const stmt = await db.prepare("WITH RECURSIVE infinite_loop(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM infinite_loop) SELECT * FROM infinite_loop;");
|
||||
const fut = stmt.all();
|
||||
stmt.interrupt();
|
||||
await t.throwsAsync(async () => {
|
||||
await fut;
|
||||
}, {
|
||||
instanceOf: t.context.errorType,
|
||||
message: 'interrupted',
|
||||
code: 'SQLITE_INTERRUPT'
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("Timeout option", async (t) => {
|
||||
const timeout = 1000;
|
||||
const path = genDatabaseFilename();
|
||||
const [conn1] = await connect(path);
|
||||
await conn1.exec("CREATE TABLE t(x)");
|
||||
await conn1.exec("BEGIN IMMEDIATE");
|
||||
await conn1.exec("INSERT INTO t VALUES (1)")
|
||||
const options = { timeout };
|
||||
const [conn2] = await connect(path, options);
|
||||
const start = Date.now();
|
||||
try {
|
||||
await conn2.exec("INSERT INTO t VALUES (1)")
|
||||
} catch (e) {
|
||||
t.is(e.code, "SQLITE_BUSY");
|
||||
const end = Date.now();
|
||||
const elapsed = end - start;
|
||||
// Allow some tolerance for the timeout.
|
||||
t.is(elapsed > timeout/2, true);
|
||||
}
|
||||
fs.unlinkSync(path);
|
||||
});
|
||||
|
||||
test.serial("Concurrent writes over same connection", async (t) => {
|
||||
const db = t.context.db;
|
||||
await db.exec(`
|
||||
DROP TABLE IF EXISTS users;
|
||||
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
|
||||
`);
|
||||
const stmt = await db.prepare("INSERT INTO users(name, email) VALUES (:name, :email)");
|
||||
const promises = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
promises.push(stmt.run({ name: "Alice", email: "alice@example.org" }));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
const stmt2 = await db.prepare("SELECT * FROM users ORDER BY name");
|
||||
const rows = await stmt2.all();
|
||||
t.is(rows.length, 1000);
|
||||
});
|
||||
|
||||
const connect = async (path, options = {}) => {
|
||||
if (!path) {
|
||||
path = genDatabaseFilename();
|
||||
}
|
||||
const provider = process.env.PROVIDER;
|
||||
if (provider === "turso") {
|
||||
const x = await import("@tursodatabase/turso");
|
||||
const db = new x.default(path, options);
|
||||
return [db, x.SqliteError];
|
||||
}
|
||||
if (provider === "serverless") {
|
||||
const x = await import("@tursodatabase/serverless");
|
||||
const url = process.env.TURSO_DATABASE_URL;
|
||||
const authToken = process.env.TURSO_AUTH_TOKEN;
|
||||
const db = new x.connect({
|
||||
url: process.env.TURSO_DATABASE_URL,
|
||||
authToken: process.env.TURSO_AUTH_TOKEN,
|
||||
});
|
||||
return [db, x.SqliteError];
|
||||
}
|
||||
};
|
||||
|
||||
/// Generate a unique database filename
|
||||
const genDatabaseFilename = () => {
|
||||
return `test-${crypto.randomBytes(8).toString('hex')}.db`;
|
||||
};
|
||||
467
testing/javascript/__test__/sync.test.js
Normal file
467
testing/javascript/__test__/sync.test.js
Normal file
@@ -0,0 +1,467 @@
|
||||
import test from "ava";
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
|
||||
test.beforeEach(async (t) => {
|
||||
const [db, errorType, provider] = await connect();
|
||||
db.exec(`
|
||||
DROP TABLE IF EXISTS users;
|
||||
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
|
||||
`);
|
||||
db.exec(
|
||||
"INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"
|
||||
);
|
||||
db.exec(
|
||||
"INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')"
|
||||
);
|
||||
t.context = {
|
||||
db,
|
||||
errorType,
|
||||
provider
|
||||
};
|
||||
});
|
||||
|
||||
test.after.always(async (t) => {
|
||||
if (t.context.db != undefined) {
|
||||
t.context.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.serial("Open in-memory database", async (t) => {
|
||||
const [db] = await connect(":memory:");
|
||||
t.is(db.memory, true);
|
||||
});
|
||||
|
||||
test.skip("Statement.prepare() error", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
t.throws(() => {
|
||||
return db.prepare("SYNTAX ERROR");
|
||||
}, {
|
||||
instanceOf: t.context.errorType,
|
||||
message: 'near "SYNTAX": syntax error'
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("Statement.run() returning rows", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("SELECT 1");
|
||||
const info = stmt.run();
|
||||
t.is(info.changes, 0);
|
||||
});
|
||||
|
||||
test.serial("Statement.run() [positional]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("INSERT INTO users(name, email) VALUES (?, ?)");
|
||||
const info = stmt.run(["Carol", "carol@example.net"]);
|
||||
t.is(info.changes, 1);
|
||||
t.is(info.lastInsertRowid, 3);
|
||||
});
|
||||
|
||||
test.serial("Statement.run() [named]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("INSERT INTO users(name, email) VALUES (@name, @email);");
|
||||
const info = stmt.run({"name": "Carol", "email": "carol@example.net"});
|
||||
t.is(info.changes, 1);
|
||||
t.is(info.lastInsertRowid, 3);
|
||||
});
|
||||
|
||||
test.serial("Statement.get() [no parameters]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
var stmt = 0;
|
||||
|
||||
stmt = db.prepare("SELECT * FROM users");
|
||||
t.is(stmt.get().name, "Alice");
|
||||
t.deepEqual(stmt.raw().get(), [1, 'Alice', 'alice@example.org']);
|
||||
});
|
||||
|
||||
test.serial("Statement.get() [positional]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
var stmt = 0;
|
||||
|
||||
stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
||||
t.is(stmt.get(0), undefined);
|
||||
t.is(stmt.get([0]), undefined);
|
||||
t.is(stmt.get(1).name, "Alice");
|
||||
t.is(stmt.get(2).name, "Bob");
|
||||
|
||||
stmt = db.prepare("SELECT * FROM users WHERE id = ?1");
|
||||
t.is(stmt.get({1: 0}), undefined);
|
||||
t.is(stmt.get({1: 1}).name, "Alice");
|
||||
t.is(stmt.get({1: 2}).name, "Bob");
|
||||
});
|
||||
|
||||
test.serial("Statement.get() [named]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
var stmt = undefined;
|
||||
|
||||
stmt = db.prepare("SELECT :b, :a");
|
||||
t.deepEqual(stmt.raw().get({ a: 'a', b: 'b' }), ['b', 'a']);
|
||||
|
||||
stmt = db.prepare("SELECT * FROM users WHERE id = :id");
|
||||
t.is(stmt.get({ id: 0 }), undefined);
|
||||
t.is(stmt.get({ id: 1 }).name, "Alice");
|
||||
t.is(stmt.get({ id: 2 }).name, "Bob");
|
||||
|
||||
stmt = db.prepare("SELECT * FROM users WHERE id = @id");
|
||||
t.is(stmt.get({ id: 0 }), undefined);
|
||||
t.is(stmt.get({ id: 1 }).name, "Alice");
|
||||
t.is(stmt.get({ id: 2 }).name, "Bob");
|
||||
|
||||
stmt = db.prepare("SELECT * FROM users WHERE id = $id");
|
||||
t.is(stmt.get({ id: 0 }), undefined);
|
||||
t.is(stmt.get({ id: 1 }).name, "Alice");
|
||||
t.is(stmt.get({ id: 2 }).name, "Bob");
|
||||
});
|
||||
|
||||
test.serial("Statement.get() [raw]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
||||
t.deepEqual(stmt.raw().get(1), [1, "Alice", "alice@example.org"]);
|
||||
});
|
||||
|
||||
test.skip("Statement.iterate() [empty]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("SELECT * FROM users WHERE id = 0");
|
||||
t.is(stmt.iterate().next().done, true);
|
||||
t.is(stmt.iterate([]).next().done, true);
|
||||
t.is(stmt.iterate({}).next().done, true);
|
||||
});
|
||||
|
||||
test.skip("Statement.iterate()", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("SELECT * FROM users");
|
||||
const expected = [1, 2];
|
||||
var idx = 0;
|
||||
for (const row of stmt.iterate()) {
|
||||
t.is(row.id, expected[idx++]);
|
||||
}
|
||||
});
|
||||
|
||||
test.serial("Statement.all()", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("SELECT * FROM users");
|
||||
const expected = [
|
||||
{ id: 1, name: "Alice", email: "alice@example.org" },
|
||||
{ id: 2, name: "Bob", email: "bob@example.com" },
|
||||
];
|
||||
t.deepEqual(stmt.all(), expected);
|
||||
});
|
||||
|
||||
test.serial("Statement.all() [raw]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("SELECT * FROM users");
|
||||
const expected = [
|
||||
[1, "Alice", "alice@example.org"],
|
||||
[2, "Bob", "bob@example.com"],
|
||||
];
|
||||
t.deepEqual(stmt.raw().all(), expected);
|
||||
});
|
||||
|
||||
test.serial("Statement.all() [pluck]", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("SELECT * FROM users");
|
||||
const expected = [
|
||||
1,
|
||||
2,
|
||||
];
|
||||
t.deepEqual(stmt.pluck().all(), expected);
|
||||
});
|
||||
|
||||
test.skip("Statement.all() [default safe integers]", async (t) => {
|
||||
const db = t.context.db;
|
||||
db.defaultSafeIntegers();
|
||||
const stmt = db.prepare("SELECT * FROM users");
|
||||
const expected = [
|
||||
[1n, "Alice", "alice@example.org"],
|
||||
[2n, "Bob", "bob@example.com"],
|
||||
];
|
||||
t.deepEqual(stmt.raw().all(), expected);
|
||||
});
|
||||
|
||||
test.skip("Statement.all() [statement safe integers]", async (t) => {
|
||||
const db = t.context.db;
|
||||
const stmt = db.prepare("SELECT * FROM users");
|
||||
stmt.safeIntegers();
|
||||
const expected = [
|
||||
[1n, "Alice", "alice@example.org"],
|
||||
[2n, "Bob", "bob@example.com"],
|
||||
];
|
||||
t.deepEqual(stmt.raw().all(), expected);
|
||||
});
|
||||
|
||||
test.skip("Statement.raw() [failure]", async (t) => {
|
||||
const db = t.context.db;
|
||||
const stmt = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)");
|
||||
t.throws(() => {
|
||||
stmt.raw()
|
||||
}, {
|
||||
message: 'The raw() method is only for statements that return data'
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("Statement.run() with array bind parameter", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
db.exec(`
|
||||
DROP TABLE IF EXISTS t;
|
||||
CREATE TABLE t (value BLOB);
|
||||
`);
|
||||
|
||||
const array = [1, 2, 3];
|
||||
|
||||
const insertStmt = db.prepare("INSERT INTO t (value) VALUES (?)");
|
||||
t.throws(() => {
|
||||
insertStmt.run([array]);
|
||||
}, {
|
||||
message: 'SQLite3 can only bind numbers, strings, bigints, buffers, and null'
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("Statement.run() with Float32Array bind parameter", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
db.exec(`
|
||||
DROP TABLE IF EXISTS t;
|
||||
CREATE TABLE t (value BLOB);
|
||||
`);
|
||||
|
||||
const array = new Float32Array([1, 2, 3]);
|
||||
|
||||
const insertStmt = db.prepare("INSERT INTO t (value) VALUES (?)");
|
||||
insertStmt.run([array]);
|
||||
|
||||
const selectStmt = db.prepare("SELECT value FROM t");
|
||||
t.deepEqual(selectStmt.raw().get()[0], Buffer.from(array.buffer));
|
||||
});
|
||||
|
||||
test.skip("Statement.run() for vector feature with Float32Array bind parameter", async (t) => {
|
||||
if (t.context.provider === 'better-sqlite3') {
|
||||
// skip this test for better-sqlite3
|
||||
t.assert(true);
|
||||
return;
|
||||
}
|
||||
const db = t.context.db;
|
||||
|
||||
db.exec(`
|
||||
DROP TABLE IF EXISTS t;
|
||||
CREATE TABLE t (embedding FLOAT32(8));
|
||||
CREATE INDEX t_idx ON t ( libsql_vector_idx(embedding) );
|
||||
`);
|
||||
|
||||
const insertStmt = db.prepare("INSERT INTO t VALUES (?)");
|
||||
insertStmt.run([new Float32Array([1,1,1,1,1,1,1,1])]);
|
||||
insertStmt.run([new Float32Array([-1,-1,-1,-1,-1,-1,-1,-1])]);
|
||||
|
||||
const selectStmt = db.prepare("SELECT embedding FROM vector_top_k('t_idx', vector('[2,2,2,2,2,2,2,2]'), 1) n JOIN t ON n.rowid = t.rowid");
|
||||
t.deepEqual(selectStmt.raw().get()[0], Buffer.from(new Float32Array([1,1,1,1,1,1,1,1]).buffer));
|
||||
|
||||
// we need to explicitly delete this table because later when sqlite-based (not LibSQL) tests will delete table 't' they will leave 't_idx_shadow' table untouched
|
||||
db.exec(`DROP TABLE t`);
|
||||
});
|
||||
|
||||
test.skip("Statement.columns()", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
var stmt = undefined;
|
||||
|
||||
stmt = db.prepare("SELECT 1");
|
||||
t.deepEqual(stmt.columns(), [
|
||||
{
|
||||
column: null,
|
||||
database: null,
|
||||
name: '1',
|
||||
table: null,
|
||||
type: null,
|
||||
},
|
||||
]);
|
||||
|
||||
stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
||||
t.deepEqual(stmt.columns(), [
|
||||
{
|
||||
column: "id",
|
||||
database: "main",
|
||||
name: "id",
|
||||
table: "users",
|
||||
type: "INTEGER",
|
||||
},
|
||||
{
|
||||
column: "name",
|
||||
database: "main",
|
||||
name: "name",
|
||||
table: "users",
|
||||
type: "TEXT",
|
||||
},
|
||||
{
|
||||
column: "email",
|
||||
database: "main",
|
||||
name: "email",
|
||||
table: "users",
|
||||
type: "TEXT",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test.skip("Database.transaction()", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const insert = db.prepare(
|
||||
"INSERT INTO users(name, email) VALUES (:name, :email)"
|
||||
);
|
||||
|
||||
const insertMany = db.transaction((users) => {
|
||||
t.is(db.inTransaction, true);
|
||||
for (const user of users) insert.run(user);
|
||||
});
|
||||
|
||||
t.is(db.inTransaction, false);
|
||||
insertMany([
|
||||
{ name: "Joey", email: "joey@example.org" },
|
||||
{ name: "Sally", email: "sally@example.org" },
|
||||
{ name: "Junior", email: "junior@example.org" },
|
||||
]);
|
||||
t.is(db.inTransaction, false);
|
||||
|
||||
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
||||
t.is(stmt.get(3).name, "Joey");
|
||||
t.is(stmt.get(4).name, "Sally");
|
||||
t.is(stmt.get(5).name, "Junior");
|
||||
});
|
||||
|
||||
test.skip("Database.transaction().immediate()", async (t) => {
|
||||
const db = t.context.db;
|
||||
const insert = db.prepare(
|
||||
"INSERT INTO users(name, email) VALUES (:name, :email)"
|
||||
);
|
||||
const insertMany = db.transaction((users) => {
|
||||
t.is(db.inTransaction, true);
|
||||
for (const user of users) insert.run(user);
|
||||
});
|
||||
t.is(db.inTransaction, false);
|
||||
insertMany.immediate([
|
||||
{ name: "Joey", email: "joey@example.org" },
|
||||
{ name: "Sally", email: "sally@example.org" },
|
||||
{ name: "Junior", email: "junior@example.org" },
|
||||
]);
|
||||
t.is(db.inTransaction, false);
|
||||
});
|
||||
|
||||
test.skip("values", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const stmt = db.prepare("SELECT ?").raw();
|
||||
t.deepEqual(stmt.get(1), [1]);
|
||||
t.deepEqual(stmt.get(Number.MIN_VALUE), [Number.MIN_VALUE]);
|
||||
t.deepEqual(stmt.get(Number.MAX_VALUE), [Number.MAX_VALUE]);
|
||||
t.deepEqual(stmt.get(Number.MAX_SAFE_INTEGER), [Number.MAX_SAFE_INTEGER]);
|
||||
t.deepEqual(stmt.get(9007199254740991n), [9007199254740991]);
|
||||
});
|
||||
|
||||
test.serial("Database.pragma()", async (t) => {
|
||||
const db = t.context.db;
|
||||
db.pragma("cache_size = 2000");
|
||||
t.deepEqual(db.pragma("cache_size"), [{ "cache_size": 2000 }]);
|
||||
});
|
||||
|
||||
test.skip("errors", async (t) => {
|
||||
const db = t.context.db;
|
||||
|
||||
const syntaxError = t.throws(() => {
|
||||
db.exec("SYNTAX ERROR");
|
||||
}, {
|
||||
instanceOf: t.context.errorType,
|
||||
message: 'near "SYNTAX": syntax error',
|
||||
code: 'SQLITE_ERROR'
|
||||
});
|
||||
const noTableError = t.throws(() => {
|
||||
db.exec("SELECT * FROM missing_table");
|
||||
}, {
|
||||
instanceOf: t.context.errorType,
|
||||
message: "no such table: missing_table",
|
||||
code: 'SQLITE_ERROR'
|
||||
});
|
||||
|
||||
if (t.context.provider === 'libsql') {
|
||||
t.is(noTableError.rawCode, 1)
|
||||
t.is(syntaxError.rawCode, 1)
|
||||
}
|
||||
});
|
||||
|
||||
test.skip("Database.prepare() after close()", async (t) => {
|
||||
const db = t.context.db;
|
||||
db.close();
|
||||
t.throws(() => {
|
||||
db.prepare("SELECT 1");
|
||||
}, {
|
||||
instanceOf: TypeError,
|
||||
message: "The database connection is not open"
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("Database.exec() after close()", async (t) => {
|
||||
const db = t.context.db;
|
||||
db.close();
|
||||
t.throws(() => {
|
||||
db.exec("SELECT 1");
|
||||
}, {
|
||||
instanceOf: TypeError,
|
||||
message: "The database connection is not open"
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("Timeout option", async (t) => {
|
||||
const timeout = 1000;
|
||||
const path = genDatabaseFilename();
|
||||
const [conn1] = await connect(path);
|
||||
conn1.exec("CREATE TABLE t(x)");
|
||||
conn1.exec("BEGIN IMMEDIATE");
|
||||
conn1.exec("INSERT INTO t VALUES (1)")
|
||||
const options = { timeout };
|
||||
const [conn2] = await connect(path, options);
|
||||
const start = Date.now();
|
||||
try {
|
||||
conn2.exec("INSERT INTO t VALUES (1)")
|
||||
} catch (e) {
|
||||
t.is(e.code, "SQLITE_BUSY");
|
||||
const end = Date.now();
|
||||
const elapsed = end - start;
|
||||
// Allow some tolerance for the timeout.
|
||||
t.is(elapsed > timeout/2, true);
|
||||
}
|
||||
fs.unlinkSync(path);
|
||||
});
|
||||
|
||||
const connect = async (path, options = {}) => {
|
||||
if (!path) {
|
||||
path = genDatabaseFilename();
|
||||
}
|
||||
const provider = process.env.PROVIDER;
|
||||
if (provider === "turso") {
|
||||
const x = await import("@tursodatabase/turso/sync");
|
||||
const db = new x.default(path, options);
|
||||
return [db, x.SqliteError, provider];
|
||||
}
|
||||
if (provider == "better-sqlite3") {
|
||||
const x = await import("better-sqlite3");
|
||||
const db = x.default(path, options);
|
||||
return [db, x.default.SqliteError, provider];
|
||||
}
|
||||
throw new Error("Unknown provider: " + provider);
|
||||
};
|
||||
|
||||
/// Generate a unique database filename
|
||||
const genDatabaseFilename = () => {
|
||||
return `test-${crypto.randomBytes(8).toString('hex')}.db`;
|
||||
};
|
||||
3
testing/javascript/artifacts/basic-test.sql
Normal file
3
testing/javascript/artifacts/basic-test.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
CREATE TABLE users (name TEXT, age INTEGER);
|
||||
INSERT INTO users (name, age) VALUES ('Bob', 24);
|
||||
INSERT INTO users (name, age) VALUES ('Alice', 42);
|
||||
2329
testing/javascript/package-lock.json
generated
Normal file
2329
testing/javascript/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
testing/javascript/package.json
Normal file
19
testing/javascript/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "turso-integration-tests",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "npm run test:turso && npm run test:serverless && npm run test:better-sqlite3",
|
||||
"test:turso": "PROVIDER=turso ava __test__/*.test.js",
|
||||
"test:serverless": "PROVIDER=serverless ava __test__/async.test.js",
|
||||
"test:better-sqlite3": "PROVIDER=better-sqlite3 ava __test__/sync.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^5.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tursodatabase/turso": "../../bindings/javascript",
|
||||
"@tursodatabase/serverless": "../../packages/turso-serverless",
|
||||
"better-sqlite3": "^11.9.1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user