diff --git a/api/pkgs/@duckdb/node-api/README.md b/api/pkgs/@duckdb/node-api/README.md index 3bfd2d8..e64b6a1 100644 --- a/api/pkgs/@duckdb/node-api/README.md +++ b/api/pkgs/@duckdb/node-api/README.md @@ -191,6 +191,12 @@ function typeToString(dataType) { } ``` +While the example above demonstrates how to access the properties of data type objects, there is a much simpler way to convert them to strings: + +```ts +const dataTypeString = dataType.toString(); +``` + ### Inspect Data Values ```ts diff --git a/api/src/DuckDBType.ts b/api/src/DuckDBType.ts index 773bcad..ddb629c 100644 --- a/api/src/DuckDBType.ts +++ b/api/src/DuckDBType.ts @@ -1,10 +1,14 @@ import { DuckDBTypeId } from './DuckDBTypeId'; +import { quotedIdentifier, quotedString } from './sql'; export abstract class BaseDuckDBType { public readonly typeId: DuckDBTypeId; protected constructor(typeId: DuckDBTypeId) { this.typeId = typeId; } + public toString(): string { + return DuckDBTypeId[this.typeId]; + } } export class DuckDBBooleanType extends BaseDuckDBType { @@ -150,6 +154,9 @@ export class DuckDBDecimalType extends BaseDuckDBType { this.width = width; this.scale = scale; } + public toString(): string { + return `DECIMAL(${this.width},${this.scale})`; + } public static readonly default = new DuckDBDecimalType(18, 3); } @@ -182,6 +189,9 @@ export class DuckDBEnumType extends BaseDuckDBType { this.values = values; this.internalTypeId = internalTypeId; } + public toString(): string { + return `ENUM(${this.values.map(quotedString).join(', ')})`; + } } export class DuckDBListType extends BaseDuckDBType { @@ -190,6 +200,9 @@ export class DuckDBListType extends BaseDuckDBType { super(DuckDBTypeId.LIST); this.valueType = valueType; } + public toString(): string { + return `${this.valueType}[]`; + } } export interface DuckDBStructEntryType { @@ -203,6 +216,11 @@ export class DuckDBStructType extends BaseDuckDBType { super(DuckDBTypeId.STRUCT); this.entries = entries; } + public toString(): string { + return `STRUCT(${this.entries.map( + entry => `${quotedIdentifier(entry.name)} ${entry.valueType}` + ).join(', ')})`; + } } export class DuckDBMapType extends BaseDuckDBType { @@ -213,6 +231,9 @@ export class DuckDBMapType extends BaseDuckDBType { this.keyType = keyType; this.valueType = valueType; } + public toString(): string { + return `MAP(${this.keyType}, ${this.valueType})`; + } } export class DuckDBArrayType extends BaseDuckDBType { @@ -223,6 +244,9 @@ export class DuckDBArrayType extends BaseDuckDBType { this.valueType = valueType; this.length = length; } + public toString(): string { + return `${this.valueType}[${this.length}]`; + } } export class DuckDBUUIDType extends BaseDuckDBType { @@ -243,6 +267,11 @@ export class DuckDBUnionType extends BaseDuckDBType { super(DuckDBTypeId.UNION); this.alternatives = alternatives; } + public toString(): string { + return `UNION(${this.alternatives.map( + entry => `${quotedIdentifier(entry.tag)} ${entry.valueType}` + ).join(', ')})`; + } } export class DuckDBBitType extends BaseDuckDBType { @@ -256,6 +285,9 @@ export class DuckDBTimeTZType extends BaseDuckDBType { private constructor() { super(DuckDBTypeId.TIME_TZ); } + public toString(): string { + return "TIME WITH TIME ZONE"; + } public static readonly instance = new DuckDBTimeTZType(); } @@ -263,6 +295,9 @@ export class DuckDBTimestampTZType extends BaseDuckDBType { private constructor() { super(DuckDBTypeId.TIMESTAMP_TZ); } + public toString(): string { + return "TIMESTAMP WITH TIME ZONE"; + } public static readonly instance = new DuckDBTimestampTZType(); } diff --git a/api/src/index.ts b/api/src/index.ts index a9604a7..f81a423 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -13,4 +13,5 @@ export * from './DuckDBTypeId'; export * from './DuckDBValue'; export * from './DuckDBVector'; export * from './enums'; +export * from './sql'; export * from './version'; diff --git a/api/src/sql.ts b/api/src/sql.ts new file mode 100644 index 0000000..91460da --- /dev/null +++ b/api/src/sql.ts @@ -0,0 +1,7 @@ +export function quotedString(input: string): string { + return `'${input.replace(`'`, `''`)}'`; +} + +export function quotedIdentifier(input: string): string { + return `"${input.replace(`"`, `""`)}"`; +} diff --git a/api/test/api.test.ts b/api/test/api.test.ts index 3ca70c3..0fad963 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -1,5 +1,6 @@ import { assert, describe, test } from 'vitest'; import { + DuckDBAnyType, DuckDBArrayType, DuckDBArrayVector, DuckDBBigIntType, @@ -44,6 +45,7 @@ import { DuckDBMapVector, DuckDBPendingResultState, DuckDBResult, + DuckDBSQLNullType, DuckDBSmallDecimal, DuckDBSmallIntType, DuckDBSmallIntVector, @@ -283,6 +285,10 @@ describe('api', () => { const ver = version(); assert.ok(ver.startsWith('v'), `version starts with 'v'`); }); + test('should expose configuration option descriptions', () => { + const descriptions = configurationOptionDescriptions(); + assert.ok(descriptions['memory_limit'], `descriptions has 'memory_limit'`); + }); test('ReturnResultType enum', () => { assert.equal(ResultReturnType.INVALID, 0); assert.equal(ResultReturnType.CHANGED_ROWS, 1); @@ -353,9 +359,55 @@ describe('api', () => { assert.equal(StatementType[StatementType.DETACH], 'DETACH'); assert.equal(StatementType[StatementType.MULTI], 'MULTI'); }); - test('should expose configuration option descriptions', () => { - const descriptions = configurationOptionDescriptions(); - assert.ok(descriptions['memory_limit'], `descriptions has 'memory_limit'`); + test('DuckDBType toString', () => { + assert.equal(DuckDBBooleanType.instance.toString(), 'BOOLEAN'); + assert.equal(DuckDBTinyIntType.instance.toString(), 'TINYINT'); + assert.equal(DuckDBSmallIntType.instance.toString(), 'SMALLINT'); + assert.equal(DuckDBIntegerType.instance.toString(), 'INTEGER'); + assert.equal(DuckDBBigIntType.instance.toString(), 'BIGINT'); + assert.equal(DuckDBUTinyIntType.instance.toString(), 'UTINYINT'); + assert.equal(DuckDBUSmallIntType.instance.toString(), 'USMALLINT'); + assert.equal(DuckDBUIntegerType.instance.toString(), 'UINTEGER'); + assert.equal(DuckDBUBigIntType.instance.toString(), 'UBIGINT'); + assert.equal(DuckDBFloatType.instance.toString(), 'FLOAT'); + assert.equal(DuckDBDoubleType.instance.toString(), 'DOUBLE'); + assert.equal(DuckDBTimestampType.instance.toString(), 'TIMESTAMP'); + assert.equal(DuckDBDateType.instance.toString(), 'DATE'); + assert.equal(DuckDBTimeType.instance.toString(), 'TIME'); + assert.equal(DuckDBIntervalType.instance.toString(), 'INTERVAL'); + assert.equal(DuckDBHugeIntType.instance.toString(), 'HUGEINT'); + assert.equal(DuckDBUHugeIntType.instance.toString(), 'UHUGEINT'); + assert.equal(DuckDBVarCharType.instance.toString(), 'VARCHAR'); + assert.equal(DuckDBBlobType.instance.toString(), 'BLOB'); + assert.equal((new DuckDBDecimalType(17, 5)).toString(), 'DECIMAL(17,5)'); + assert.equal(DuckDBTimestampSecondsType.instance.toString(), 'TIMESTAMP_S'); + assert.equal(DuckDBTimestampMillisecondsType.instance.toString(), 'TIMESTAMP_MS'); + assert.equal(DuckDBTimestampNanosecondsType.instance.toString(), 'TIMESTAMP_NS'); + assert.equal( + (new DuckDBEnumType(['fly', 'swim', 'walk'], DuckDBTypeId.UTINYINT)).toString(), + `ENUM('fly', 'swim', 'walk')` + ); + assert.equal((new DuckDBListType(DuckDBIntegerType.instance)).toString(), 'INTEGER[]'); + assert.equal((new DuckDBStructType([ + { name: 'id', valueType: DuckDBVarCharType.instance }, + { name: 'ts', valueType: DuckDBTimestampType.instance }, + ])).toString(), 'STRUCT("id" VARCHAR, "ts" TIMESTAMP)'); + assert.equal( + (new DuckDBMapType(DuckDBIntegerType.instance, DuckDBVarCharType.instance)).toString(), + 'MAP(INTEGER, VARCHAR)' + ); + assert.equal((new DuckDBArrayType(DuckDBIntegerType.instance, 3)).toString(), 'INTEGER[3]'); + assert.equal(DuckDBUUIDType.instance.toString(), 'UUID'); + assert.equal((new DuckDBUnionType([ + { tag: 'str', valueType: DuckDBVarCharType.instance }, + { tag: 'num', valueType: DuckDBIntegerType.instance }, + ])).toString(), 'UNION("str" VARCHAR, "num" INTEGER)'); + assert.equal(DuckDBBitType.instance.toString(), 'BIT'); + assert.equal(DuckDBTimeTZType.instance.toString(), 'TIME WITH TIME ZONE'); + assert.equal(DuckDBTimestampTZType.instance.toString(), 'TIMESTAMP WITH TIME ZONE'); + assert.equal(DuckDBAnyType.instance.toString(), 'ANY'); + assert.equal(DuckDBVarIntType.instance.toString(), 'VARINT'); + assert.equal(DuckDBSQLNullType.instance.toString(), 'SQLNULL'); }); test('should support creating, connecting, running a basic query, and reading results', async () => { const instance = await DuckDBInstance.create();