From 0a9e1f816d85fe9601b3fa152d74dc9668b6d11b Mon Sep 17 00:00:00 2001 From: erezrokah Date: Fri, 11 Aug 2023 18:56:42 +0200 Subject: [PATCH] feat: Add UUID scalar --- src/scalar/scalar.ts | 7 ++++ src/scalar/text.ts | 2 +- src/scalar/uuid.ts | 68 ++++++++++++++++++++++++++++++++++++++ src/scheduler/cqid.test.ts | 53 +++++++++++++++++++---------- src/scheduler/cqid.ts | 24 +++++++------- src/schema/meta.test.ts | 4 +-- src/types/uuid.ts | 10 ++++++ 7 files changed, 136 insertions(+), 32 deletions(-) create mode 100644 src/scalar/uuid.ts diff --git a/src/scalar/scalar.ts b/src/scalar/scalar.ts index 13495ad..9b4aa8d 100644 --- a/src/scalar/scalar.ts +++ b/src/scalar/scalar.ts @@ -1,5 +1,7 @@ import { DataType, Precision } from '@apache-arrow/esnext-esm'; +import { UUIDType } from '../types/uuid.js'; + import { Bool } from './bool.js'; import { Date } from './date.js'; import { Float32 } from './float32.js'; @@ -13,6 +15,7 @@ import { Timestamp } from './timestamp.js'; import { Uint16 } from './uint16.js'; import { Uint32 } from './uint32.js'; import { Uint64 } from './uint64.js'; +import { UUID } from './uuid.js'; export type Stringable = { toString: () => string }; @@ -86,5 +89,9 @@ export const newScalar = (dataType: DataType): Scalar => { return new Date(dataType.unit); } + if (dataType instanceof UUIDType) { + return new UUID(); + } + return new Text(); }; diff --git a/src/scalar/text.ts b/src/scalar/text.ts index d39a91b..83fad97 100644 --- a/src/scalar/text.ts +++ b/src/scalar/text.ts @@ -59,7 +59,7 @@ export class Text implements Scalar { public toString() { if (this._valid) { - return this._value.toString(); + return this._value; } return NULL_VALUE; diff --git a/src/scalar/uuid.ts b/src/scalar/uuid.ts new file mode 100644 index 0000000..41a1db2 --- /dev/null +++ b/src/scalar/uuid.ts @@ -0,0 +1,68 @@ +import { Utf8 as ArrowString } from '@apache-arrow/esnext-esm'; +import { validate } from 'uuid'; + +import { Scalar } from './scalar.js'; +import { isInvalid, NULL_VALUE } from './util.js'; + +export class UUID implements Scalar { + private _valid = false; + private _value = ''; + + public constructor(v?: unknown) { + this.value = v; + return this; + } + + public get dataType() { + return new ArrowString(); + } + + public get valid(): boolean { + return this._valid; + } + + public get value(): string { + return this._value; + } + + public set value(value: unknown) { + if (isInvalid(value)) { + this._valid = false; + return; + } + + if (typeof value === 'string') { + this._value = value; + this._valid = validate(value); + return; + } + + if (value instanceof Uint8Array) { + this._value = new TextDecoder().decode(value); + this._valid = validate(this._value); + return; + } + + if (value instanceof UUID) { + this._value = value.value; + this._valid = value.valid; + return; + } + + if (typeof value!.toString === 'function' && value!.toString !== Object.prototype.toString) { + this._value = value!.toString(); + this._valid = validate(this._value); + return; + } + + throw new Error(`Unable to set '${value}' as UUID`); + } + + public toString() { + if (this._valid) { + return this._value; + } + + return NULL_VALUE; + } +} diff --git a/src/scheduler/cqid.test.ts b/src/scheduler/cqid.test.ts index ea5fa48..bd7f778 100644 --- a/src/scheduler/cqid.test.ts +++ b/src/scheduler/cqid.test.ts @@ -1,6 +1,5 @@ -import { createHash } from 'node:crypto'; - import test from 'ava'; +import { NIL as NIL_UUID } from 'uuid'; import { createColumn } from '../schema/column.js'; import { addCQIDsColumns, cqIDColumn } from '../schema/meta.js'; @@ -10,21 +9,49 @@ import { createTable } from '../schema/table.js'; import { setCQId } from './cqid.js'; test('setCQId - should set to random value if deterministicCQId is false', (t): void => { - const resource = new Resource(addCQIDsColumns(createTable({ name: 'table1' })), null, null); + const resource = new Resource( + addCQIDsColumns( + createTable({ + name: 'table1', + columns: [ + createColumn({ name: 'pk1', primaryKey: true, unique: true, notNull: true }), + createColumn({ name: 'pk2', primaryKey: true, unique: true, notNull: true }), + createColumn({ name: 'pk3', primaryKey: true, unique: true, notNull: true }), + createColumn({ name: 'non_pk' }), + ], + }), + ), + null, + null, + ); - setCQId(resource, false, () => 'random'); + setCQId(resource, false, () => NIL_UUID); t.is(resource.getColumnData(cqIDColumn.name).valid, true); - t.is(resource.getColumnData(cqIDColumn.name).value.toString(), 'random'); + t.is(resource.getColumnData(cqIDColumn.name).value.toString(), NIL_UUID); }); test('setCQId - should set to random value if deterministicCQId is true and table does not have non _cq_id PKs', (t): void => { - const resource = new Resource(addCQIDsColumns(createTable({ name: 'table1' })), null, null); + const resource = new Resource( + addCQIDsColumns( + createTable({ + name: 'table1', + columns: [ + createColumn({ name: 'pk1', primaryKey: false, unique: true, notNull: true }), + createColumn({ name: 'pk2', primaryKey: false, unique: true, notNull: true }), + createColumn({ name: 'pk3', primaryKey: false, unique: true, notNull: true }), + createColumn({ name: 'non_pk' }), + ], + }), + ), + null, + null, + ); - setCQId(resource, true, () => 'random'); + setCQId(resource, true, () => NIL_UUID); t.is(resource.getColumnData(cqIDColumn.name).valid, true); - t.is(resource.getColumnData(cqIDColumn.name).value.toString(), 'random'); + t.is(resource.getColumnData(cqIDColumn.name).value.toString(), NIL_UUID); }); test('setCQId - should set to fixed value if deterministicCQId is true and table has non _cq_id PKs', (t): void => { @@ -49,16 +76,8 @@ test('setCQId - should set to fixed value if deterministicCQId is true and table resource.setColumData('pk3', 'pk3-value'); resource.setColumData('non_pk', 'non-pk-value'); - const expectedSha256 = createHash('sha256'); - expectedSha256.update('pk1'); - expectedSha256.update('pk1-value'); - expectedSha256.update('pk2'); - expectedSha256.update('pk2-value'); - expectedSha256.update('pk3'); - expectedSha256.update('pk3-value'); - setCQId(resource, true); t.is(resource.getColumnData(cqIDColumn.name).valid, true); - t.is(resource.getColumnData(cqIDColumn.name).value.toString(), expectedSha256.digest('hex')); + t.is(resource.getColumnData(cqIDColumn.name).value.toString(), '415bd5dd-9bac-5806-b9d1-c53f17d37455'); }); diff --git a/src/scheduler/cqid.ts b/src/scheduler/cqid.ts index 4e4ecdd..58049f0 100644 --- a/src/scheduler/cqid.ts +++ b/src/scheduler/cqid.ts @@ -1,6 +1,6 @@ import { createHash } from 'node:crypto'; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4, v5 as uuidv5, NIL as NIL_UUID } from 'uuid'; import { cqIDColumn } from '../schema/meta.js'; import { Resource } from '../schema/resource.js'; @@ -9,20 +9,20 @@ import { getPrimaryKeys } from '../schema/table.js'; export const setCQId = (resource: Resource, deterministicCQId: boolean, generator: () => string = uuidv4) => { const randomCQId = generator(); if (!deterministicCQId) { - resource.setCqId(randomCQId); + return resource.setCqId(randomCQId); } const primaryKeys = getPrimaryKeys(resource.table); - const hasNonCqPKs = primaryKeys.some((pk) => pk !== cqIDColumn.name); - if (hasNonCqPKs) { - const sha256 = createHash('sha256'); - primaryKeys.sort(); - for (const pk of primaryKeys) { - sha256.update(pk); - sha256.update(resource.getColumnData(pk).toString()); - } - return resource.setCqId(sha256.digest('hex')); + const cqOnlyPK = primaryKeys.every((pk) => pk === cqIDColumn.name); + if (cqOnlyPK) { + return resource.setCqId(randomCQId); } - return resource.setCqId(randomCQId); + const sha256 = createHash('sha256'); + primaryKeys.sort(); + for (const pk of primaryKeys) { + sha256.update(pk); + sha256.update(resource.getColumnData(pk).toString()); + } + return resource.setCqId(uuidv5(sha256.digest('hex'), NIL_UUID)); }; diff --git a/src/schema/meta.test.ts b/src/schema/meta.test.ts index cdb582f..08afbfa 100644 --- a/src/schema/meta.test.ts +++ b/src/schema/meta.test.ts @@ -90,11 +90,11 @@ test('parentCqUUIDResolver - should set to _cq_id column value when parent has i const table = addCQIDsColumns(createTable({ name: 'table1', relations: [createTable({ name: 'table1-child1' })] })); const parentResource = new Resource(table, null, null); - parentResource.setColumData(cqIDColumn.name, 'parent-cq-id'); + parentResource.setColumData(cqIDColumn.name, '9241a9cb-f580-420f-8fd7-46d2c4f55ccb'); const childResource = new Resource(table.relations[0], parentResource, null); parentCqUUIDResolver()({ id: () => '' }, childResource, cqParentIDColumn); - t.is(childResource.getColumnData(cqParentIDColumn.name).value, 'parent-cq-id'); + t.is(childResource.getColumnData(cqParentIDColumn.name).value, '9241a9cb-f580-420f-8fd7-46d2c4f55ccb'); t.is(childResource.getColumnData(cqParentIDColumn.name).valid, true); }); diff --git a/src/types/uuid.ts b/src/types/uuid.ts index 62fa404..d0ee013 100644 --- a/src/types/uuid.ts +++ b/src/types/uuid.ts @@ -10,4 +10,14 @@ export class UUIDType extends DataType { get typeId(): Type.Utf8 { return Type.Utf8; } + + public toString() { + return `uuid`; + } + + protected static [Symbol.toStringTag] = ((proto: UUIDType) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (proto).ArrayType = Uint8Array; + return (proto[Symbol.toStringTag] = 'uuid'); + })(UUIDType.prototype); }