swc/crates/swc_bundler/tests/.cache/deno/7c23c2a3ed280e486b392c825480f3ca80048e58.ts
2021-11-09 20:42:49 +09:00

295 lines
8.7 KiB
TypeScript

// Loaded from https://deno.land/x/sqlite@v2.3.1/src/db.ts
import instantiate from "../build/sqlite.js";
import { getStr, setArr, setStr } from "./wasm.ts";
import { Status, Values } from "./constants.ts";
import SqliteError from "./error.ts";
import { Empty, Rows } from "./rows.ts";
// Possible parameters to be bound to a query
type QueryParam =
| boolean
| number
| bigint
| string
| null
| undefined
| Date
| Uint8Array;
export class DB {
private _wasm: any;
private _open: boolean;
private _transactions: Set<Rows>;
/**
* DB
*
* Create a new database. The passed
* path will be opened with read/ write
* permissions and created if it does not
* already exist.
*
* The default opens an in-memory database.
*/
constructor(path: string = ":memory:") {
this._wasm = instantiate().exports;
this._open = false;
this._transactions = new Set();
// Try to open the database
let status;
setStr(this._wasm, path, (ptr) => {
status = this._wasm.open(ptr);
});
if (status !== Status.SqliteOk) {
throw this._error();
}
this._open = true;
}
/**
* DB.query
*
* Run a query against the database. The query
* can contain placeholder parameters, which
* are bound to the values passed in 'values'.
*
* db.query("SELECT name, email FROM users WHERE subscribed = ? AND list LIKE ?", [true, listName]);
*
* This supports positional and named parameters.
* Positional parameters can be set by passing an
* array for values. Named parameters can be set
* by passing an object for values.
*
* While they can be mixed in principle, this is
* not recommended.
*
* | Parameter | Values |
* |---------------|-------------------------|
* | `?NNN` or `?` | NNN-th value in array |
* | `:AAAA` | value `AAAA` or `:AAAA` |
* | `@AAAA` | value `@AAAA` |
* | `$AAAA` | value `$AAAA` |
*
* (see https://www.sqlite.org/lang_expr.html)
*
* Values may only be of the following
* types and are converted as follows:
*
* | JS in | SQL type | JS out |
* |------------|-----------------|------------------|
* | number | INTEGER or REAL | number or bigint |
* | bigint | INTEGER | number or bigint |
* | boolean | INTEGER | number |
* | string | TEXT | string |
* | Date | TEXT | string |
* | Uint8Array | BLOB | Uint8Array |
* | null | NULL | null |
* | undefined | NULL | null |
*
* If no value is provided to a given parameter,
* SQLite will default to NULL.
*
* If a `bigint` is bound, it is converted to a
* signed 64 big integer, which may not be lossless.
* If an integer value is read from the database, which
* is too big to safely be contained in a `number`, it
* is automatically returned as a `bigint`.
*
* If a `Date` is bound, it will be converted to
* an ISO 8601 string: `YYYY-MM-DDTHH:MM:SS.SSSZ`.
* This format is understood by built-in SQLite
* date-time functions. Also see
* https://sqlite.org/lang_datefunc.html.
*
* This always returns an iterable Rows object.
* As a special case, if the query has no rows
* to return this returns the Empty row (which
* is also iterable, but has zero entries).
*
* !> Any returned Rows object needs to be fully
* iterated over or discarded by calling
* `.return()` or closing the iterator.
*/
query(sql: string, values?: object | QueryParam[]): Rows {
if (!this._open) {
throw new SqliteError("Database was closed.");
}
// Prepare sqlite query statement
let stmt: number = Values.Null;
setStr(this._wasm, sql, (ptr) => {
stmt = this._wasm.prepare(ptr);
});
if (stmt === Values.Null) {
throw this._error();
}
// Prepare parameter array
let parameters: any[] = [];
if (Array.isArray(values)) {
parameters = values;
} else if (typeof values === "object") {
// Resolve parameter index for named values
for (const key of Object.keys(values)) {
let idx = Values.Error;
// Prepend ':' to name, if it does not have a special starting character
let name = key;
if (name[0] !== ":" && name[0] !== "@" && name[0] !== "$") {
name = `:${name}`;
}
setStr(this._wasm, name, (ptr) => {
idx = this._wasm.bind_parameter_index(stmt, ptr);
});
if (idx === Values.Error) {
this._wasm.finalize(stmt);
throw new SqliteError(`No parameter named '${name}'.`);
}
parameters[idx - 1] = (values as any)[key];
}
}
// Bind parameters
for (let i = 0; i < parameters.length; i++) {
let value = parameters[i];
let status;
switch (typeof value) {
case "boolean":
value = value ? 1 : 0;
// fall through
case "number":
if (Number.isSafeInteger(value)) {
status = this._wasm.bind_int(stmt, i + 1, value);
} else {
status = this._wasm.bind_double(stmt, i + 1, value);
}
break;
case "bigint":
// bigint is bound as a string and converted to i64 on C side
setStr(this._wasm, value.toString(), (ptr) => {
status = this._wasm.bind_big_int(stmt, i + 1, ptr);
});
break;
case "string":
setStr(this._wasm, value, (ptr) => {
status = this._wasm.bind_text(stmt, i + 1, ptr);
});
break;
default:
if (value instanceof Date) {
// Dates are allowed and bound to TEXT, formatted `YYYY-MM-DDTHH:MM:SS.SSSZ`
setStr(this._wasm, value.toISOString(), (ptr) => {
status = this._wasm.bind_text(stmt, i + 1, ptr);
});
} else if (value instanceof Uint8Array) {
// Uint8Arrays are allowed and bound to BLOB
setArr(this._wasm, value, (ptr) => {
status = this._wasm.bind_blob(stmt, i + 1, ptr, value.length);
});
} else if (value === null || value === undefined) {
// Both null and undefined result in a NULL entry
status = this._wasm.bind_null(stmt, i + 1);
} else {
this._wasm.finalize(stmt);
throw new SqliteError(`Can not bind ${typeof value}.`);
}
break;
}
if (status !== Status.SqliteOk) {
this._wasm.finalize(stmt);
throw this._error(status);
}
}
// Step once to handle case where result is empty
const status = this._wasm.step(stmt);
switch (status) {
case Status.SqliteDone:
this._wasm.finalize(stmt);
return Empty;
break;
case Status.SqliteRow:
const transaction = new Rows(this, stmt);
this._transactions.add(transaction);
return transaction;
break;
default:
this._wasm.finalize(stmt);
throw this._error(status);
break;
}
}
/**
* DB.close
*
* Close database handle. This must be called if
* DB is no longer used, to avoid leaking file
* resources.
*
* If force is specified, any on-going transactions
* will be closed.
*/
close(force: boolean = false) {
if (!this._open) {
return;
}
if (force) {
for (const transaction of this._transactions) {
transaction.return();
}
}
if (this._wasm.close() !== Status.SqliteOk) {
throw this._error();
}
this._open = false;
}
/**
* DB.lastInsertRowId
*
* Get last inserted row id. This corresponds to
* the SQLite function `sqlite3_last_insert_rowid`.
*
* By default, it will return 0 if there is no row
* inserted yet.
*/
get lastInsertRowId(): number {
return this._wasm.last_insert_rowid();
}
/**
* DB.changes
*
* Return the number of rows modified, inserted or
* deleted by the most recently completed query.
* This corresponds to the SQLite function
* `sqlite3_changes`.
*/
get changes(): number {
return this._wasm.changes();
}
/**
* DB.totalChanges
*
* Return the number of rows modified, inserted or
* deleted since the database was opened.
* This corresponds to the SQLite function
* `sqlite3_total_changes`.
*/
get totalChanges(): number {
return this._wasm.total_changes();
}
private _error(code?: number): SqliteError {
if (code === undefined) {
code = this._wasm.get_status() as number;
}
const msg = getStr(this._wasm, this._wasm.get_sqlite_error_str());
return new SqliteError(msg, code);
}
}