You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/app/server/lib/SqliteCommon.ts

127 lines
3.9 KiB

import { Marshaller } from 'app/common/marshal';
import { OpenMode, quoteIdent } from 'app/server/lib/SQLiteDB';
/**
* Code common to SQLite wrappers.
*/
/**
* It is important that Statement exists - but we don't expect
* anything of it.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Statement {}
export interface MinDB {
exec(sql: string): Promise<void>;
run(sql: string, ...params: any[]): Promise<MinRunResult>;
get(sql: string, ...params: any[]): Promise<ResultRow|undefined>;
all(sql: string, ...params: any[]): Promise<ResultRow[]>;
prepare(sql: string, ...params: any[]): Promise<PreparedStatement>;
runAndGetId(sql: string, ...params: any[]): Promise<number>;
close(): Promise<void>;
allMarshal(sql: string, ...params: any[]): Promise<Buffer>;
/**
* Limit the number of ATTACHed databases permitted.
*/
limitAttach(maxAttach: number): Promise<void>;
}
export interface MinRunResult {
changes: number;
}
// Describes the result of get() and all() database methods.
export interface ResultRow {
[column: string]: any;
}
export interface PreparedStatement {
run(...params: any[]): Promise<MinRunResult>;
finalize(): Promise<void>;
columns(): string[];
}
export interface SqliteVariant {
opener(dbPath: string, mode: OpenMode): Promise<MinDB>;
}
/**
* A crude implementation of Grist marshalling.
* There is a fork of node-sqlite3 that has Grist
* marshalling built in, at:
* https://github.com/gristlabs/node-sqlite3
* If using a version of SQLite without this built
* in, another option is to add custom functions
* to do it. This object has the initialize, step,
* and finalize callbacks typically needed to add
* a custom aggregration function.
*/
export const gristMarshal = {
initialize(): GristMarshalIntermediateValue {
return {};
},
step(accum: GristMarshalIntermediateValue, ...row: any[]) {
if (!accum.names || !accum.values) {
accum.names = row.map(value => String(value));
accum.values = row.map(() => []);
} else {
for (const [i, v] of row.entries()) {
accum.values[i].push(v);
}
}
return accum;
},
finalize(accum: GristMarshalIntermediateValue) {
const marshaller = new Marshaller({version: 2, keysAreBuffers: true});
const result: Record<string, Array<any>> = {};
if (accum.names && accum.values) {
for (const [i, name] of accum.names.entries()) {
result[name] = accum.values[i];
}
}
marshaller.marshal(result);
return marshaller.dumpAsBuffer();
}
};
/**
* An intermediate value used during an aggregation.
*/
interface GristMarshalIntermediateValue {
// The names of the columns, once known.
names?: string[];
// Values stored in the columns.
// There is one element in the outermost array per column.
// That element contains a list of values stored in that column.
values?: Array<Array<any>>;
}
/**
* Run Grist marshalling as a SQLite query, assuming
* a custom aggregation has been added as "grist_marshal".
* The marshalled result needs to contain the column
* identifiers embedded in it. This is a little awkward
* to organize - hence the hacky UNION here. This is
* for compatibility with the existing marshalling method,
* which could be replaced instead.
*/
export async function allMarshalQuery(db: MinDB, sql: string, ...params: any[]): Promise<Buffer> {
const statement = await db.prepare(sql);
const columns = statement.columns();
const quotedColumnList = columns.map(quoteIdent).join(',');
const query = await db.all(`select grist_marshal(${quotedColumnList}) as buf FROM ` +
`(select ${quotedColumnList} UNION ALL select * from (` + sql + '))', ..._fixParameters(params));
return query[0].buf;
}
/**
* Booleans need to be cast to 1 or 0 for SQLite.
* The node-sqlite3 wrapper does this automatically, but other
* wrappers do not.
*/
function _fixParameters(params: any[]) {
return params.map(p => p === true ? 1 : (p === false ? 0 : p));
}