mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
|
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));
|
||
|
}
|