gristlabs_grist-core/app/server/lib/SqliteNode.ts
Paul Fitzpatrick bfd0fa8c7f
add an endpoint for doing SQL selects (#641)
* add an endpoint for doing SQL selects

This adds an endpoint for doing SQL selects directly on a Grist document. Other kinds of statements are not supported. There is a default timeout of a second on queries.

This follows loosely an API design by Alex Hall.

Co-authored-by: jarek <jaroslaw.sadzinski@gmail.com>
2023-09-04 09:21:18 -04:00

116 lines
4.0 KiB
TypeScript

import * as sqlite3 from '@gristlabs/sqlite3';
import { fromCallback } from 'app/server/lib/serverUtils';
import { MinDB, MinDBOptions, PreparedStatement, ResultRow, SqliteVariant } from 'app/server/lib/SqliteCommon';
import { OpenMode, RunResult } from 'app/server/lib/SQLiteDB';
export class NodeSqliteVariant implements SqliteVariant {
public opener(dbPath: string, mode: OpenMode): Promise<MinDB> {
return NodeSqlite3DatabaseAdapter.opener(dbPath, mode);
}
}
export class NodeSqlite3PreparedStatement implements PreparedStatement {
public constructor(private _statement: sqlite3.Statement) {
}
public async run(...params: any[]): Promise<RunResult> {
return fromCallback(cb => this._statement.run(...params, cb));
}
public async finalize() {
await fromCallback(cb => this._statement.finalize(cb));
}
public columns(): string[] {
// This method is only needed if marshalling is not built in -
// and node-sqlite3 has marshalling built in.
throw new Error('not available (but should not be needed)');
}
}
export class NodeSqlite3DatabaseAdapter implements MinDB {
public static async opener(dbPath: string, mode: OpenMode): Promise<any> {
const sqliteMode: number =
// tslint:disable-next-line:no-bitwise
(mode === OpenMode.OPEN_READONLY ? sqlite3.OPEN_READONLY : sqlite3.OPEN_READWRITE) |
(mode === OpenMode.OPEN_CREATE || mode === OpenMode.CREATE_EXCL ? sqlite3.OPEN_CREATE : 0);
let _db: sqlite3.Database;
await fromCallback(cb => { _db = new sqlite3.Database(dbPath, sqliteMode, cb); });
const result = new NodeSqlite3DatabaseAdapter(_db!);
await result.limitAttach(0); // Outside of VACUUM, we don't allow ATTACH.
return result;
}
public constructor(protected _db: sqlite3.Database) {
// Default database to serialized execution. See https://github.com/mapbox/node-sqlite3/wiki/Control-Flow
// This isn't enough for transactions, which we serialize explicitly.
this._db.serialize();
}
public async exec(sql: string): Promise<void> {
return fromCallback(cb => this._db.exec(sql, cb));
}
public async run(sql: string, ...params: any[]): Promise<RunResult> {
return new Promise((resolve, reject) => {
function callback(this: RunResult, err: Error | null) {
if (err) {
reject(err);
} else {
resolve(this);
}
}
this._db.run(sql, ...params, callback);
});
}
public async get(sql: string, ...params: any[]): Promise<ResultRow|undefined> {
return fromCallback(cb => this._db.get(sql, ...params, cb));
}
public async all(sql: string, ...params: any[]): Promise<ResultRow[]> {
return fromCallback(cb => this._db.all(sql, params, cb));
}
public async prepare(sql: string): Promise<PreparedStatement> {
let stmt: sqlite3.Statement|undefined;
// The original interface is a little strange; we resolve to Statement if prepare() succeeded.
await fromCallback(cb => { stmt = this._db.prepare(sql, cb); }).then(() => stmt);
if (!stmt) { throw new Error('could not prepare statement'); }
return new NodeSqlite3PreparedStatement(stmt);
}
public async close() {
this._db.close();
}
public async interrupt(): Promise<void> {
this._db.interrupt();
}
public getOptions(): MinDBOptions {
return {
canInterrupt: true,
bindableMethodsProcessOneStatement: true,
};
}
public async allMarshal(sql: string, ...params: any[]): Promise<Buffer> {
// allMarshal isn't in the typings, because it is our addition to our fork of sqlite3 JS lib.
return fromCallback(cb => (this._db as any).allMarshal(sql, ...params, cb));
}
public async runAndGetId(sql: string, ...params: any[]): Promise<number> {
const result = await this.run(sql, ...params);
return (result as any).lastID;
}
public async limitAttach(maxAttach: number) {
const SQLITE_LIMIT_ATTACHED = (sqlite3 as any).LIMIT_ATTACHED;
// Cast because types out of date.
(this._db as any).configure('limit', SQLITE_LIMIT_ATTACHED, maxAttach);
}
}