|
|
@@ -5,9 +5,38 @@ import assert from 'assert'; |
|
|
|
|
|
|
|
type ID = number; |
|
|
|
|
|
|
|
interface DuckDbDataSourceBase< |
|
|
|
ID, |
|
|
|
Schema extends v.BaseSchema = v.BaseSchema, |
|
|
|
CurrentName extends string = string, |
|
|
|
CurrentRouteName extends string = string, |
|
|
|
Data extends object = v.Output<Schema>, |
|
|
|
> extends DataSource<Data, ID> { |
|
|
|
resource?: Resource<BaseResourceType & { |
|
|
|
schema: Schema, |
|
|
|
name: CurrentName, |
|
|
|
routeName: CurrentRouteName, |
|
|
|
}>; |
|
|
|
|
|
|
|
db?: Database; |
|
|
|
} |
|
|
|
|
|
|
|
export const AutoincrementIdConfig = { |
|
|
|
// TODO |
|
|
|
generationStrategy: async () => {}, |
|
|
|
// TODO add options: https://duckdb.org/docs/sql/statements/create_sequence |
|
|
|
generationStrategy: async (dataSourceRaw: DataSource) => { |
|
|
|
const dataSource = dataSourceRaw as DuckDbDataSourceBase<ID>; |
|
|
|
assert(typeof dataSource.db !== 'undefined'); |
|
|
|
assert(typeof dataSource.resource !== 'undefined'); |
|
|
|
const idAttr = dataSource.resource.state.shared.get('idAttr'); |
|
|
|
assert(typeof idAttr === 'string'); |
|
|
|
|
|
|
|
const con = await dataSource.db.connect(); |
|
|
|
const stmt = await con.prepare(` |
|
|
|
SELECT nextval('${dataSource.resource.state.routeName}_sequence') as ${idAttr}; |
|
|
|
`); |
|
|
|
const [v] = await stmt.all(); |
|
|
|
return v[idAttr]; |
|
|
|
}, |
|
|
|
schema: v.number(), |
|
|
|
serialize: (v: unknown) => v?.toString() ?? '', |
|
|
|
deserialize: (v: string) => Number(v), |
|
|
@@ -18,8 +47,8 @@ export class DuckDbDataSource< |
|
|
|
CurrentName extends string = string, |
|
|
|
CurrentRouteName extends string = string, |
|
|
|
Data extends object = v.Output<Schema>, |
|
|
|
> implements DataSource<Data, ID> { |
|
|
|
private resource?: Resource<BaseResourceType & { |
|
|
|
> implements DuckDbDataSourceBase<ID, Schema, CurrentName, CurrentRouteName, Data> { |
|
|
|
resource?: Resource<BaseResourceType & { |
|
|
|
schema: Schema, |
|
|
|
name: CurrentName, |
|
|
|
routeName: CurrentRouteName, |
|
|
@@ -27,11 +56,12 @@ export class DuckDbDataSource< |
|
|
|
|
|
|
|
db?: Database; |
|
|
|
|
|
|
|
constructor() { |
|
|
|
constructor(private readonly path: string) { |
|
|
|
// noop |
|
|
|
} |
|
|
|
|
|
|
|
async initialize() { |
|
|
|
assert(typeof this.path !== 'undefined'); |
|
|
|
assert(typeof this.resource !== 'undefined'); |
|
|
|
|
|
|
|
const idConfig = this.resource.state.shared.get('idConfig') as ResourceIdConfig<any> | undefined; |
|
|
@@ -42,7 +72,7 @@ export class DuckDbDataSource< |
|
|
|
|
|
|
|
const idSchema = idConfig.schema as v.BaseSchema; |
|
|
|
|
|
|
|
this.db = await Database.create('test.db'); |
|
|
|
this.db = await Database.create(this.path); |
|
|
|
const clause = `CREATE TABLE IF NOT EXISTS ${this.resource.state.routeName}`; |
|
|
|
const resourceSchema = this.resource.schema as unknown as v.ObjectSchema<any>; |
|
|
|
const tableSchema = Object.entries(resourceSchema.entries) |
|
|
@@ -51,7 +81,16 @@ export class DuckDbDataSource< |
|
|
|
return [columnName, columnDef.type].join(' '); |
|
|
|
}) |
|
|
|
.join(','); |
|
|
|
const sql = `${clause} (${idAttr} ${idSchema.type},${tableSchema});`; |
|
|
|
let sequenceSql = ''; |
|
|
|
let defaultValue = ''; |
|
|
|
let idType = 'STRING'; |
|
|
|
if (idSchema.type === 'number') { |
|
|
|
// TODO support more sequence statements: https://duckdb.org/docs/sql/statements/create_sequence |
|
|
|
sequenceSql = `CREATE SEQUENCE IF NOT EXISTS ${this.resource.state.routeName}_sequence START 1;`; |
|
|
|
defaultValue = `DEFAULT nextval('${this.resource.state.routeName}_sequence')`; |
|
|
|
idType = 'INTEGER'; |
|
|
|
} |
|
|
|
const sql = `${sequenceSql}${clause} (${idAttr} ${idType} ${defaultValue},${tableSchema});`; |
|
|
|
const con = await this.db.connect(); |
|
|
|
const stmt = await con.prepare(sql); |
|
|
|
await stmt.run(); |
|
|
@@ -77,7 +116,8 @@ export class DuckDbDataSource< |
|
|
|
this.resource = resource as any; |
|
|
|
} |
|
|
|
|
|
|
|
async getMultiple() { |
|
|
|
async getMultiple(query) { |
|
|
|
// TODO translate query to SQL statements |
|
|
|
assert(typeof this.db !== 'undefined'); |
|
|
|
assert(typeof this.resource !== 'undefined'); |
|
|
|
|
|
|
@@ -90,7 +130,8 @@ export class DuckDbDataSource< |
|
|
|
} |
|
|
|
|
|
|
|
async newId() { |
|
|
|
const idConfig = this.resource?.state.shared.get('idConfig') as ResourceIdConfig<any>; |
|
|
|
assert(typeof this.resource !== 'undefined'); |
|
|
|
const idConfig = this.resource.state.shared.get('idConfig') as ResourceIdConfig<any>; |
|
|
|
assert(typeof idConfig !== 'undefined'); |
|
|
|
|
|
|
|
const theNewId = await idConfig.generationStrategy(this); |
|
|
@@ -107,7 +148,18 @@ export class DuckDbDataSource< |
|
|
|
const idAttr = this.resource.state.shared.get('idAttr'); |
|
|
|
assert(typeof idAttr === 'string'); |
|
|
|
|
|
|
|
const { [idAttr]: _, ...effectiveData } = data as Record<string, unknown>; |
|
|
|
let theId: any; |
|
|
|
const { [idAttr]: dataId } = data as Record<string, unknown>; |
|
|
|
if (typeof dataId !== 'undefined') { |
|
|
|
theId = idConfig.deserialize((data as Record<string, string>)[idAttr]); |
|
|
|
} else { |
|
|
|
const newId = await this.newId(); |
|
|
|
theId = idConfig.deserialize(newId.toString()); |
|
|
|
} |
|
|
|
const effectiveData = { |
|
|
|
...data, |
|
|
|
} as Record<string, unknown>; |
|
|
|
effectiveData[idAttr] = theId; |
|
|
|
|
|
|
|
const clause = `INSERT INTO ${this.resource.state.routeName}`; |
|
|
|
const keys = Object.keys(effectiveData).join(','); |
|
|
@@ -115,14 +167,14 @@ export class DuckDbDataSource< |
|
|
|
const sql = `${clause} (${keys}) VALUES (${values});`; |
|
|
|
const con = await this.db.connect(); |
|
|
|
const stmt = await con.prepare(sql); |
|
|
|
await stmt.all(); |
|
|
|
await stmt.run(); |
|
|
|
const newData = { |
|
|
|
...effectiveData |
|
|
|
}; |
|
|
|
return newData as Data; |
|
|
|
} |
|
|
|
|
|
|
|
async getTotalCount() { |
|
|
|
async getTotalCount(query) { |
|
|
|
assert(typeof this.db !== 'undefined'); |
|
|
|
assert(typeof this.resource !== 'undefined'); |
|
|
|
|
|
|
@@ -131,10 +183,10 @@ export class DuckDbDataSource< |
|
|
|
|
|
|
|
const con = await this.db.connect(); |
|
|
|
const stmt = await con.prepare(` |
|
|
|
SELECT COUNT(*) FROM ?; |
|
|
|
`, this.resource.state.routeName); |
|
|
|
SELECT COUNT(*) as c FROM ${this.resource.state.routeName}; |
|
|
|
`); |
|
|
|
const [data] = await stmt.all(); |
|
|
|
return data as unknown as number; |
|
|
|
return data['c'] as unknown as number; |
|
|
|
} |
|
|
|
|
|
|
|
async getById(id: ID) { |
|
|
@@ -148,11 +200,11 @@ export class DuckDbDataSource< |
|
|
|
const stmt = await con.prepare(` |
|
|
|
SELECT * FROM ${this.resource.state.routeName} WHERE ${idAttr} = ${JSON.stringify(id).replace(/"/g, "'")}; |
|
|
|
`); |
|
|
|
const data = await stmt.all(); |
|
|
|
return data as Data; |
|
|
|
const [data = null] = await stmt.all(); |
|
|
|
return data as Data | null; |
|
|
|
} |
|
|
|
|
|
|
|
async getSingle() { |
|
|
|
async getSingle(query) { |
|
|
|
assert(typeof this.db !== 'undefined'); |
|
|
|
assert(typeof this.resource !== 'undefined'); |
|
|
|
|
|
|
@@ -160,8 +212,8 @@ export class DuckDbDataSource< |
|
|
|
const stmt = await con.prepare(` |
|
|
|
SELECT * FROM ${this.resource.state.routeName} LIMIT 1; |
|
|
|
`); |
|
|
|
const [data] = await stmt.all(); |
|
|
|
return data as Data; |
|
|
|
const [data = null] = await stmt.all(); |
|
|
|
return data as Data | null; |
|
|
|
} |
|
|
|
|
|
|
|
async delete(id: ID) { |
|
|
|