Skip to content

Commit d8474ce

Browse files
authored
feat: Add UUID scalar (#54)
1 parent a9589b6 commit d8474ce

File tree

7 files changed

+136
-32
lines changed

7 files changed

+136
-32
lines changed

src/scalar/scalar.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { DataType, Precision } from '@apache-arrow/esnext-esm';
22

3+
import { UUIDType } from '../types/uuid.js';
4+
35
import { Bool } from './bool.js';
46
import { Date } from './date.js';
57
import { Float32 } from './float32.js';
@@ -13,6 +15,7 @@ import { Timestamp } from './timestamp.js';
1315
import { Uint16 } from './uint16.js';
1416
import { Uint32 } from './uint32.js';
1517
import { Uint64 } from './uint64.js';
18+
import { UUID } from './uuid.js';
1619

1720
export type Stringable = { toString: () => string };
1821

@@ -86,5 +89,9 @@ export const newScalar = (dataType: DataType): Scalar<Stringable> => {
8689
return new Date(dataType.unit);
8790
}
8891

92+
if (dataType instanceof UUIDType) {
93+
return new UUID();
94+
}
95+
8996
return new Text();
9097
};

src/scalar/text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class Text implements Scalar<string> {
5959

6060
public toString() {
6161
if (this._valid) {
62-
return this._value.toString();
62+
return this._value;
6363
}
6464

6565
return NULL_VALUE;

src/scalar/uuid.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Utf8 as ArrowString } from '@apache-arrow/esnext-esm';
2+
import { validate } from 'uuid';
3+
4+
import { Scalar } from './scalar.js';
5+
import { isInvalid, NULL_VALUE } from './util.js';
6+
7+
export class UUID implements Scalar<string> {
8+
private _valid = false;
9+
private _value = '';
10+
11+
public constructor(v?: unknown) {
12+
this.value = v;
13+
return this;
14+
}
15+
16+
public get dataType() {
17+
return new ArrowString();
18+
}
19+
20+
public get valid(): boolean {
21+
return this._valid;
22+
}
23+
24+
public get value(): string {
25+
return this._value;
26+
}
27+
28+
public set value(value: unknown) {
29+
if (isInvalid(value)) {
30+
this._valid = false;
31+
return;
32+
}
33+
34+
if (typeof value === 'string') {
35+
this._value = value;
36+
this._valid = validate(value);
37+
return;
38+
}
39+
40+
if (value instanceof Uint8Array) {
41+
this._value = new TextDecoder().decode(value);
42+
this._valid = validate(this._value);
43+
return;
44+
}
45+
46+
if (value instanceof UUID) {
47+
this._value = value.value;
48+
this._valid = value.valid;
49+
return;
50+
}
51+
52+
if (typeof value!.toString === 'function' && value!.toString !== Object.prototype.toString) {
53+
this._value = value!.toString();
54+
this._valid = validate(this._value);
55+
return;
56+
}
57+
58+
throw new Error(`Unable to set '${value}' as UUID`);
59+
}
60+
61+
public toString() {
62+
if (this._valid) {
63+
return this._value;
64+
}
65+
66+
return NULL_VALUE;
67+
}
68+
}

src/scheduler/cqid.test.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { createHash } from 'node:crypto';
2-
31
import test from 'ava';
2+
import { NIL as NIL_UUID } from 'uuid';
43

54
import { createColumn } from '../schema/column.js';
65
import { addCQIDsColumns, cqIDColumn } from '../schema/meta.js';
@@ -10,21 +9,49 @@ import { createTable } from '../schema/table.js';
109
import { setCQId } from './cqid.js';
1110

1211
test('setCQId - should set to random value if deterministicCQId is false', (t): void => {
13-
const resource = new Resource(addCQIDsColumns(createTable({ name: 'table1' })), null, null);
12+
const resource = new Resource(
13+
addCQIDsColumns(
14+
createTable({
15+
name: 'table1',
16+
columns: [
17+
createColumn({ name: 'pk1', primaryKey: true, unique: true, notNull: true }),
18+
createColumn({ name: 'pk2', primaryKey: true, unique: true, notNull: true }),
19+
createColumn({ name: 'pk3', primaryKey: true, unique: true, notNull: true }),
20+
createColumn({ name: 'non_pk' }),
21+
],
22+
}),
23+
),
24+
null,
25+
null,
26+
);
1427

15-
setCQId(resource, false, () => 'random');
28+
setCQId(resource, false, () => NIL_UUID);
1629

1730
t.is(resource.getColumnData(cqIDColumn.name).valid, true);
18-
t.is(resource.getColumnData(cqIDColumn.name).value.toString(), 'random');
31+
t.is(resource.getColumnData(cqIDColumn.name).value.toString(), NIL_UUID);
1932
});
2033

2134
test('setCQId - should set to random value if deterministicCQId is true and table does not have non _cq_id PKs', (t): void => {
22-
const resource = new Resource(addCQIDsColumns(createTable({ name: 'table1' })), null, null);
35+
const resource = new Resource(
36+
addCQIDsColumns(
37+
createTable({
38+
name: 'table1',
39+
columns: [
40+
createColumn({ name: 'pk1', primaryKey: false, unique: true, notNull: true }),
41+
createColumn({ name: 'pk2', primaryKey: false, unique: true, notNull: true }),
42+
createColumn({ name: 'pk3', primaryKey: false, unique: true, notNull: true }),
43+
createColumn({ name: 'non_pk' }),
44+
],
45+
}),
46+
),
47+
null,
48+
null,
49+
);
2350

24-
setCQId(resource, true, () => 'random');
51+
setCQId(resource, true, () => NIL_UUID);
2552

2653
t.is(resource.getColumnData(cqIDColumn.name).valid, true);
27-
t.is(resource.getColumnData(cqIDColumn.name).value.toString(), 'random');
54+
t.is(resource.getColumnData(cqIDColumn.name).value.toString(), NIL_UUID);
2855
});
2956

3057
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
4976
resource.setColumData('pk3', 'pk3-value');
5077
resource.setColumData('non_pk', 'non-pk-value');
5178

52-
const expectedSha256 = createHash('sha256');
53-
expectedSha256.update('pk1');
54-
expectedSha256.update('pk1-value');
55-
expectedSha256.update('pk2');
56-
expectedSha256.update('pk2-value');
57-
expectedSha256.update('pk3');
58-
expectedSha256.update('pk3-value');
59-
6079
setCQId(resource, true);
6180

6281
t.is(resource.getColumnData(cqIDColumn.name).valid, true);
63-
t.is(resource.getColumnData(cqIDColumn.name).value.toString(), expectedSha256.digest('hex'));
82+
t.is(resource.getColumnData(cqIDColumn.name).value.toString(), '415bd5dd-9bac-5806-b9d1-c53f17d37455');
6483
});

