Browse Source

Add DuckDB sequences

Include sequences in data source definition.
master
TheoryOfNekomata 8 months ago
parent
commit
268c41c912
7 changed files with 82 additions and 21 deletions
  1. +5
    -0
      TODO.md
  2. +1
    -0
      packages/core/test/features/decorators.test.ts
  3. +72
    -20
      packages/data-sources/duckdb/src/index.ts
  4. +2
    -0
      packages/examples/duckdb/.gitignore
  5. +2
    -1
      packages/examples/duckdb/src/index.ts
  6. BIN
      packages/examples/duckdb/test.db
  7. BIN
      packages/examples/duckdb/test.db.wal

+ 5
- 0
TODO.md View File

@@ -1,12 +1,16 @@
- [ ] Integrate with other data stores
- [ ] SQLite
- [ ] PostgreSQL
- [ ] DuckDB
- [X] Access control with resources
- [ ] Custom definitions
- [ ] Middlewares
- [ ] Request decorators
- [ ] Status messages
- [ ] Response bodies (in case of error messages)
- [ ] Resource operations
- [ ] Single item
- [ ] Collection
- [ ] Content negotiation
- [X] Language
- [X] Charset
@@ -32,3 +36,4 @@
- [ ] Swagger docs plugin
- [ ] Plugin support
- [ ] Add option to reject content negotiation params with `406 Not Acceptable`
- [ ] Add `fast-check` for property-based checking

+ 1
- 0
packages/core/test/features/decorators.test.ts View File

@@ -71,6 +71,7 @@ describe('decorators', () => {
resolve();
});

// TODO add .inject() method
server.listen({
port: PORT
});


+ 72
- 20
packages/data-sources/duckdb/src/index.ts View File

@@ -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) {


+ 2
- 0
packages/examples/duckdb/.gitignore View File

@@ -105,3 +105,5 @@ dist
.tern-port

.npmrc
*.db
*.db.wal

+ 2
- 1
packages/examples/duckdb/src/index.ts View File

@@ -27,8 +27,9 @@ const app = application({
.resource(Post);

const backend = app.createBackend({
dataSource: new DuckDbDataSource(),
dataSource: new DuckDbDataSource('test.db'),
})
.showTotalItemCountOnGetCollection()
.throwsErrorOnDeletingNotFound();

const server = backend.createHttpServer({


BIN
packages/examples/duckdb/test.db View File


BIN
packages/examples/duckdb/test.db.wal View File


Loading…
Cancel
Save