src/scheduler/cqid.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createHash } from 'node:crypto';
22

3-
import { v4 as uuidv4 } from 'uuid';
3+
import { v4 as uuidv4, v5 as uuidv5, NIL as NIL_UUID } from 'uuid';
44

55
import { cqIDColumn } from '../schema/meta.js';
66
import { Resource } from '../schema/resource.js';
@@ -9,20 +9,20 @@ import { getPrimaryKeys } from '../schema/table.js';
99
export const setCQId = (resource: Resource, deterministicCQId: boolean, generator: () => string = uuidv4) => {
1010
const randomCQId = generator();
1111
if (!deterministicCQId) {
12-
resource.setCqId(randomCQId);
12+
return resource.setCqId(randomCQId);
1313
}
1414

1515
const primaryKeys = getPrimaryKeys(resource.table);
16-
const hasNonCqPKs = primaryKeys.some((pk) => pk !== cqIDColumn.name);
17-
if (hasNonCqPKs) {
18-
const sha256 = createHash('sha256');
19-
primaryKeys.sort();
20-
for (const pk of primaryKeys) {
21-
sha256.update(pk);
22-
sha256.update(resource.getColumnData(pk).toString());
23-
}
24-
return resource.setCqId(sha256.digest('hex'));
16+
const cqOnlyPK = primaryKeys.every((pk) => pk === cqIDColumn.name);
17+
if (cqOnlyPK) {
18+
return resource.setCqId(randomCQId);
2519
}
2620

27-
return resource.setCqId(randomCQId);
21+
const sha256 = createHash('sha256');
22+
primaryKeys.sort();
23+
for (const pk of primaryKeys) {
24+
sha256.update(pk);
25+
sha256.update(resource.getColumnData(pk).toString());
26+
}
27+
return resource.setCqId(uuidv5(sha256.digest('hex'), NIL_UUID));
2828
};

src/schema/meta.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@ test('parentCqUUIDResolver - should set to _cq_id column value when parent has i
9090
const table = addCQIDsColumns(createTable({ name: 'table1', relations: [createTable({ name: 'table1-child1' })] }));
9191

9292
const parentResource = new Resource(table, null, null);
93-
parentResource.setColumData(cqIDColumn.name, 'parent-cq-id');
93+
parentResource.setColumData(cqIDColumn.name, '9241a9cb-f580-420f-8fd7-46d2c4f55ccb');
9494
const childResource = new Resource(table.relations[0], parentResource, null);
9595

9696
parentCqUUIDResolver()({ id: () => '' }, childResource, cqParentIDColumn);
9797

98-
t.is(childResource.getColumnData(cqParentIDColumn.name).value, 'parent-cq-id');
98+
t.is(childResource.getColumnData(cqParentIDColumn.name).value, '9241a9cb-f580-420f-8fd7-46d2c4f55ccb');
9999
t.is(childResource.getColumnData(cqParentIDColumn.name).valid, true);
100100
});

src/types/uuid.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,14 @@ export class UUIDType extends DataType<Type.Utf8> {
1010
get typeId(): Type.Utf8 {
1111
return Type.Utf8;
1212
}
13+
14+
public toString() {
15+
return `uuid`;
16+
}
17+
18+
protected static [Symbol.toStringTag] = ((proto: UUIDType) => {
19+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20+
(<any>proto).ArrayType = Uint8Array;
21+
return (proto[Symbol.toStringTag] = 'uuid');
22+
})(UUIDType.prototype);
1323
}

0 commit comments

Comments
 (0)