From 6cfcf94235d0ad6d04cf54e0b7d8b8f68770e8f7 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Thu, 31 Oct 2024 00:24:19 -0700 Subject: [PATCH 1/2] values to string --- api/src/DuckDBType.ts | 26 + api/src/DuckDBVector.ts | 53 +- .../conversion/dateTimeStringConversion.ts | 209 +++++++ .../conversion/displayStringForDuckDBValue.ts | 12 + api/src/conversion/stringFromBlob.ts | 17 + api/src/conversion/stringFromDecimal.ts | 129 ++++ api/src/values/DuckDBArrayValue.ts | 21 +- api/src/values/DuckDBBitValue.ts | 16 +- api/src/values/DuckDBBlobValue.ts | 17 +- api/src/values/DuckDBDateValue.ts | 18 +- api/src/values/DuckDBDecimalValue.ts | 44 +- api/src/values/DuckDBIntervalValue.ts | 10 +- api/src/values/DuckDBListValue.ts | 21 +- api/src/values/DuckDBMapValue.ts | 32 +- api/src/values/DuckDBStructValue.ts | 26 +- api/src/values/DuckDBTimeTZValue.ts | 31 +- api/src/values/DuckDBTimeValue.ts | 13 +- .../DuckDBTimestampMillisecondsValue.ts | 15 +- .../values/DuckDBTimestampNanosecondsValue.ts | 14 +- api/src/values/DuckDBTimestampSecondsValue.ts | 14 +- api/src/values/DuckDBTimestampTZValue.ts | 18 +- api/src/values/DuckDBTimestampValue.ts | 17 +- api/src/values/DuckDBUUIDValue.ts | 16 +- api/src/values/DuckDBUnionValue.ts | 17 +- api/test/api.test.ts | 569 +++++++----------- 25 files changed, 916 insertions(+), 459 deletions(-) create mode 100644 api/src/conversion/dateTimeStringConversion.ts create mode 100644 api/src/conversion/displayStringForDuckDBValue.ts create mode 100644 api/src/conversion/stringFromBlob.ts create mode 100644 api/src/conversion/stringFromDecimal.ts diff --git a/api/src/DuckDBType.ts b/api/src/DuckDBType.ts index cb11c9f6..21a988b3 100644 --- a/api/src/DuckDBType.ts +++ b/api/src/DuckDBType.ts @@ -23,6 +23,8 @@ export class DuckDBTinyIntType extends BaseDuckDBType { super(DuckDBTypeId.TINYINT); } public static readonly instance = new DuckDBTinyIntType(); + public static readonly Max = 2 ** 7 - 1; + public static readonly Min = -(2 ** 7); } export class DuckDBSmallIntType extends BaseDuckDBType { @@ -30,6 +32,8 @@ export class DuckDBSmallIntType extends BaseDuckDBType { super(DuckDBTypeId.SMALLINT); } public static readonly instance = new DuckDBSmallIntType(); + public static readonly Max = 2 ** 15 - 1; + public static readonly Min = -(2 ** 15); } export class DuckDBIntegerType extends BaseDuckDBType { @@ -37,6 +41,8 @@ export class DuckDBIntegerType extends BaseDuckDBType { super(DuckDBTypeId.INTEGER); } public static readonly instance = new DuckDBIntegerType(); + public static readonly Max = 2 ** 31 - 1; + public static readonly Min = -(2 ** 31); } export class DuckDBBigIntType extends BaseDuckDBType { @@ -44,6 +50,8 @@ export class DuckDBBigIntType extends BaseDuckDBType { super(DuckDBTypeId.BIGINT); } public static readonly instance = new DuckDBBigIntType(); + public static readonly Max = 2n ** 63n - 1n; + public static readonly Min = -(2n ** 63n); } export class DuckDBUTinyIntType extends BaseDuckDBType { @@ -51,6 +59,8 @@ export class DuckDBUTinyIntType extends BaseDuckDBType { super(DuckDBTypeId.UTINYINT); } public static readonly instance = new DuckDBUTinyIntType(); + public static readonly Max = 2 ** 8 - 1; + public static readonly Min = 0; } export class DuckDBUSmallIntType extends BaseDuckDBType { @@ -58,6 +68,8 @@ export class DuckDBUSmallIntType extends BaseDuckDBType super(DuckDBTypeId.USMALLINT); } public static readonly instance = new DuckDBUSmallIntType(); + public static readonly Max = 2 ** 16 - 1; + public static readonly Min = 0; } export class DuckDBUIntegerType extends BaseDuckDBType { @@ -65,6 +77,8 @@ export class DuckDBUIntegerType extends BaseDuckDBType { super(DuckDBTypeId.UINTEGER); } public static readonly instance = new DuckDBUIntegerType(); + public static readonly Max = 2 ** 32 - 1; + public static readonly Min = 0; } export class DuckDBUBigIntType extends BaseDuckDBType { @@ -72,6 +86,8 @@ export class DuckDBUBigIntType extends BaseDuckDBType { super(DuckDBTypeId.UBIGINT); } public static readonly instance = new DuckDBUBigIntType(); + public static readonly Max = 2n ** 64n - 1n; + public static readonly Min = 0n; } export class DuckDBFloatType extends BaseDuckDBType { @@ -79,6 +95,8 @@ export class DuckDBFloatType extends BaseDuckDBType { super(DuckDBTypeId.FLOAT); } public static readonly instance = new DuckDBFloatType(); + public static readonly Min = Math.fround(-3.4028235e+38); + public static readonly Max = Math.fround( 3.4028235e+38); } export class DuckDBDoubleType extends BaseDuckDBType { @@ -86,6 +104,8 @@ export class DuckDBDoubleType extends BaseDuckDBType { super(DuckDBTypeId.DOUBLE); } public static readonly instance = new DuckDBDoubleType(); + public static readonly Min = -Number.MAX_VALUE; + public static readonly Max = Number.MAX_VALUE; } export class DuckDBTimestampType extends BaseDuckDBType { @@ -124,6 +144,8 @@ export class DuckDBHugeIntType extends BaseDuckDBType { super(DuckDBTypeId.HUGEINT); } public static readonly instance = new DuckDBHugeIntType(); + public static readonly Max = 2n ** 127n - 1n; + public static readonly Min = -(2n ** 127n); } export class DuckDBUHugeIntType extends BaseDuckDBType { @@ -131,6 +153,8 @@ export class DuckDBUHugeIntType extends BaseDuckDBType { super(DuckDBTypeId.UHUGEINT); } public static readonly instance = new DuckDBUHugeIntType(); + public static readonly Max = 2n ** 128n - 1n; + public static readonly Min = 0n; } export class DuckDBVarCharType extends BaseDuckDBType { @@ -314,6 +338,8 @@ export class DuckDBVarIntType extends BaseDuckDBType { super(DuckDBTypeId.VARINT); } public static readonly instance = new DuckDBVarIntType(); + public static readonly Max: bigint = 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368n; + public static readonly Min: bigint = -179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368n; } export class DuckDBSQLNullType extends BaseDuckDBType { diff --git a/api/src/DuckDBVector.ts b/api/src/DuckDBVector.ts index c88622e9..9b952c39 100644 --- a/api/src/DuckDBVector.ts +++ b/api/src/DuckDBVector.ts @@ -193,22 +193,22 @@ const getBoolean = makeGetBoolean(); function getDecimal2(dataView: DataView, offset: number, type: DuckDBDecimalType): DuckDBDecimalValue { const scaledValue = getInt16(dataView, offset); - return new DuckDBDecimalValue(type, scaledValue); + return new DuckDBDecimalValue(type.width, type.scale, scaledValue); } function getDecimal4(dataView: DataView, offset: number, type: DuckDBDecimalType): DuckDBDecimalValue { const scaledValue = getInt32(dataView, offset); - return new DuckDBDecimalValue(type, scaledValue); + return new DuckDBDecimalValue(type.width, type.scale, scaledValue); } function getDecimal8(dataView: DataView, offset: number, type: DuckDBDecimalType): DuckDBDecimalValue { const scaledValue = getInt64(dataView, offset); - return new DuckDBDecimalValue(type, scaledValue); + return new DuckDBDecimalValue(type.width, type.scale, scaledValue); } function getDecimal16(dataView: DataView, offset: number, type: DuckDBDecimalType): DuckDBDecimalValue { const scaledValue = getInt128(dataView, offset); - return new DuckDBDecimalValue(type, scaledValue); + return new DuckDBDecimalValue(type.width, type.scale, scaledValue); } function vectorData(vector: duckdb.Vector, byteCount: number): Uint8Array { @@ -391,6 +391,13 @@ export abstract class DuckDBVector { public abstract get itemCount(): number; public abstract getItem(itemIndex: number): TValue | null; public abstract slice(offset: number, length: number): DuckDBVector; + public toArray(): (TValue | null)[] { + const items: (TValue | null)[] = []; + for (let i = 0; i < this.itemCount; i++) { + items.push(this.getItem(i)); + } + return items; + } } export class DuckDBBooleanVector extends DuckDBVector { @@ -1302,17 +1309,17 @@ export class DuckDBEnum4Vector extends DuckDBVector { } } -export class DuckDBListVector extends DuckDBVector { +export class DuckDBListVector extends DuckDBVector { private readonly listType: DuckDBListType; private readonly entryData: BigUint64Array; private readonly validity: DuckDBValidity; - private readonly childData: DuckDBVector; + private readonly childData: DuckDBVector; private readonly _itemCount: number; constructor( listType: DuckDBListType, entryData: BigUint64Array, validity: DuckDBValidity, - childData: DuckDBVector, + childData: DuckDBVector, itemCount: number, ) { super(); @@ -1340,7 +1347,7 @@ export class DuckDBListVector extends public override get itemCount(): number { return this._itemCount; } - public getItemVector(itemIndex: number): DuckDBVector | null { + public getItemVector(itemIndex: number): DuckDBVector | null { if (!this.validity.itemValid(itemIndex)) { return null; } @@ -1349,16 +1356,16 @@ export class DuckDBListVector extends const length = Number(this.entryData[entryDataStartIndex + 1]); return this.childData.slice(offset, length); } - public override getItem(itemIndex: number): DuckDBListValue | null { + public override getItem(itemIndex: number): DuckDBListValue | null { const vector = this.getItemVector(itemIndex); if (!vector) { return null; } - return new DuckDBListValue(this.listType, vector); + return new DuckDBListValue(vector.toArray()); } - public override slice(offset: number, length: number): DuckDBListVector { + public override slice(offset: number, length: number): DuckDBListVector { const entryDataStartIndex = offset * 2; - return new DuckDBListVector( + return new DuckDBListVector( this.listType, this.entryData.slice(entryDataStartIndex, entryDataStartIndex + length * 2), this.validity.slice(offset), @@ -1401,12 +1408,12 @@ export class DuckDBStructVector extends DuckDBVector { if (!this.validity.itemValid(itemIndex)) { return null; } - const values: DuckDBValue[] = []; + const entries: { [name: string]: DuckDBValue } = {}; const entryCount = this.structType.entries.length; for (let i = 0; i < entryCount; i++) { - values.push(this.entryVectors[i].getItem(itemIndex)); + entries[this.structType.entries[i].name] = this.entryVectors[i].getItem(itemIndex); } - return new DuckDBStructValue(this.structType, values); + return new DuckDBStructValue(entries); } public getItemValue(itemIndex: number, entryIndex: number): DuckDBValue | null { if (!this.validity.itemValid(itemIndex)) { @@ -1461,7 +1468,7 @@ export class DuckDBMapVector extends DuckDBVector { const value = itemVector.getItemValue(i, 1); entries.push({ key, value }); } - return new DuckDBMapValue(this.mapType, entries); + return new DuckDBMapValue(entries); } public override slice(offset: number, length: number): DuckDBMapVector { return new DuckDBMapVector( @@ -1471,15 +1478,15 @@ export class DuckDBMapVector extends DuckDBVector { } } -export class DuckDBArrayVector extends DuckDBVector> { +export class DuckDBArrayVector extends DuckDBVector { private readonly arrayType: DuckDBArrayType; private readonly validity: DuckDBValidity; - private readonly childData: DuckDBVector; + private readonly childData: DuckDBVector; private readonly _itemCount: number; constructor( arrayType: DuckDBArrayType, validity: DuckDBValidity, - childData: DuckDBVector, + childData: DuckDBVector, itemCount: number, ) { super(); @@ -1508,14 +1515,14 @@ export class DuckDBArrayVector extends public override get itemCount(): number { return this._itemCount; } - public override getItem(itemIndex: number): DuckDBArrayValue | null { + public override getItem(itemIndex: number): DuckDBArrayValue | null { if (!this.validity.itemValid(itemIndex)) { return null; } - return new DuckDBArrayValue(this.arrayType, this.childData.slice(itemIndex * this.arrayType.length, this.arrayType.length)); + return new DuckDBArrayValue(this.childData.slice(itemIndex * this.arrayType.length, this.arrayType.length).toArray()); } - public override slice(offset: number, length: number): DuckDBArrayVector { - return new DuckDBArrayVector( + public override slice(offset: number, length: number): DuckDBArrayVector { + return new DuckDBArrayVector( this.arrayType, this.validity.slice(offset), this.childData.slice(offset * this.arrayType.length, length * this.arrayType.length), diff --git a/api/src/conversion/dateTimeStringConversion.ts b/api/src/conversion/dateTimeStringConversion.ts new file mode 100644 index 00000000..00291a17 --- /dev/null +++ b/api/src/conversion/dateTimeStringConversion.ts @@ -0,0 +1,209 @@ +const DAYS_IN_400_YEARS = 146097; // (((365 * 4 + 1) * 25) - 1) * 4 + 1 +const MILLISECONDS_PER_DAY_NUM = 86400000; // 1000 * 60 * 60 * 24 + +const MICROSECONDS_PER_SECOND = BigInt(1000000); +const MICROSECONDS_PER_MILLISECOND = BigInt(1000); +const NANOSECONDS_PER_MICROSECOND = BigInt(1000); +const SECONDS_PER_MINUTE = BigInt(60); +const MINUTES_PER_HOUR = BigInt(60); +const MICROSECONDS_PER_DAY = BigInt(86400000000); // 24 * 60 * 60 * 1000000 + +const NEGATIVE_INFINITY_TIMESTAMP = BigInt('-9223372036854775807'); // -(2^63-1) +const POSITIVE_INFINITY_TIMESTAMP = BigInt('9223372036854775807'); // 2^63-1 + +export function getDuckDBDateStringFromYearMonthDay( + year: number, + month: number, + dayOfMonth: number, +): string { + const yearStr = String(Math.abs(year)).padStart(4, '0'); + const monthStr = String(month).padStart(2, '0'); + const dayOfMonthStr = String(dayOfMonth).padStart(2, '0'); + return `${yearStr}-${monthStr}-${dayOfMonthStr}${year < 0 ? ' (BC)' : ''}`; +} + +export function getDuckDBDateStringFromDays(days: number): string { + const absDays = Math.abs(days); + const sign = days < 0 ? -1 : 1; + // 400 years is the shortest interval with a fixed number of days. (Leap years and different length months can result + // in shorter intervals having different number of days.) By separating the number of 400 year intervals from the + // interval covered by the remaining days, we can guarantee that the date resulting from shifting the epoch by the + // remaining interval is within the valid range of the JS Date object. This allows us to use JS Date to calculate the + // year, month, and day of month for the date represented by the remaining interval, thus accounting for leap years + // and different length months. We can then safely add back the years from the 400 year intervals, because the month + // and day of month won't change when a date is shifted by a whole number of such intervals. + const num400YearIntervals = Math.floor(absDays / DAYS_IN_400_YEARS); + const yearsFrom400YearIntervals = sign * num400YearIntervals * 400; + const absDaysFromRemainingInterval = absDays % DAYS_IN_400_YEARS; + const millisecondsFromRemainingInterval = + sign * absDaysFromRemainingInterval * MILLISECONDS_PER_DAY_NUM; + const date = new Date(millisecondsFromRemainingInterval); + let year = yearsFrom400YearIntervals + date.getUTCFullYear(); + if (year < 0) { + year--; // correct for non-existence of year zero + } + const month = date.getUTCMonth() + 1; // getUTCMonth returns zero-indexed month, but we want a one-index month for display + const dayOfMonth = date.getUTCDate(); // getUTCDate returns one-indexed day-of-month + return getDuckDBDateStringFromYearMonthDay(year, month, dayOfMonth); +} + +export function getDuckDBTimeStringFromParts( + hoursPart: bigint, + minutesPart: bigint, + secondsPart: bigint, + microsecondsPart: bigint, +): string { + const hoursStr = String(hoursPart).padStart(2, '0'); + const minutesStr = String(minutesPart).padStart(2, '0'); + const secondsStr = String(secondsPart).padStart(2, '0'); + const microsecondsStr = String(microsecondsPart) + .padStart(6, '0') + .replace(/0+$/, ''); + return `${hoursStr}:${minutesStr}:${secondsStr}${ + microsecondsStr.length > 0 ? `.${microsecondsStr}` : '' + }`; +} + +export function getDuckDBTimeStringFromPositiveMicroseconds( + positiveMicroseconds: bigint, +): string { + const microsecondsPart = positiveMicroseconds % MICROSECONDS_PER_SECOND; + const seconds = positiveMicroseconds / MICROSECONDS_PER_SECOND; + const secondsPart = seconds % SECONDS_PER_MINUTE; + const minutes = seconds / SECONDS_PER_MINUTE; + const minutesPart = minutes % MINUTES_PER_HOUR; + const hoursPart = minutes / MINUTES_PER_HOUR; + return getDuckDBTimeStringFromParts( + hoursPart, + minutesPart, + secondsPart, + microsecondsPart, + ); +} + +export function getDuckDBTimeStringFromMicrosecondsInDay( + microsecondsInDay: bigint, +): string { + const positiveMicroseconds = + microsecondsInDay < 0 + ? microsecondsInDay + MICROSECONDS_PER_DAY + : microsecondsInDay; + return getDuckDBTimeStringFromPositiveMicroseconds(positiveMicroseconds); +} + +export function getDuckDBTimeStringFromMicroseconds( + microseconds: bigint, +): string { + const negative = microseconds < 0; + const positiveMicroseconds = negative ? -microseconds : microseconds; + const positiveString = + getDuckDBTimeStringFromPositiveMicroseconds(positiveMicroseconds); + return negative ? `-${positiveString}` : positiveString; +} + +export function getDuckDBTimestampStringFromDaysAndMicroseconds( + days: bigint, + microsecondsInDay: bigint, + timezone?: string | null, +): string { + // This conversion of BigInt to Number is safe, because the largest absolute value that `days` can has is 106751991, + // which fits without loss of precision in a JS Number. (106751991 = (2^63-1) / MICROSECONDS_PER_DAY) + const dateStr = getDuckDBDateStringFromDays(Number(days)); + const timeStr = getDuckDBTimeStringFromMicrosecondsInDay(microsecondsInDay); + const timezoneStr = timezone ? ` ${timezone}` : ''; + return `${dateStr} ${timeStr}${timezoneStr}`; +} + +export function getDuckDBTimestampStringFromMicroseconds( + microseconds: bigint, + timezone?: string | null, +): string { + // Note that -infinity and infinity are only representable in TIMESTAMP (and TIMESTAMPTZ), not the other timestamp + // variants. This is by-design and matches DuckDB. + if (microseconds === NEGATIVE_INFINITY_TIMESTAMP) { + return '-infinity'; + } + if (microseconds === POSITIVE_INFINITY_TIMESTAMP) { + return 'infinity'; + } + let days = microseconds / MICROSECONDS_PER_DAY; + let microsecondsPart = microseconds % MICROSECONDS_PER_DAY; + if (microsecondsPart < 0) { + days--; + microsecondsPart += MICROSECONDS_PER_DAY; + } + return getDuckDBTimestampStringFromDaysAndMicroseconds( + days, + microsecondsPart, + timezone, + ); +} + +export function getDuckDBTimestampStringFromSeconds( + seconds: bigint, + timezone?: string | null, +): string { + return getDuckDBTimestampStringFromMicroseconds( + seconds * MICROSECONDS_PER_SECOND, + timezone, + ); +} + +export function getDuckDBTimestampStringFromMilliseconds( + milliseconds: bigint, + timezone?: string | null, +): string { + return getDuckDBTimestampStringFromMicroseconds( + milliseconds * MICROSECONDS_PER_MILLISECOND, + timezone, + ); +} + +export function getDuckDBTimestampStringFromNanoseconds( + nanoseconds: bigint, + timezone?: string | null, +): string { + // Note that this division causes loss of precision. This matches the behavior of the DuckDB. It's important that this + // precision loss happen before the negative correction in getTimestampStringFromMicroseconds, otherwise off-by-one + // errors can occur. + return getDuckDBTimestampStringFromMicroseconds( + nanoseconds / NANOSECONDS_PER_MICROSECOND, + timezone, + ); +} + +// Assumes baseUnit can be pluralized by adding an 's'. +function numberAndUnit(value: number, baseUnit: string): string { + return `${value} ${baseUnit}${value !== 1 ? 's' : ''}`; +} + +export function getDuckDBIntervalString( + months: number, + days: number, + microseconds: bigint, +): string { + const parts: string[] = []; + if (months !== 0) { + const sign = months < 0 ? -1 : 1; + const absMonths = Math.abs(months); + const absYears = Math.floor(absMonths / 12); + const years = sign * absYears; + const extraMonths = sign * (absMonths - absYears * 12); + if (years !== 0) { + parts.push(numberAndUnit(years, 'year')); + } + if (extraMonths !== 0) { + parts.push(numberAndUnit(extraMonths, 'month')); + } + } + if (days !== 0) { + parts.push(numberAndUnit(days, 'day')); + } + if (microseconds !== BigInt(0)) { + parts.push(getDuckDBTimeStringFromMicroseconds(microseconds)); + } + if (parts.length > 0) { + return parts.join(' '); + } + return '00:00:00'; +} diff --git a/api/src/conversion/displayStringForDuckDBValue.ts b/api/src/conversion/displayStringForDuckDBValue.ts new file mode 100644 index 00000000..bf6beae1 --- /dev/null +++ b/api/src/conversion/displayStringForDuckDBValue.ts @@ -0,0 +1,12 @@ +import { quotedString } from '../sql'; +import { DuckDBValue } from '../values'; + +export function displayStringForDuckDBValue(value: DuckDBValue): string { + if (value == null) { + return 'NULL'; + } + if (typeof value === 'string') { + return quotedString(value); + } + return value.toString(); +} diff --git a/api/src/conversion/stringFromBlob.ts b/api/src/conversion/stringFromBlob.ts new file mode 100644 index 00000000..a0432424 --- /dev/null +++ b/api/src/conversion/stringFromBlob.ts @@ -0,0 +1,17 @@ +/** Matches BLOB-to-VARCHAR conversion behavior of DuckDB. */ +export function stringFromBlob(bytes: Uint8Array): string { + const byteStrings: string[] = []; + for (const byte of bytes) { + if ( + byte <= 0x1f || + byte === 0x22 /* single quote */ || + byte === 0x27 /* double quote */ || + byte >= 0x7f + ) { + byteStrings.push(`\\x${byte.toString(16).toUpperCase().padStart(2, '0')}`); + } else { + byteStrings.push(String.fromCharCode(byte)); + } + } + return byteStrings.join(''); +} diff --git a/api/src/conversion/stringFromDecimal.ts b/api/src/conversion/stringFromDecimal.ts new file mode 100644 index 00000000..d7162c32 --- /dev/null +++ b/api/src/conversion/stringFromDecimal.ts @@ -0,0 +1,129 @@ +/** + * Decimal string formatting. + * + * Supports a subset of the functionality of `BigInt.prototype.toLocaleString` for locale-specific formatting. + */ + +/* + * Locale formatting options for DuckDBDecimal. + * + * This is a subset of the options available for `BigInt.prototype.toLocaleString` + */ +export interface DuckDBDecimalFormatOptions { + useGrouping?: boolean; + minimumFractionDigits?: number; + maximumFractionDigits?: number; +} + +export interface LocaleOptions { + locales?: string | string[]; + options?: DuckDBDecimalFormatOptions; +} + +/* + * Get the decimal separator for a given locale. + * Somewhat expensive, so use getCachedDecimalSeparator if you need to call this multiple times. + */ + +function getDecimalSeparator(locales?: string | string[]): string { + const decimalSeparator = + new Intl.NumberFormat(locales, { useGrouping: false }) + .formatToParts(0.1) + .find((part) => part.type === 'decimal')?.value ?? '.'; + return decimalSeparator; +} + +/* + * Get the decimal separator for a given locale, and cache the result. + */ +const cachedDecimalSeparators: { [localeKey: string]: string } = {}; + +function getCachedDecimalSeparator(locales?: string | string[]): string { + const cacheKey = JSON.stringify(locales); + if (cacheKey in cachedDecimalSeparators) { + return cachedDecimalSeparators[cacheKey]; + } + const decimalSeparator = getDecimalSeparator(locales); + cachedDecimalSeparators[cacheKey] = decimalSeparator; + return decimalSeparator; +} + +// Helper function to format whole part of a decimal value. +// Note that we explicitly omit 'minimumFractionDigits' and 'maximumFractionDigits' from the options +// passed to toLocaleString, because they are only relevant for the fractional part of the number, and +// would result in formatting the whole part as a real number, which we don't want. +function formatWholePart( + localeOptions: LocaleOptions | undefined, + val: bigint, +): string { + if (localeOptions) { + const { + minimumFractionDigits: _minFD, + maximumFractionDigits: _maxFD, + ...restOptions + } = localeOptions.options ?? {}; + return val.toLocaleString(localeOptions?.locales, restOptions); + } + return String(val); +} + +// Format the fractional part of a decimal value +// Note that we must handle minimumFractionDigits and maximumFractionDigits ourselves, and that +// we don't apply `useGrouping` because that only applies to the whole part of the number. +function formatFractionalPart( + localeOptions: LocaleOptions | undefined, + val: bigint, + scale: number, +): string { + const fractionalPartStr = String(val).padStart(scale, '0'); + if (!localeOptions) { + return fractionalPartStr; + } + const minFracDigits = localeOptions?.options?.minimumFractionDigits ?? 0; + const maxFracDigits = localeOptions?.options?.maximumFractionDigits ?? 20; + + return fractionalPartStr.padEnd(minFracDigits, '0').slice(0, maxFracDigits); +} + +/** + * Convert a scaled decimal value to a string, possibly using locale-specific formatting. + */ +export function stringFromDecimal( + scaledValue: bigint, + scale: number, + localeOptions?: LocaleOptions, +): string { + // Decimal values are represented as integers that have been scaled up by a power of ten. The `scale` property of + // the type is the exponent of the scale factor. For a scale greater than zero, we need to separate out the + // fractional part by reversing this scaling. + if (scale > 0) { + const scaleFactor = BigInt(10) ** BigInt(scale); + const absScaledValue = scaledValue < 0 ? -scaledValue : scaledValue; + + const prefix = scaledValue < 0 ? '-' : ''; + + const wholePartNum = absScaledValue / scaleFactor; + const wholePartStr = formatWholePart(localeOptions, wholePartNum); + + const fractionalPartNum = absScaledValue % scaleFactor; + const fractionalPartStr = formatFractionalPart( + localeOptions, + fractionalPartNum, + scale, + ); + + const decimalSeparatorStr = localeOptions + ? getCachedDecimalSeparator(localeOptions.locales) + : '.'; + + return `${prefix}${wholePartStr}${decimalSeparatorStr}${fractionalPartStr}`; + } + // For a scale of zero, there is no fractional part, so a direct string conversion works. + if (localeOptions) { + return scaledValue.toLocaleString( + localeOptions?.locales, + localeOptions?.options as BigIntToLocaleStringOptions | undefined, + ); + } + return String(scaledValue); +} diff --git a/api/src/values/DuckDBArrayValue.ts b/api/src/values/DuckDBArrayValue.ts index 003cc7d5..ea2afb20 100644 --- a/api/src/values/DuckDBArrayValue.ts +++ b/api/src/values/DuckDBArrayValue.ts @@ -1,13 +1,18 @@ -import { DuckDBArrayType } from '../DuckDBType'; -import { DuckDBVector } from '../DuckDBVector'; +import { displayStringForDuckDBValue } from '../conversion/displayStringForDuckDBValue'; import { DuckDBValue } from './DuckDBValue'; -export class DuckDBArrayValue { - public readonly type: DuckDBArrayType; - public readonly vector: DuckDBVector; +export class DuckDBArrayValue { + public readonly items: readonly DuckDBValue[]; - public constructor(type: DuckDBArrayType, vector: DuckDBVector) { - this.type = type; - this.vector = vector; + public constructor(items: readonly DuckDBValue[]) { + this.items = items; } + + public toString(): string { + return `[${this.items.map(displayStringForDuckDBValue).join(', ')}]`; + } +} + +export function arrayValue(items: readonly DuckDBValue[]): DuckDBArrayValue { + return new DuckDBArrayValue(items); } diff --git a/api/src/values/DuckDBBitValue.ts b/api/src/values/DuckDBBitValue.ts index 02777465..40a8b823 100644 --- a/api/src/values/DuckDBBitValue.ts +++ b/api/src/values/DuckDBBitValue.ts @@ -1,5 +1,3 @@ -import { DuckDBBitType } from '../DuckDBType'; - export class DuckDBBitValue { public readonly data: Uint8Array; @@ -7,10 +5,6 @@ export class DuckDBBitValue { this.data = data; } - public get type(): DuckDBBitType { - return DuckDBBitType.instance; - } - private padding(): number { return this.data[0]; } @@ -20,8 +14,10 @@ export class DuckDBBitValue { } public getBool(index: number): boolean { - const dataIndex = Math.floor(index / 8) + 1; - return (this.data[dataIndex] & (1 << (index % 8))) !== 0; + const offset = index + this.padding(); + const dataIndex = Math.floor(offset / 8) + 1; + const byte = this.data[dataIndex] >> (7 - (offset % 8)); + return (byte & 1) !== 0; } public getBit(index: number): 0 | 1 { @@ -78,3 +74,7 @@ export class DuckDBBitValue { return new DuckDBBitValue(data); } } + +export function bitValue(str: string): DuckDBBitValue { + return DuckDBBitValue.fromString(str); +} diff --git a/api/src/values/DuckDBBlobValue.ts b/api/src/values/DuckDBBlobValue.ts index 9dcbc19e..9ab863ad 100644 --- a/api/src/values/DuckDBBlobValue.ts +++ b/api/src/values/DuckDBBlobValue.ts @@ -1,4 +1,6 @@ -import { DuckDBBlobType } from '../DuckDBType'; +import { stringFromBlob } from '../conversion/stringFromBlob'; + +const textEncoder = new TextEncoder(); export class DuckDBBlobValue { public readonly bytes: Uint8Array; @@ -7,7 +9,16 @@ export class DuckDBBlobValue { this.bytes = bytes; } - public get type(): DuckDBBlobType { - return DuckDBBlobType.instance; + /** Matches BLOB-to-VARCHAR conversion behavior of DuckDB. */ + public toString(): string { + return stringFromBlob(this.bytes); + } + + public static fromString(str: string): DuckDBBlobValue { + return new DuckDBBlobValue(Buffer.from(textEncoder.encode(str))); } } + +export function blobValue(bytes: Uint8Array): DuckDBBlobValue { + return new DuckDBBlobValue(bytes); +} diff --git a/api/src/values/DuckDBDateValue.ts b/api/src/values/DuckDBDateValue.ts index a4daac62..05e9b0ac 100644 --- a/api/src/values/DuckDBDateValue.ts +++ b/api/src/values/DuckDBDateValue.ts @@ -1,5 +1,5 @@ import { Date_ } from '@duckdb/node-bindings'; -import { DuckDBDateType } from '../DuckDBType'; +import { getDuckDBDateStringFromDays } from '../conversion/dateTimeStringConversion'; export class DuckDBDateValue implements Date_ { public readonly days: number; @@ -8,7 +8,19 @@ export class DuckDBDateValue implements Date_ { this.days = days; } - public get type(): DuckDBDateType { - return DuckDBDateType.instance; + public toString(): string { + return getDuckDBDateStringFromDays(this.days); } + + public static readonly Epoch = new DuckDBDateValue(0); + + public static readonly Max = new DuckDBDateValue(2 ** 31 - 2); + public static readonly Min = new DuckDBDateValue(-(2 ** 31 - 2)); + + public static readonly PosInf = new DuckDBDateValue(2 ** 31 - 1); + public static readonly NegInf = new DuckDBDateValue(-(2 ** 31 - 1)); +} + +export function dateValue(days: number): DuckDBDateValue { + return new DuckDBDateValue(days); } diff --git a/api/src/values/DuckDBDecimalValue.ts b/api/src/values/DuckDBDecimalValue.ts index 646b5ead..375864fb 100644 --- a/api/src/values/DuckDBDecimalValue.ts +++ b/api/src/values/DuckDBDecimalValue.ts @@ -1,24 +1,46 @@ import { Decimal } from '@duckdb/node-bindings'; -import { DuckDBDecimalType } from '../DuckDBType'; +import { stringFromDecimal } from '../conversion/stringFromDecimal'; export class DuckDBDecimalValue implements Decimal { - public readonly type: DuckDBDecimalType; + public readonly width: number; + public readonly scale: number; public readonly scaledValue: T; - public constructor(type: DuckDBDecimalType, scaledValue: T) { - this.type = type; + public constructor(width: number, scale: number, scaledValue: T) { + this.width = width; + this.scale = scale; this.scaledValue = scaledValue; } - get width(): number { - return this.type.width; + get value(): bigint { + return BigInt(this.scaledValue); } - get scale(): number { - return this.type.scale; + public toString(): string { + return stringFromDecimal(this.value, this.scale); } +} - get value(): bigint { - return BigInt(this.scaledValue); - } +export function decimalValue( + width: number, + scale: number, + scaledValue: T +): DuckDBDecimalValue { + return new DuckDBDecimalValue(width, scale, scaledValue); +} + +export function decimalNumber( + width: number, + scale: number, + scaledValue: number +): DuckDBDecimalValue { + return new DuckDBDecimalValue(width, scale, scaledValue); +} + +export function decimalBigint( + width: number, + scale: number, + scaledValue: bigint +): DuckDBDecimalValue { + return new DuckDBDecimalValue(width, scale, scaledValue); } diff --git a/api/src/values/DuckDBIntervalValue.ts b/api/src/values/DuckDBIntervalValue.ts index 907c873f..3a78aed1 100644 --- a/api/src/values/DuckDBIntervalValue.ts +++ b/api/src/values/DuckDBIntervalValue.ts @@ -1,5 +1,5 @@ import { Interval } from '@duckdb/node-bindings'; -import { DuckDBIntervalType } from '../DuckDBType'; +import { getDuckDBIntervalString } from '../conversion/dateTimeStringConversion'; export class DuckDBIntervalValue implements Interval { public readonly months: number; @@ -12,7 +12,11 @@ export class DuckDBIntervalValue implements Interval { this.micros = micros; } - public get type(): DuckDBIntervalType { - return DuckDBIntervalType.instance; + public toString(): string { + return getDuckDBIntervalString(this.months, this.days, this.micros); } } + +export function intervalValue(months: number, days: number, micros: bigint): DuckDBIntervalValue { + return new DuckDBIntervalValue(months, days, micros); +} diff --git a/api/src/values/DuckDBListValue.ts b/api/src/values/DuckDBListValue.ts index a8600d8d..678d03cd 100644 --- a/api/src/values/DuckDBListValue.ts +++ b/api/src/values/DuckDBListValue.ts @@ -1,13 +1,18 @@ -import { DuckDBListType } from '../DuckDBType'; -import { DuckDBVector } from '../DuckDBVector'; +import { displayStringForDuckDBValue } from '../conversion/displayStringForDuckDBValue'; import { DuckDBValue } from './DuckDBValue'; -export class DuckDBListValue { - public readonly type: DuckDBListType; - public readonly vector: DuckDBVector; +export class DuckDBListValue { + public readonly items: readonly DuckDBValue[]; - public constructor(type: DuckDBListType, vector: DuckDBVector) { - this.type = type; - this.vector = vector; + public constructor(items: readonly DuckDBValue[]) { + this.items = items; } + + public toString(): string { + return `[${this.items.map(displayStringForDuckDBValue).join(', ')}]`; + } +} + +export function listValue(items: readonly DuckDBValue[]): DuckDBListValue { + return new DuckDBListValue(items); } diff --git a/api/src/values/DuckDBMapValue.ts b/api/src/values/DuckDBMapValue.ts index f16aa22e..ab347662 100644 --- a/api/src/values/DuckDBMapValue.ts +++ b/api/src/values/DuckDBMapValue.ts @@ -1,23 +1,25 @@ -import { DuckDBMapType } from '../DuckDBType'; +import { displayStringForDuckDBValue } from '../conversion/displayStringForDuckDBValue'; import { DuckDBValue } from './DuckDBValue'; -export interface DuckDBMapEntry< - TKey extends DuckDBValue = DuckDBValue, - TValue extends DuckDBValue = DuckDBValue, -> { - key: TKey; - value: TValue; +export interface DuckDBMapEntry { + key: DuckDBValue; + value: DuckDBValue; } -export class DuckDBMapValue< - TKey extends DuckDBValue = DuckDBValue, - TValue extends DuckDBValue = DuckDBValue, -> { - public readonly type: DuckDBMapType; - public readonly entries: DuckDBMapEntry[]; +export class DuckDBMapValue { + public readonly entries: DuckDBMapEntry[]; - public constructor(type: DuckDBMapType, entries: DuckDBMapEntry[]) { - this.type = type; + public constructor(entries: DuckDBMapEntry[]) { this.entries = entries; } + + public toString(): string { + return `{${this.entries.map(({ key, value }) => + `${displayStringForDuckDBValue(key)}: ${displayStringForDuckDBValue(value)}` + ).join(', ')}}`; + } +} + +export function mapValue(entries: DuckDBMapEntry[]): DuckDBMapValue { + return new DuckDBMapValue(entries); } diff --git a/api/src/values/DuckDBStructValue.ts b/api/src/values/DuckDBStructValue.ts index 628fe320..0804a304 100644 --- a/api/src/values/DuckDBStructValue.ts +++ b/api/src/values/DuckDBStructValue.ts @@ -1,12 +1,26 @@ -import { DuckDBStructType } from '../DuckDBType'; +import { displayStringForDuckDBValue } from '../conversion/displayStringForDuckDBValue'; import { DuckDBValue } from './DuckDBValue'; +export interface DuckDBStructEntries { + readonly [name: string]: DuckDBValue; +} + export class DuckDBStructValue { - public readonly type: DuckDBStructType; - public readonly values: readonly DuckDBValue[]; + public readonly entries: DuckDBStructEntries; + + public constructor(entries: DuckDBStructEntries) { + this.entries = entries; + } - public constructor(type: DuckDBStructType, values: readonly DuckDBValue[]) { - this.type = type; - this.values = values; + public toString(): string { + const parts: string[] = []; + for (const name in this.entries) { + parts.push(`${displayStringForDuckDBValue(name)}: ${displayStringForDuckDBValue(this.entries[name])}`); + } + return `{${parts.join(', ')}}`; } } + +export function structValue(entries: DuckDBStructEntries): DuckDBStructValue { + return new DuckDBStructValue(entries); +} diff --git a/api/src/values/DuckDBTimeTZValue.ts b/api/src/values/DuckDBTimeTZValue.ts index f0ba1675..b8b88fca 100644 --- a/api/src/values/DuckDBTimeTZValue.ts +++ b/api/src/values/DuckDBTimeTZValue.ts @@ -1,5 +1,5 @@ import { TimeTZ } from '@duckdb/node-bindings'; -import { DuckDBTimeTZType } from '../DuckDBType'; +import { getDuckDBTimeStringFromMicrosecondsInDay } from '../conversion/dateTimeStringConversion'; export class DuckDBTimeTZValue implements TimeTZ { public readonly bits: bigint; @@ -16,23 +16,34 @@ export class DuckDBTimeTZValue implements TimeTZ { this.offset = offset; } - public get type(): DuckDBTimeTZType { - return DuckDBTimeTZType.instance; + public toString(): string { + // TODO: display offset + return getDuckDBTimeStringFromMicrosecondsInDay(BigInt(this.microseconds)); } - public static TIME_BITS = 40; - public static OFFSET_BITS = 24; - public static MAX_OFFSET = 16 * 60 * 60 - 1; // ±15:59:59 = 57599 seconds + public static TimeBits = 40; + public static OffsetBits = 24; + public static MaxOffset = 16 * 60 * 60 - 1; // ±15:59:59 = 57599 seconds + public static MinOffset = -DuckDBTimeTZValue.MaxOffset; + public static MaxMicroseconds = 24 * 60 * 60 * 1000 * 1000; // 86400000000 + public static MinMicroseconds = 0; public static fromBits(bits: bigint): DuckDBTimeTZValue { - const microseconds = Number(BigInt.asUintN(DuckDBTimeTZValue.TIME_BITS, bits >> BigInt(DuckDBTimeTZValue.OFFSET_BITS))); - const offset = DuckDBTimeTZValue.MAX_OFFSET - Number(BigInt.asUintN(DuckDBTimeTZValue.OFFSET_BITS, bits)); + const microseconds = Number(BigInt.asUintN(DuckDBTimeTZValue.TimeBits, bits >> BigInt(DuckDBTimeTZValue.OffsetBits))); + const offset = DuckDBTimeTZValue.MaxOffset - Number(BigInt.asUintN(DuckDBTimeTZValue.OffsetBits, bits)); return new DuckDBTimeTZValue(bits, microseconds, offset); } public static fromParts(microseconds: number, offset: number): DuckDBTimeTZValue { - const bits = BigInt.asUintN(DuckDBTimeTZValue.TIME_BITS, BigInt(microseconds)) << BigInt(DuckDBTimeTZValue.OFFSET_BITS) - | BigInt.asUintN(DuckDBTimeTZValue.OFFSET_BITS, BigInt(DuckDBTimeTZValue.MAX_OFFSET - offset)); + const bits = BigInt.asUintN(DuckDBTimeTZValue.TimeBits, BigInt(microseconds)) << BigInt(DuckDBTimeTZValue.OffsetBits) + | BigInt.asUintN(DuckDBTimeTZValue.OffsetBits, BigInt(DuckDBTimeTZValue.MaxOffset - offset)); return new DuckDBTimeTZValue(bits, microseconds, offset); } + + public static readonly Max = DuckDBTimeTZValue.fromParts(DuckDBTimeTZValue.MaxMicroseconds, DuckDBTimeTZValue.MinOffset); + public static readonly Min = DuckDBTimeTZValue.fromParts(DuckDBTimeTZValue.MinMicroseconds, DuckDBTimeTZValue.MaxOffset); +} + +export function timeTZValue(microseconds: number, offset: number): DuckDBTimeTZValue { + return DuckDBTimeTZValue.fromParts(microseconds, offset); } diff --git a/api/src/values/DuckDBTimeValue.ts b/api/src/values/DuckDBTimeValue.ts index 29b401a6..c3f818d0 100644 --- a/api/src/values/DuckDBTimeValue.ts +++ b/api/src/values/DuckDBTimeValue.ts @@ -1,5 +1,5 @@ import { Time } from '@duckdb/node-bindings'; -import { DuckDBTimeType } from '../DuckDBType'; +import { getDuckDBTimeStringFromMicrosecondsInDay } from '../conversion/dateTimeStringConversion'; export class DuckDBTimeValue implements Time { public readonly micros: bigint; @@ -8,7 +8,14 @@ export class DuckDBTimeValue implements Time { this.micros = micros; } - public get type(): DuckDBTimeType { - return DuckDBTimeType.instance; + public toString(): string { + return getDuckDBTimeStringFromMicrosecondsInDay(this.micros); } + + public static readonly Max = new DuckDBTimeValue(24n * 60n * 60n * 1000n * 1000n); + public static readonly Min = new DuckDBTimeValue(0n); +} + +export function timeValue(micros: bigint): DuckDBTimeValue { + return new DuckDBTimeValue(micros); } diff --git a/api/src/values/DuckDBTimestampMillisecondsValue.ts b/api/src/values/DuckDBTimestampMillisecondsValue.ts index 4b11d478..b86159be 100644 --- a/api/src/values/DuckDBTimestampMillisecondsValue.ts +++ b/api/src/values/DuckDBTimestampMillisecondsValue.ts @@ -1,4 +1,5 @@ -import { DuckDBTimestampMillisecondsType } from '../DuckDBType'; +import { getDuckDBTimestampStringFromMilliseconds } from '../conversion/dateTimeStringConversion'; +import { DuckDBTimestampSecondsValue } from './DuckDBTimestampSecondsValue'; export class DuckDBTimestampMillisecondsValue { public readonly milliseconds: bigint; @@ -7,7 +8,15 @@ export class DuckDBTimestampMillisecondsValue { this.milliseconds = milliseconds; } - public get type(): DuckDBTimestampMillisecondsType { - return DuckDBTimestampMillisecondsType.instance; + public toString(): string { + return getDuckDBTimestampStringFromMilliseconds(this.milliseconds); } + + public static readonly Epoch = new DuckDBTimestampMillisecondsValue(0n); + public static readonly Max = new DuckDBTimestampMillisecondsValue((2n ** 63n - 2n) / 1000n); + public static readonly Min = new DuckDBTimestampMillisecondsValue(DuckDBTimestampSecondsValue.Min.seconds * 1000n); +} + +export function timestampMillisValue(milliseconds: bigint): DuckDBTimestampMillisecondsValue { + return new DuckDBTimestampMillisecondsValue(milliseconds); } diff --git a/api/src/values/DuckDBTimestampNanosecondsValue.ts b/api/src/values/DuckDBTimestampNanosecondsValue.ts index 1455485f..6e68aaf5 100644 --- a/api/src/values/DuckDBTimestampNanosecondsValue.ts +++ b/api/src/values/DuckDBTimestampNanosecondsValue.ts @@ -1,4 +1,4 @@ -import { DuckDBTimestampNanosecondsType } from '../DuckDBType'; +import { getDuckDBTimestampStringFromNanoseconds } from '../conversion/dateTimeStringConversion'; export class DuckDBTimestampNanosecondsValue { public readonly nanoseconds: bigint; @@ -7,7 +7,15 @@ export class DuckDBTimestampNanosecondsValue { this.nanoseconds = nanoseconds; } - public get type(): DuckDBTimestampNanosecondsType { - return DuckDBTimestampNanosecondsType.instance; + public toString(): string { + return getDuckDBTimestampStringFromNanoseconds(this.nanoseconds); } + + public static readonly Epoch = new DuckDBTimestampNanosecondsValue(0n); + public static readonly Max = new DuckDBTimestampNanosecondsValue(2n ** 63n - 2n); + public static readonly Min = new DuckDBTimestampNanosecondsValue(-9223286400000000000n); +} + +export function timestampNanosValue(nanoseconds: bigint): DuckDBTimestampNanosecondsValue { + return new DuckDBTimestampNanosecondsValue(nanoseconds); } diff --git a/api/src/values/DuckDBTimestampSecondsValue.ts b/api/src/values/DuckDBTimestampSecondsValue.ts index b43e4766..410c6617 100644 --- a/api/src/values/DuckDBTimestampSecondsValue.ts +++ b/api/src/values/DuckDBTimestampSecondsValue.ts @@ -1,4 +1,4 @@ -import { DuckDBTimestampSecondsType } from '../DuckDBType'; +import { getDuckDBTimestampStringFromSeconds } from '../conversion/dateTimeStringConversion'; export class DuckDBTimestampSecondsValue { public readonly seconds: bigint; @@ -7,7 +7,15 @@ export class DuckDBTimestampSecondsValue { this.seconds = seconds; } - public get type(): DuckDBTimestampSecondsType { - return DuckDBTimestampSecondsType.instance; + public toString(): string { + return getDuckDBTimestampStringFromSeconds(this.seconds); } + + public static readonly Epoch = new DuckDBTimestampSecondsValue(0n); + public static readonly Max = new DuckDBTimestampSecondsValue( 9223372036854n); + public static readonly Min = new DuckDBTimestampSecondsValue(-9223372022400n); // from test_all_types() select epoch(timestamp_s)::bigint; +} + +export function timestampSecondsValue(seconds: bigint): DuckDBTimestampSecondsValue { + return new DuckDBTimestampSecondsValue(seconds); } diff --git a/api/src/values/DuckDBTimestampTZValue.ts b/api/src/values/DuckDBTimestampTZValue.ts index e54a1dba..bd1d9dcc 100644 --- a/api/src/values/DuckDBTimestampTZValue.ts +++ b/api/src/values/DuckDBTimestampTZValue.ts @@ -1,4 +1,5 @@ -import { DuckDBTimestampTZType } from '../DuckDBType'; +import { getDuckDBTimestampStringFromMicroseconds } from '../conversion/dateTimeStringConversion'; +import { DuckDBTimestampValue } from './DuckDBTimestampValue'; export class DuckDBTimestampTZValue { public readonly micros: bigint; @@ -7,7 +8,18 @@ export class DuckDBTimestampTZValue { this.micros = micros; } - public get type(): DuckDBTimestampTZType { - return DuckDBTimestampTZType.instance; + public toString(): string { + // TODO: adjust micros for local timezone offset, and pass in timezone string + return getDuckDBTimestampStringFromMicroseconds(this.micros); } + + public static readonly Epoch = new DuckDBTimestampTZValue(0n); + public static readonly Max = new DuckDBTimestampTZValue(DuckDBTimestampValue.Max.micros); + public static readonly Min = new DuckDBTimestampTZValue(DuckDBTimestampValue.Min.micros); + public static readonly PosInf = new DuckDBTimestampTZValue(DuckDBTimestampValue.PosInf.micros); + public static readonly NegInf = new DuckDBTimestampTZValue(DuckDBTimestampValue.NegInf.micros); +} + +export function timestampTZValue(micros: bigint): DuckDBTimestampTZValue { + return new DuckDBTimestampTZValue(micros); } diff --git a/api/src/values/DuckDBTimestampValue.ts b/api/src/values/DuckDBTimestampValue.ts index 66d69454..895303cd 100644 --- a/api/src/values/DuckDBTimestampValue.ts +++ b/api/src/values/DuckDBTimestampValue.ts @@ -1,5 +1,6 @@ import { Timestamp } from '@duckdb/node-bindings'; -import { DuckDBTimestampType } from '../DuckDBType'; +import { DuckDBTimestampMillisecondsValue } from './DuckDBTimestampMillisecondsValue'; +import { getDuckDBTimestampStringFromMicroseconds } from '../conversion/dateTimeStringConversion'; export class DuckDBTimestampValue implements Timestamp { public readonly micros: bigint; @@ -8,10 +9,20 @@ export class DuckDBTimestampValue implements Timestamp { this.micros = micros; } - public get type(): DuckDBTimestampType { - return DuckDBTimestampType.instance; + public toString(): string { + return getDuckDBTimestampStringFromMicroseconds(this.micros); } + + public static readonly Epoch = new DuckDBTimestampValue(0n); + public static readonly Max = new DuckDBTimestampValue(2n ** 63n - 2n); + public static readonly Min = new DuckDBTimestampValue(DuckDBTimestampMillisecondsValue.Min.milliseconds * 1000n); + public static readonly PosInf = new DuckDBTimestampValue(2n ** 63n - 1n); + public static readonly NegInf = new DuckDBTimestampValue(-(2n ** 63n - 1n)); } export type DuckDBTimestampMicrosecondsValue = DuckDBTimestampValue; export const DuckDBTimestampMicrosecondsValue = DuckDBTimestampValue; + +export function timestampValue(micros: bigint): DuckDBTimestampValue { + return new DuckDBTimestampValue(micros); +} diff --git a/api/src/values/DuckDBUUIDValue.ts b/api/src/values/DuckDBUUIDValue.ts index 2e91db19..ec4cae46 100644 --- a/api/src/values/DuckDBUUIDValue.ts +++ b/api/src/values/DuckDBUUIDValue.ts @@ -1,5 +1,3 @@ -import { DuckDBUUIDType } from '../DuckDBType'; - export class DuckDBUUIDValue { public readonly hugeint: bigint; @@ -7,7 +5,17 @@ export class DuckDBUUIDValue { this.hugeint = hugeint; } - public get type(): DuckDBUUIDType { - return DuckDBUUIDType.instance; + public toString(): string { + // Truncate to 32 hex digits, then prepend with a hex 1, before converting to a hex string. + // This ensures the trailing 32 characters are the hex digits we want, left padded with zeros as needed. + const hex = ((this.hugeint & 0xffffffffffffffffffffffffffffffffn) | 0x100000000000000000000000000000000n).toString(16); + return `${hex.substring(1, 9)}-${hex.substring(9, 13)}-${hex.substring(13, 17)}-${hex.substring(17, 21)}-${hex.substring(21, 33)}`; } + + public static readonly Max = new DuckDBUUIDValue(2n ** 127n - 1n); + public static readonly Min = new DuckDBUUIDValue(-(2n ** 127n)); +} + +export function uuidValue(hugeint: bigint): DuckDBUUIDValue { + return new DuckDBUUIDValue(hugeint); } diff --git a/api/src/values/DuckDBUnionValue.ts b/api/src/values/DuckDBUnionValue.ts index b7b9f09a..7fbb5ce8 100644 --- a/api/src/values/DuckDBUnionValue.ts +++ b/api/src/values/DuckDBUnionValue.ts @@ -1,11 +1,22 @@ import { DuckDBValue } from './DuckDBValue'; -export class DuckDBUnionValue { +export class DuckDBUnionValue { public readonly tag: string; - public readonly value: TValue; + public readonly value: DuckDBValue; - public constructor(tag: string, value: TValue) { + public constructor(tag: string, value: DuckDBValue) { this.tag = tag; this.value = value; } + + public toString(): string { + if (this.value == null) { + return 'NULL'; + } + return this.value.toString(); + } +} + +export function unionValue(tag: string, value: DuckDBValue): DuckDBUnionValue { + return new DuckDBUnionValue(tag, value); } diff --git a/api/test/api.test.ts b/api/test/api.test.ts index 0741bacd..0684efd7 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -2,12 +2,10 @@ import { assert, describe, test } from 'vitest'; import { DuckDBAnyType, DuckDBArrayType, - DuckDBArrayValue, DuckDBArrayVector, DuckDBBigIntType, DuckDBBigIntVector, DuckDBBitType, - DuckDBBitValue, DuckDBBitVector, DuckDBBlobType, DuckDBBlobValue, @@ -24,7 +22,6 @@ import { DuckDBDecimal4Vector, DuckDBDecimal8Vector, DuckDBDecimalType, - DuckDBDecimalValue, DuckDBDoubleType, DuckDBDoubleVector, DuckDBEnum1Vector, @@ -39,13 +36,10 @@ import { DuckDBIntegerType, DuckDBIntegerVector, DuckDBIntervalType, - DuckDBIntervalValue, DuckDBIntervalVector, DuckDBListType, - DuckDBListValue, DuckDBListVector, DuckDBMapType, - DuckDBMapValue, DuckDBMapVector, DuckDBPendingResultState, DuckDBResult, @@ -53,7 +47,6 @@ import { DuckDBSmallIntType, DuckDBSmallIntVector, DuckDBStructType, - DuckDBStructValue, DuckDBStructVector, DuckDBTimeTZType, DuckDBTimeTZValue, @@ -94,7 +87,6 @@ import { DuckDBUUIDValue, DuckDBUUIDVector, DuckDBUnionType, - DuckDBUnionValue, DuckDBUnionVector, DuckDBValue, DuckDBVarCharType, @@ -104,82 +96,26 @@ import { DuckDBVector, ResultReturnType, StatementType, + arrayValue, + bitValue, configurationOptionDescriptions, + dateValue, + decimalBigint, + decimalNumber, + intervalValue, + listValue, + mapValue, + structValue, + timestampTZValue, + timestampValue, + unionValue, version } from '../src'; -const N_2_7 = 2 ** 7; -const N_2_8 = 2 ** 8; -const N_2_15 = 2 ** 15; -const N_2_16 = 2 ** 16; -const N_2_31 = 2 ** 31; -const N_2_32 = 2 ** 32; - -const BI_0 = BigInt(0); -const BI_1 = BigInt(1); -// const BI_2 = BigInt(2); -const BI_24 = BigInt(24); -const BI_60 = BigInt(60); -const BI_1000 = BigInt(1000); -const BI_2_63 = BI_1 << BigInt(63); -const BI_2_64 = BI_1 << BigInt(64); -const BI_2_127 = BI_1 << BigInt(127); -const BI_2_128 = BI_1 << BigInt(128); -const BI_10_8 = BigInt(100000000); -const BI_10_10 = BigInt(10000000000); -const BI_18_9s = BI_10_8 * BI_10_10 - BI_1; -const BI_38_9s = BI_10_8 * BI_10_10 * BI_10_10 * BI_10_10 - BI_1; - -const MinInt8 = -N_2_7; -const MaxInt8 = N_2_7 - 1; -const MinUInt8 = 0; -const MaxUInt8 = N_2_8 - 1; -const MinInt16 = -N_2_15; -const MaxInt16 = N_2_15 - 1; -const MinUInt16 = 0; -const MaxUInt16 = N_2_16 - 1; -const MinInt32 = -N_2_31; -const MaxInt32 = N_2_31 - 1; -const MinUInt32 = 0; -const MaxUInt32 = N_2_32 - 1; -const MinInt64 = -BI_2_63; -const MaxInt64 = BI_2_63 - BI_1; -const MinUInt64 = BI_0; -const MaxUInt64 = BI_2_64 - BI_1; -const MinInt128 = -BI_2_127; -const MaxInt128 = BI_2_127 - BI_1; -const MaxUInt128 = BI_2_128 - BI_1; -const MinHugeInt = MinInt128; -const MinUHugeInt = BI_0; -const MinDate = new DuckDBDateValue(MinInt32 + 2); -const MaxDate = new DuckDBDateValue(MaxInt32 - 1); -const DatePosInf = new DuckDBDateValue(MaxInt32); -const DateNegInf = new DuckDBDateValue(-MaxInt32); -const MinTime = new DuckDBTimeValue(BI_0); -const MaxTime = new DuckDBTimeValue(BI_24 * BI_60 * BI_60 * BI_1000 * BI_1000); // 86400000000 -const MinTimeTZMicroseconds = 0; -const MaxTimeTZMicroseconds = 24 * 60 * 60 * 1000 * 1000; // 86400000000 -const MaxTimeTZOffset = 16 * 60 * 60 - 1; // from dtime_tz_t (MAX_OFFSET) -const MinTimeTZOffset = -MaxTimeTZOffset; -const MinTimeTZ = DuckDBTimeTZValue.fromParts(MinTimeTZMicroseconds, MaxTimeTZOffset); -const MaxTimeTZ = DuckDBTimeTZValue.fromParts(MaxTimeTZMicroseconds, MinTimeTZOffset); -const MinTS_S = BigInt(-9223372022400); // from test_all_types() select epoch(timestamp_s)::bigint; -const MaxTS_S = BigInt( 9223372036854); -const MinTS_MS = MinTS_S * BI_1000; -const MaxTS_MS = (MaxInt64 - BI_1) / BI_1000; -const MinTS_US = MinTS_MS * BI_1000; -const MaxTS_US = MaxInt64 - BI_1; -const TS_US_Inf = MaxInt64; -const MinTS_NS = -9223286400000000000n; -const MaxTS_NS = MaxInt64 - BI_1; -const MinFloat32 = Math.fround(-3.4028235e+38); -const MaxFloat32 = Math.fround( 3.4028235e+38); -const MinFloat64 = -Number.MAX_VALUE; -const MaxFloat64 = Number.MAX_VALUE; -const MinUUID = new DuckDBUUIDValue(MinInt128); -const MaxUUID = new DuckDBUUIDValue(MaxInt128); -const MinVarInt: bigint = -179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368n -const MaxVarInt: bigint = 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368n; +const BI_10_8 = 100000000n; +const BI_10_10 = 10000000000n; +const BI_18_9s = BI_10_8 * BI_10_10 - 1n; +const BI_38_9s = BI_10_8 * BI_10_10 * BI_10_10 * BI_10_10 - 1n; async function sleep(ms: number): Promise { return new Promise((resolve) => { @@ -253,21 +189,6 @@ function assertVectorValues( } } -function assertNestedVectorValues( - vector: DuckDBVector | null | undefined, - checkVectorValueFns: ((value: TValue | null, valueName: string) => void)[], - vectorName: string, -) { - if (!vector) { - assert.fail(`${vectorName} unexpectedly null or undefined`); - } - assert.strictEqual(vector.itemCount, checkVectorValueFns.length, - `expected vector ${vectorName} item count to be ${checkVectorValueFns.length} but found ${vector.itemCount}`); - for (let i = 0; i < vector.itemCount; i++) { - checkVectorValueFns[i](vector.getItem(i), `${vectorName}[${i}]`) - } -} - function assertValues>( chunk: DuckDBDataChunk, columnIndex: number, @@ -278,21 +199,6 @@ function assertValues>( - chunk: DuckDBDataChunk, - columnIndex: number, - vectorType: new (...args: any[]) => TVector, - checkVectorValueFns: ((value: TValue | null, valueName: string) => void)[], -) { - const vector = getColumnVector(chunk, columnIndex, vectorType); - assertNestedVectorValues(vector, checkVectorValueFns, `col${columnIndex}`); -} - -const textEncoder = new TextEncoder(); -function blobFromString(str: string): DuckDBBlobValue { - return new DuckDBBlobValue(Buffer.from(textEncoder.encode(str))); -} - function bigints(start: bigint, end: bigint) { return Array.from({ length: Number(end - start) + 1 }).map((_, i) => start + BigInt(i)); } @@ -510,7 +416,7 @@ describe('api', () => { try { assert.strictEqual(chunk.columnCount, 1); assert.strictEqual(chunk.rowCount, 3); - assertValues(chunk, 0, DuckDBIntegerVector, [MinInt32, MaxInt32, null]); + assertValues(chunk, 0, DuckDBIntegerVector, [DuckDBIntegerType.Min, DuckDBIntegerType.Max, null]); } finally { chunk.dispose(); } @@ -547,11 +453,11 @@ describe('api', () => { currentChunk.dispose(); // this is the empty chunk that signifies the end of the stream currentChunk = null; assert.strictEqual(chunks.length, 5); // ceil(10000 / 2048) = 5 - assertValues(chunks[0], 0, DuckDBBigIntVector, bigints(BigInt(0), BigInt(2048-1))); - assertValues(chunks[1], 0, DuckDBBigIntVector, bigints(BigInt(2048), BigInt(2048*2-1))); - assertValues(chunks[2], 0, DuckDBBigIntVector, bigints(BigInt(2048*2), BigInt(2048*3-1))); - assertValues(chunks[3], 0, DuckDBBigIntVector, bigints(BigInt(2048*3), BigInt(2048*4-1))); - assertValues(chunks[4], 0, DuckDBBigIntVector, bigints(BigInt(2048*4), BigInt(9999))); + assertValues(chunks[0], 0, DuckDBBigIntVector, bigints(0n, 2048n - 1n)); + assertValues(chunks[1], 0, DuckDBBigIntVector, bigints(2048n, 2048n * 2n - 1n)); + assertValues(chunks[2], 0, DuckDBBigIntVector, bigints(2048n * 2n, 2048n * 3n - 1n)); + assertValues(chunks[3], 0, DuckDBBigIntVector, bigints(2048n * 3n, 2048n * 4n - 1n)); + assertValues(chunks[4], 0, DuckDBBigIntVector, bigints(2048n * 4n, 9999n)); } finally { if (currentChunk) { currentChunk.dispose(); @@ -578,19 +484,6 @@ describe('api', () => { const smallEnumValues = ['DUCK_DUCK_ENUM', 'GOOSE']; const mediumEnumValues = Array.from({ length: 300 }).map((_, i) => `enum_${i}`); const largeEnumValues = Array.from({ length: 70000 }).map((_, i) => `enum_${i}`); - const structType = new DuckDBStructType([ - { name: 'a', valueType: DuckDBIntegerType.instance }, - { name: 'b', valueType: DuckDBVarCharType.instance }, - ]); - const structOfArraysType = new DuckDBStructType([ - { name: 'a', valueType: new DuckDBListType(DuckDBIntegerType.instance) }, - { name: 'b', valueType: new DuckDBListType(DuckDBVarCharType.instance) }, - ]); - const structOfFixedArrayType = new DuckDBStructType([ - { name: 'a', valueType: new DuckDBArrayType(DuckDBIntegerType.instance, 3) }, - { name: 'b', valueType: new DuckDBArrayType(DuckDBVarCharType.instance, 3) }, - ]); - const mapType = new DuckDBMapType(DuckDBVarCharType.instance, DuckDBVarCharType.instance); assertColumns(result, [ { name: 'bool', type: DuckDBBooleanType.instance }, { name: 'tinyint', type: DuckDBTinyIntType.instance }, @@ -633,13 +526,19 @@ describe('api', () => { { name: 'timestamptz_array', type: new DuckDBListType(DuckDBTimestampTZType.instance) }, { name: 'varchar_array', type: new DuckDBListType(DuckDBVarCharType.instance) }, { name: 'nested_int_array', type: new DuckDBListType(new DuckDBListType(DuckDBIntegerType.instance)) }, - { name: 'struct', type: structType }, - { name: 'struct_of_arrays', type: structOfArraysType}, + { name: 'struct', type: new DuckDBStructType([ + { name: 'a', valueType: DuckDBIntegerType.instance }, + { name: 'b', valueType: DuckDBVarCharType.instance }, + ]) }, + { name: 'struct_of_arrays', type: new DuckDBStructType([ + { name: 'a', valueType: new DuckDBListType(DuckDBIntegerType.instance) }, + { name: 'b', valueType: new DuckDBListType(DuckDBVarCharType.instance) }, + ])}, { name: 'array_of_structs', type: new DuckDBListType(new DuckDBStructType([ { name: 'a', valueType: DuckDBIntegerType.instance }, { name: 'b', valueType: DuckDBVarCharType.instance }, ]))}, - { name: 'map', type: mapType }, + { name: 'map', type: new DuckDBMapType(DuckDBVarCharType.instance, DuckDBVarCharType.instance) }, { name: 'union', type: new DuckDBUnionType([ { tag: 'name', valueType: DuckDBVarCharType.instance }, { tag: 'age', valueType: DuckDBSmallIntType.instance }, @@ -652,7 +551,10 @@ describe('api', () => { { name: 'a', valueType: DuckDBIntegerType.instance }, { name: 'b', valueType: DuckDBVarCharType.instance }, ]), 3) }, - { name: 'struct_of_fixed_array', type: structOfFixedArrayType }, + { name: 'struct_of_fixed_array', type: new DuckDBStructType([ + { name: 'a', valueType: new DuckDBArrayType(DuckDBIntegerType.instance, 3) }, + { name: 'b', valueType: new DuckDBArrayType(DuckDBVarCharType.instance, 3) }, + ]) }, { name: 'fixed_array_of_int_list', type: new DuckDBArrayType(new DuckDBListType(DuckDBIntegerType.instance), 3) }, { name: 'list_of_fixed_int_array', type: new DuckDBListType(new DuckDBArrayType(DuckDBIntegerType.instance, 3)) }, ]); @@ -663,67 +565,67 @@ describe('api', () => { assert.strictEqual(chunk.rowCount, 3); assertValues(chunk, 0, DuckDBBooleanVector, [false, true, null]); - assertValues(chunk, 1, DuckDBTinyIntVector, [MinInt8, MaxInt8, null]); - assertValues(chunk, 2, DuckDBSmallIntVector, [MinInt16, MaxInt16, null]); - assertValues(chunk, 3, DuckDBIntegerVector, [MinInt32, MaxInt32, null]); - assertValues(chunk, 4, DuckDBBigIntVector, [MinInt64, MaxInt64, null]); - assertValues(chunk, 5, DuckDBHugeIntVector, [MinHugeInt, MaxInt128, null]); - assertValues(chunk, 6, DuckDBUHugeIntVector, [MinUHugeInt, MaxUInt128, null]); - assertValues(chunk, 7, DuckDBUTinyIntVector, [MinUInt8, MaxUInt8, null]); - assertValues(chunk, 8, DuckDBUSmallIntVector, [MinUInt16, MaxUInt16, null]); - assertValues(chunk, 9, DuckDBUIntegerVector, [MinUInt32, MaxUInt32, null]); - assertValues(chunk, 10, DuckDBUBigIntVector, [MinUInt64, MaxUInt64, null]); - assertValues(chunk, 11, DuckDBVarIntVector, [MinVarInt, MaxVarInt, null]); - assertValues(chunk, 12, DuckDBDateVector, [MinDate, MaxDate, null]); - assertValues(chunk, 13, DuckDBTimeVector, [MinTime, MaxTime, null]); + assertValues(chunk, 1, DuckDBTinyIntVector, [DuckDBTinyIntType.Min, DuckDBTinyIntType.Max, null]); + assertValues(chunk, 2, DuckDBSmallIntVector, [DuckDBSmallIntType.Min, DuckDBSmallIntType.Max, null]); + assertValues(chunk, 3, DuckDBIntegerVector, [DuckDBIntegerType.Min, DuckDBIntegerType.Max, null]); + assertValues(chunk, 4, DuckDBBigIntVector, [DuckDBBigIntType.Min, DuckDBBigIntType.Max, null]); + assertValues(chunk, 5, DuckDBHugeIntVector, [DuckDBHugeIntType.Min, DuckDBHugeIntType.Max, null]); + assertValues(chunk, 6, DuckDBUHugeIntVector, [DuckDBUHugeIntType.Min, DuckDBUHugeIntType.Max, null]); + assertValues(chunk, 7, DuckDBUTinyIntVector, [DuckDBUTinyIntType.Min, DuckDBUTinyIntType.Max, null]); + assertValues(chunk, 8, DuckDBUSmallIntVector, [DuckDBUSmallIntType.Min, DuckDBUSmallIntType.Max, null]); + assertValues(chunk, 9, DuckDBUIntegerVector, [DuckDBUIntegerType.Min, DuckDBUIntegerType.Max, null]); + assertValues(chunk, 10, DuckDBUBigIntVector, [DuckDBUBigIntType.Min, DuckDBUBigIntType.Max, null]); + assertValues(chunk, 11, DuckDBVarIntVector, [DuckDBVarIntType.Min, DuckDBVarIntType.Max, null]); + assertValues(chunk, 12, DuckDBDateVector, [DuckDBDateValue.Min, DuckDBDateValue.Max, null]); + assertValues(chunk, 13, DuckDBTimeVector, [DuckDBTimeValue.Min, DuckDBTimeValue.Max, null]); assertValues(chunk, 14, DuckDBTimestampVector, - [new DuckDBTimestampValue(MinTS_US), new DuckDBTimestampValue(MaxTS_US), null]); + [DuckDBTimestampValue.Min, DuckDBTimestampValue.Max, null]); assertValues(chunk, 15, DuckDBTimestampSecondsVector, - [new DuckDBTimestampSecondsValue(MinTS_S), new DuckDBTimestampSecondsValue(MaxTS_S), null]); + [DuckDBTimestampSecondsValue.Min, DuckDBTimestampSecondsValue.Max, null]); assertValues(chunk, 16, DuckDBTimestampMillisecondsVector, - [new DuckDBTimestampMillisecondsValue(MinTS_MS), new DuckDBTimestampMillisecondsValue(MaxTS_MS), null]); + [DuckDBTimestampMillisecondsValue.Min, DuckDBTimestampMillisecondsValue.Max, null]); assertValues(chunk, 17, DuckDBTimestampNanosecondsVector, - [new DuckDBTimestampNanosecondsValue(MinTS_NS), new DuckDBTimestampNanosecondsValue(MaxTS_NS), null]); - assertValues(chunk, 18, DuckDBTimeTZVector, [MinTimeTZ, MaxTimeTZ, null]); + [DuckDBTimestampNanosecondsValue.Min, DuckDBTimestampNanosecondsValue.Max, null]); + assertValues(chunk, 18, DuckDBTimeTZVector, [DuckDBTimeTZValue.Min, DuckDBTimeTZValue.Max, null]); assertValues(chunk, 19, DuckDBTimestampTZVector, - [new DuckDBTimestampTZValue(MinTS_US), new DuckDBTimestampTZValue(MaxTS_US), null]); - assertValues(chunk, 20, DuckDBFloatVector, [MinFloat32, MaxFloat32, null]); - assertValues(chunk, 21, DuckDBDoubleVector, [MinFloat64, MaxFloat64, null]); + [DuckDBTimestampTZValue.Min, DuckDBTimestampTZValue.Max, null]); + assertValues(chunk, 20, DuckDBFloatVector, [DuckDBFloatType.Min, DuckDBFloatType.Max, null]); + assertValues(chunk, 21, DuckDBDoubleVector, [DuckDBDoubleType.Min, DuckDBDoubleType.Max, null]); assertValues(chunk, 22, DuckDBDecimal2Vector, [ - new DuckDBDecimalValue(new DuckDBDecimalType(4, 1), -9999), - new DuckDBDecimalValue(new DuckDBDecimalType(4, 1), 9999), + decimalNumber(4, 1, -9999), + decimalNumber(4, 1, 9999 as number), null, ]); assertValues(chunk, 23, DuckDBDecimal4Vector, [ - new DuckDBDecimalValue(new DuckDBDecimalType(9, 4), -999999999), - new DuckDBDecimalValue(new DuckDBDecimalType(9, 4), 999999999), + decimalNumber(9, 4, -999999999), + decimalNumber(9, 4, 999999999), null, ]); assertValues(chunk, 24, DuckDBDecimal8Vector, [ - new DuckDBDecimalValue(new DuckDBDecimalType(18, 6), -BI_18_9s), - new DuckDBDecimalValue(new DuckDBDecimalType(18, 6), BI_18_9s), + decimalBigint(18, 6, -BI_18_9s), + decimalBigint(18, 6, BI_18_9s), null, ]); assertValues(chunk, 25, DuckDBDecimal16Vector, [ - new DuckDBDecimalValue(new DuckDBDecimalType(38, 10), -BI_38_9s), - new DuckDBDecimalValue(new DuckDBDecimalType(38, 10), BI_38_9s), + decimalBigint(38, 10, -BI_38_9s), + decimalBigint(38, 10, BI_38_9s), null, ]); - assertValues(chunk, 26, DuckDBUUIDVector, [MinUUID, MaxUUID, null]); + assertValues(chunk, 26, DuckDBUUIDVector, [DuckDBUUIDValue.Min, DuckDBUUIDValue.Max, null]); assertValues(chunk, 27, DuckDBIntervalVector, [ - new DuckDBIntervalValue(0, 0, BigInt(0)), - new DuckDBIntervalValue(999, 999, BigInt(999999999)), + intervalValue(0, 0, 0n), + intervalValue(999, 999, 999999999n), null, ]); assertValues(chunk, 28, DuckDBVarCharVector, ['🦆🦆🦆🦆🦆🦆', 'goo\0se', null]); assertValues(chunk, 29, DuckDBBlobVector, [ - blobFromString('thisisalongblob\x00withnullbytes'), - blobFromString('\x00\x00\x00a'), + DuckDBBlobValue.fromString('thisisalongblob\x00withnullbytes'), + DuckDBBlobValue.fromString('\x00\x00\x00a'), null, ]); assertValues(chunk, 30, DuckDBBitVector, [ - DuckDBBitValue.fromString('0010001001011100010101011010111'), - DuckDBBitValue.fromString('10101'), + bitValue('0010001001011100010101011010111'), + bitValue('10101'), null, ]); assertValues(chunk, 31, DuckDBEnum1Vector, [ @@ -742,218 +644,196 @@ describe('api', () => { null, ]); // int_array - assertNestedValues, DuckDBListVector>(chunk, 34, DuckDBListVector, [ - (v, n) => assertVectorValues(v?.vector, [], n), - (v, n) => assertVectorValues(v?.vector, [42, 999, null, null, -42], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 34, DuckDBListVector, [ + listValue([]), + listValue([42, 999, null, null, -42]), + null, ]); // double_array - assertNestedValues, DuckDBListVector>(chunk, 35, DuckDBListVector, [ - (v, n) => assertVectorValues(v?.vector, [], n), - (v, n) => assertVectorValues(v?.vector, [42.0, NaN, Infinity, -Infinity, null, -42.0], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 35, DuckDBListVector, [ + listValue([]), + listValue([42.0, NaN, Infinity, -Infinity, null, -42.0]), + null, ]); // date_array - assertNestedValues, DuckDBListVector>(chunk, 36, DuckDBListVector, [ - (v, n) => assertVectorValues(v?.vector, [], n), - (v, n) => assertVectorValues(v?.vector, [new DuckDBDateValue(0), DatePosInf, DateNegInf, null, new DuckDBDateValue(19124)], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 36, DuckDBListVector, [ + listValue([]), + listValue([dateValue(0), DuckDBDateValue.PosInf, DuckDBDateValue.NegInf, null, dateValue(19124)]), + null, ]); // timestamp_array - assertNestedValues, DuckDBListVector>(chunk, 37, DuckDBListVector, [ - (v, n) => assertVectorValues(v?.vector, [], n), - // 1652372625 is 2022-05-12 16:23:45 - (v, n) => assertVectorValues(v?.vector, [ - new DuckDBTimestampValue(BI_0), - new DuckDBTimestampValue(TS_US_Inf), - new DuckDBTimestampValue(-TS_US_Inf), + assertValues(chunk, 37, DuckDBListVector, [ + listValue([]), + listValue([ + DuckDBTimestampValue.Epoch, + DuckDBTimestampValue.PosInf, + DuckDBTimestampValue.NegInf, null, - new DuckDBTimestampValue(BigInt(1652372625)*BI_1000*BI_1000), - ], n), - (v, n) => assert.strictEqual(v, null, n), + // 1652372625 is 2022-05-12 16:23:45 + timestampValue(1652372625n * 1000n * 1000n), + ]), + null, ]); // timestamptz_array - assertNestedValues, DuckDBListVector>(chunk, 38, DuckDBListVector, [ - (v, n) => assertVectorValues(v?.vector, [], n), - // 1652397825 = 1652372625 + 25200, 25200 = 7 * 60 * 60 = 7 hours in seconds - // This 7 hour difference is hard coded into test_all_types (value is 2022-05-12 16:23:45-07) - (v, n) => assertVectorValues(v?.vector, [ - new DuckDBTimestampTZValue(BI_0), - new DuckDBTimestampTZValue(TS_US_Inf), - new DuckDBTimestampTZValue(-TS_US_Inf), + assertValues(chunk, 38, DuckDBListVector, [ + listValue([]), + listValue([ + DuckDBTimestampTZValue.Epoch, + DuckDBTimestampTZValue.PosInf, + DuckDBTimestampTZValue.NegInf, null, - new DuckDBTimestampTZValue(BigInt(1652397825)*BI_1000*BI_1000), - ], n), - (v, n) => assert.strictEqual(v, null, n), + // 1652397825 = 1652372625 + 25200, 25200 = 7 * 60 * 60 = 7 hours in seconds + // This 7 hour difference is hard coded into test_all_types (value is 2022-05-12 16:23:45-07) + timestampTZValue(1652397825n * 1000n * 1000n), + ]), + null, ]); // varchar_array - assertNestedValues, DuckDBListVector>(chunk, 39, DuckDBListVector, [ - (v, n) => assertVectorValues(v?.vector, [], n), + assertValues(chunk, 39, DuckDBListVector, [ + listValue([]), // Note that the string 'goose' in varchar_array does NOT have an embedded null character. - (v, n) => assertVectorValues(v?.vector, ['🦆🦆🦆🦆🦆🦆', 'goose', null, ''], n), - (v, n) => assert.strictEqual(v, null, n), + listValue(['🦆🦆🦆🦆🦆🦆', 'goose', null, '']), + null, ]); // nested_int_array - assertNestedValues>, DuckDBListVector>>(chunk, 40, DuckDBListVector, [ - (v, n) => { - assert.ok(v, `${n} unexpectedly null`); - if (!v) return; - assert.strictEqual(v.vector.itemCount, 0, `${n} not empty`); - }, - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, [], nn), - (vv, nn) => assertVectorValues(vv?.vector, [42, 999, null, null, -42], nn), - (vv, nn) => assert.strictEqual(vv, null, nn), - (vv, nn) => assertVectorValues(vv?.vector, [], nn), - (vv, nn) => assertVectorValues(vv?.vector, [42, 999, null, null, -42], nn), - ], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 40, DuckDBListVector, [ + listValue([]), + listValue([ + listValue([]), + listValue([42, 999, null, null, -42]), + null, + listValue([]), + listValue([42, 999, null, null, -42]), + ]), + null, ]); assertValues(chunk, 41, DuckDBStructVector, [ - new DuckDBStructValue(structType, [null, null]), - new DuckDBStructValue(structType, [42, '🦆🦆🦆🦆🦆🦆']), + structValue({ 'a': null, 'b': null }), + structValue({ 'a': 42, 'b': '🦆🦆🦆🦆🦆🦆' }), null, ]); // struct_of_arrays - assertNestedValues(chunk, 42, DuckDBStructVector, [ - (v, n) => assert.deepStrictEqual(v?.values, [null, null], n), - (v, n) => { - assert.ok(v, `${n} unexpectedly null`); - if (!v) return; - assert.deepStrictEqual(v.type, structOfArraysType); - assert.strictEqual(v.values.length, 2, n); - assert.ok(v.values[0] instanceof DuckDBListValue); - assertVectorValues((v.values[0] as DuckDBListValue).vector, [42, 999, null, null, -42], n); - assert.ok(v.values[1] instanceof DuckDBListValue); - assertVectorValues((v.values[1] as DuckDBListValue).vector, ['🦆🦆🦆🦆🦆🦆', 'goose', null, ''], n); - }, - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 42, DuckDBStructVector, [ + structValue({ 'a': null, 'b': null }), + structValue({ + 'a': listValue([42, 999, null, null, -42]), + 'b': listValue(['🦆🦆🦆🦆🦆🦆', 'goose', null, '']), + }), + null, ]); // array_of_structs - assertNestedValues, DuckDBListVector>(chunk, 43, DuckDBListVector, [ - (v, n) => assertVectorValues(v?.vector, [], n), - (v, n) => assertVectorValues(v?.vector, [ - new DuckDBStructValue(structType, [null, null]), - new DuckDBStructValue(structType, [42, '🦆🦆🦆🦆🦆🦆']), + assertValues(chunk, 43, DuckDBListVector, [ + listValue([]), + listValue([ + structValue({ 'a': null, 'b': null }), + structValue({ 'a': 42, 'b': '🦆🦆🦆🦆🦆🦆' }), null, - ], n), - (v, n) => assert.strictEqual(v, null, n), + ]), + null, ]); assertValues(chunk, 44, DuckDBMapVector, [ - new DuckDBMapValue(mapType, []), - new DuckDBMapValue(mapType, [{ key: 'key1', value: '🦆🦆🦆🦆🦆🦆' }, { key: 'key2', value: 'goose' }]), + mapValue([]), + mapValue([{ key: 'key1', value: '🦆🦆🦆🦆🦆🦆' }, { key: 'key2', value: 'goose' }]), null, ]); assertValues(chunk, 45, DuckDBUnionVector, [ - new DuckDBUnionValue('name', 'Frank'), - new DuckDBUnionValue('age', 5), + unionValue('name', 'Frank'), + unionValue('age', 5), null, ]); // fixed_int_array - assertNestedValues, DuckDBArrayVector>(chunk, 46, DuckDBArrayVector, [ - (v, n) => assertVectorValues(v?.vector, [null, 2, 3], n), - (v, n) => assertVectorValues(v?.vector, [4, 5, 6], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 46, DuckDBArrayVector, [ + arrayValue([null, 2, 3]), + arrayValue([4, 5, 6]), + null, ]); // fixed_varchar_array - assertNestedValues, DuckDBArrayVector>(chunk, 47, DuckDBArrayVector, [ - (v, n) => assertVectorValues(v?.vector, ['a', null, 'c'], n), - (v, n) => assertVectorValues(v?.vector, ['d', 'e', 'f'], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 47, DuckDBArrayVector, [ + arrayValue(['a', null, 'c']), + arrayValue(['d', 'e', 'f']), + null, ]); // fixed_nested_int_array - assertNestedValues>, DuckDBArrayVector>>(chunk, 48, DuckDBArrayVector, [ - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, [null, 2, 3], nn), - (vv, nn) => assert.strictEqual(vv, null, nn), - (vv, nn) => assertVectorValues(vv?.vector, [null, 2, 3], nn), - ], n), - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, [4, 5, 6], nn), - (vv, nn) => assertVectorValues(vv?.vector, [null, 2, 3], nn), - (vv, nn) => assertVectorValues(vv?.vector, [4, 5, 6], nn), - ], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 48, DuckDBArrayVector, [ + arrayValue([ + arrayValue([null, 2, 3]), + null, + arrayValue([null, 2, 3]), + ]), + arrayValue([ + arrayValue([4, 5, 6]), + arrayValue([null, 2, 3]), + arrayValue([4, 5, 6]), + ]), + null, ]); // fixed_nested_varchar_array - assertNestedValues>, DuckDBArrayVector>>(chunk, 49, DuckDBArrayVector, [ - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, ['a', null, 'c'], nn), - (vv, nn) => assert.strictEqual(vv, null, nn), - (vv, nn) => assertVectorValues(vv?.vector, ['a', null, 'c'], nn), - ], n), - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, ['d', 'e', 'f'], nn), - (vv, nn) => assertVectorValues(vv?.vector, ['a', null, 'c'], nn), - (vv, nn) => assertVectorValues(vv?.vector, ['d', 'e', 'f'], nn), - ], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 49, DuckDBArrayVector, [ + arrayValue([ + arrayValue(['a', null, 'c']), + null, + arrayValue(['a', null, 'c']), + ]), + arrayValue([ + arrayValue(['d', 'e', 'f']), + arrayValue(['a', null, 'c']), + arrayValue(['d', 'e', 'f']), + ]), + null, ]); // fixed_struct_array - assertNestedValues, DuckDBArrayVector>(chunk, 50, DuckDBArrayVector, [ - (v, n) => assertVectorValues(v?.vector, [ - new DuckDBStructValue(structType, [null, null]), - new DuckDBStructValue(structType, [42, '🦆🦆🦆🦆🦆🦆']), - new DuckDBStructValue(structType, [null, null]), - ], n), - (v, n) => assertVectorValues(v?.vector, [ - new DuckDBStructValue(structType, [42, '🦆🦆🦆🦆🦆🦆']), - new DuckDBStructValue(structType, [null, null]), - new DuckDBStructValue(structType, [42, '🦆🦆🦆🦆🦆🦆']), - ], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 50, DuckDBArrayVector, [ + arrayValue([ + structValue({ 'a': null, 'b': null }), + structValue({ 'a': 42, 'b': '🦆🦆🦆🦆🦆🦆' }), + structValue({ 'a': null, 'b': null }), + ]), + arrayValue([ + structValue({ 'a': 42, 'b': '🦆🦆🦆🦆🦆🦆' }), + structValue({ 'a': null, 'b': null }), + structValue({ 'a': 42, 'b': '🦆🦆🦆🦆🦆🦆' }), + ]), + null, ]); // struct_of_fixed_array - assertNestedValues(chunk, 51, DuckDBStructVector, [ - (v, n) => { - assert.ok(v, `${n} unexpectedly null`); - if (!v) return; - assert.deepStrictEqual(v.type, structOfFixedArrayType); - assert.strictEqual(v.values.length, 2, n); - assert.ok(v?.values[0] instanceof DuckDBArrayValue); - assertVectorValues((v.values[0] as DuckDBArrayValue).vector, [null, 2, 3], n); - assert.ok(v?.values[1] instanceof DuckDBArrayValue); - assertVectorValues((v.values[1] as DuckDBArrayValue).vector, ['a', null, 'c'], n); - }, - (v, n) => { - assert.ok(v, `${n} unexpectedly null`); - if (!v) return; - assert.deepStrictEqual(v.type, structOfFixedArrayType); - assert.strictEqual(v.values.length, 2, n); - assert.ok(v?.values[0] instanceof DuckDBArrayValue); - assertVectorValues((v.values[0] as DuckDBArrayValue).vector, [4, 5, 6], n); - assert.ok(v?.values[1] instanceof DuckDBArrayValue); - assertVectorValues((v.values[1] as DuckDBArrayValue).vector, ['d', 'e', 'f'], n); - }, - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 51, DuckDBStructVector, [ + structValue({ + 'a': arrayValue([null, 2, 3]), + 'b': arrayValue(['a', null, 'c']), + }), + structValue({ + 'a': arrayValue([4, 5, 6]), + 'b': arrayValue(['d', 'e', 'f']), + }), + null, ]); // fixed_array_of_int_list - assertNestedValues>, DuckDBArrayVector>>(chunk, 52, DuckDBArrayVector, [ - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, [], nn), - (vv, nn) => assertVectorValues(vv?.vector, [42, 999, null, null, -42], nn), - (vv, nn) => assertVectorValues(vv?.vector, [], nn), - ], n), - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, [42, 999, null, null, -42], nn), - (vv, nn) => assertVectorValues(vv?.vector, [], nn), - (vv, nn) => assertVectorValues(vv?.vector, [42, 999, null, null, -42], nn), - ], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 52, DuckDBArrayVector, [ + arrayValue([ + listValue([]), + listValue([42, 999, null, null, -42]), + listValue([]), + ]), + arrayValue([ + listValue([42, 999, null, null, -42]), + listValue([]), + listValue([42, 999, null, null, -42]), + ]), + null, ]); // list_of_fixed_int_array - assertNestedValues>, DuckDBListVector>>(chunk, 53, DuckDBListVector, [ - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, [null, 2, 3], nn), - (vv, nn) => assertVectorValues(vv?.vector, [4, 5, 6], nn), - (vv, nn) => assertVectorValues(vv?.vector, [null, 2, 3], nn), - ], n), - (v, n) => assertNestedVectorValues(v?.vector, [ - (vv, nn) => assertVectorValues(vv?.vector, [4, 5, 6], nn), - (vv, nn) => assertVectorValues(vv?.vector, [null, 2, 3], nn), - (vv, nn) => assertVectorValues(vv?.vector, [4, 5, 6], nn), - ], n), - (v, n) => assert.strictEqual(v, null, n), + assertValues(chunk, 53, DuckDBListVector, [ + listValue([ + arrayValue([null, 2, 3]), + arrayValue([4, 5, 6]), + arrayValue([null, 2, 3]), + ]), + listValue([ + arrayValue([4, 5, 6]), + arrayValue([null, 2, 3]), + arrayValue([4, 5, 6]), + ]), + null, ]); } finally { chunk.dispose(); @@ -963,4 +843,21 @@ describe('api', () => { } }); }); + test('values toString', () => { + assert.equal(arrayValue([]).toString(), '[]'); + assert.equal(arrayValue([1, 2, 3]).toString(), '[1, 2, 3]'); + assert.equal(arrayValue(['a', 'b', 'c']).toString(), `['a', 'b', 'c']`); + + assert.equal(bitValue('').toString(), ''); + assert.equal(bitValue('10101').toString(), '10101'); + assert.equal(bitValue('0010001001011100010101011010111').toString(), '0010001001011100010101011010111'); + + assert.equal(DuckDBBlobValue.fromString('').toString(), ''); + assert.equal(DuckDBBlobValue.fromString('thisisalongblob\x00withnullbytes').toString(), 'thisisalongblob\\x00withnullbytes'); + assert.equal(DuckDBBlobValue.fromString('\x00\x00\x00a').toString(), '\\x00\\x00\\x00a'); + + assert.equal(DuckDBDateValue.Epoch.toString(), '1970-01-01'); + assert.equal(DuckDBDateValue.Max.toString(), '5881580-07-10'); + assert.equal(DuckDBDateValue.Min.toString(), '5877642-06-25 (BC)'); + }); }); From 47ee8dea5461401c81dc3e3cda098795b1d1f797 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Thu, 31 Oct 2024 21:47:11 -0700 Subject: [PATCH 2/2] rest of toString tests and some fixes to datetime conversions --- .../conversion/dateTimeStringConversion.ts | 92 ++++++++++-- api/test/api.test.ts | 137 +++++++++++++++++- 2 files changed, 213 insertions(+), 16 deletions(-) diff --git a/api/src/conversion/dateTimeStringConversion.ts b/api/src/conversion/dateTimeStringConversion.ts index 00291a17..fdab7339 100644 --- a/api/src/conversion/dateTimeStringConversion.ts +++ b/api/src/conversion/dateTimeStringConversion.ts @@ -1,15 +1,16 @@ const DAYS_IN_400_YEARS = 146097; // (((365 * 4 + 1) * 25) - 1) * 4 + 1 const MILLISECONDS_PER_DAY_NUM = 86400000; // 1000 * 60 * 60 * 24 -const MICROSECONDS_PER_SECOND = BigInt(1000000); -const MICROSECONDS_PER_MILLISECOND = BigInt(1000); -const NANOSECONDS_PER_MICROSECOND = BigInt(1000); -const SECONDS_PER_MINUTE = BigInt(60); -const MINUTES_PER_HOUR = BigInt(60); -const MICROSECONDS_PER_DAY = BigInt(86400000000); // 24 * 60 * 60 * 1000000 +const MICROSECONDS_PER_SECOND = 1000000n; +const MICROSECONDS_PER_MILLISECOND = 1000n; +const NANOSECONDS_PER_SECOND = 1000000000n +const SECONDS_PER_MINUTE = 60n; +const MINUTES_PER_HOUR = 60n; +const MICROSECONDS_PER_DAY = 86400000000n; // 24 * 60 * 60 * 1000000 +const NANOSECONDS_PER_DAY = 86400000000000n; // 24 * 60 * 60 * 1000000000 -const NEGATIVE_INFINITY_TIMESTAMP = BigInt('-9223372036854775807'); // -(2^63-1) -const POSITIVE_INFINITY_TIMESTAMP = BigInt('9223372036854775807'); // 2^63-1 +const NEGATIVE_INFINITY_TIMESTAMP = -9223372036854775807n; // -(2^63-1) +const POSITIVE_INFINITY_TIMESTAMP = 9223372036854775807n; // 2^63-1 export function getDuckDBDateStringFromYearMonthDay( year: number, @@ -64,6 +65,23 @@ export function getDuckDBTimeStringFromParts( }`; } +export function getDuckDBTimeStringFromPartsNS( + hoursPart: bigint, + minutesPart: bigint, + secondsPart: bigint, + nanosecondsPart: bigint, +): string { + const hoursStr = String(hoursPart).padStart(2, '0'); + const minutesStr = String(minutesPart).padStart(2, '0'); + const secondsStr = String(secondsPart).padStart(2, '0'); + const nanosecondsStr = String(nanosecondsPart) + .padStart(9, '0') + .replace(/0+$/, ''); + return `${hoursStr}:${minutesStr}:${secondsStr}${ + nanosecondsStr.length > 0 ? `.${nanosecondsStr}` : '' + }`; +} + export function getDuckDBTimeStringFromPositiveMicroseconds( positiveMicroseconds: bigint, ): string { @@ -81,6 +99,23 @@ export function getDuckDBTimeStringFromPositiveMicroseconds( ); } +export function getDuckDBTimeStringFromPositiveNanoseconds( + positiveNanoseconds: bigint, +): string { + const nanosecondsPart = positiveNanoseconds % NANOSECONDS_PER_SECOND; + const seconds = positiveNanoseconds / NANOSECONDS_PER_SECOND; + const secondsPart = seconds % SECONDS_PER_MINUTE; + const minutes = seconds / SECONDS_PER_MINUTE; + const minutesPart = minutes % MINUTES_PER_HOUR; + const hoursPart = minutes / MINUTES_PER_HOUR; + return getDuckDBTimeStringFromPartsNS( + hoursPart, + minutesPart, + secondsPart, + nanosecondsPart, + ); +} + export function getDuckDBTimeStringFromMicrosecondsInDay( microsecondsInDay: bigint, ): string { @@ -91,6 +126,16 @@ export function getDuckDBTimeStringFromMicrosecondsInDay( return getDuckDBTimeStringFromPositiveMicroseconds(positiveMicroseconds); } +export function getDuckDBTimeStringFromNanosecondsInDay( + nanosecondsInDay: bigint, +): string { + const positiveNanoseconds = + nanosecondsInDay < 0 + ? nanosecondsInDay + NANOSECONDS_PER_DAY + : nanosecondsInDay; + return getDuckDBTimeStringFromPositiveNanoseconds(positiveNanoseconds); +} + export function getDuckDBTimeStringFromMicroseconds( microseconds: bigint, ): string { @@ -114,6 +159,19 @@ export function getDuckDBTimestampStringFromDaysAndMicroseconds( return `${dateStr} ${timeStr}${timezoneStr}`; } +export function getDuckDBTimestampStringFromDaysAndNanoseconds( + days: bigint, + nanosecondsInDay: bigint, + timezone?: string | null, +): string { + // This conversion of BigInt to Number is safe, because the largest absolute value that `days` can has is 106751 + // which fits without loss of precision in a JS Number. (106751 = (2^63-1) / NANOSECONDS_PER_DAY) + const dateStr = getDuckDBDateStringFromDays(Number(days)); + const timeStr = getDuckDBTimeStringFromNanosecondsInDay(nanosecondsInDay); + const timezoneStr = timezone ? ` ${timezone}` : ''; + return `${dateStr} ${timeStr}${timezoneStr}`; +} + export function getDuckDBTimestampStringFromMicroseconds( microseconds: bigint, timezone?: string | null, @@ -163,18 +221,22 @@ export function getDuckDBTimestampStringFromNanoseconds( nanoseconds: bigint, timezone?: string | null, ): string { - // Note that this division causes loss of precision. This matches the behavior of the DuckDB. It's important that this - // precision loss happen before the negative correction in getTimestampStringFromMicroseconds, otherwise off-by-one - // errors can occur. - return getDuckDBTimestampStringFromMicroseconds( - nanoseconds / NANOSECONDS_PER_MICROSECOND, + let days = nanoseconds / NANOSECONDS_PER_DAY; + let nanosecondsPart = nanoseconds % NANOSECONDS_PER_DAY; + if (nanosecondsPart < 0) { + days--; + nanosecondsPart += NANOSECONDS_PER_DAY; + } + return getDuckDBTimestampStringFromDaysAndNanoseconds( + days, + nanosecondsPart, timezone, ); } // Assumes baseUnit can be pluralized by adding an 's'. function numberAndUnit(value: number, baseUnit: string): string { - return `${value} ${baseUnit}${value !== 1 ? 's' : ''}`; + return `${value} ${baseUnit}${Math.abs(value) !== 1 ? 's' : ''}`; } export function getDuckDBIntervalString( @@ -199,7 +261,7 @@ export function getDuckDBIntervalString( if (days !== 0) { parts.push(numberAndUnit(days, 'day')); } - if (microseconds !== BigInt(0)) { + if (microseconds !== 0n) { parts.push(getDuckDBTimeStringFromMicroseconds(microseconds)); } if (parts.length > 0) { diff --git a/api/test/api.test.ts b/api/test/api.test.ts index 0684efd7..970bf99a 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -106,9 +106,12 @@ import { listValue, mapValue, structValue, + timeTZValue, + timeValue, timestampTZValue, timestampValue, unionValue, + uuidValue, version } from '../src'; @@ -593,7 +596,7 @@ describe('api', () => { assertValues(chunk, 21, DuckDBDoubleVector, [DuckDBDoubleType.Min, DuckDBDoubleType.Max, null]); assertValues(chunk, 22, DuckDBDecimal2Vector, [ decimalNumber(4, 1, -9999), - decimalNumber(4, 1, 9999 as number), + decimalNumber(4, 1, 9999), null, ]); assertValues(chunk, 23, DuckDBDecimal4Vector, [ @@ -844,20 +847,152 @@ describe('api', () => { }); }); test('values toString', () => { + // array assert.equal(arrayValue([]).toString(), '[]'); assert.equal(arrayValue([1, 2, 3]).toString(), '[1, 2, 3]'); assert.equal(arrayValue(['a', 'b', 'c']).toString(), `['a', 'b', 'c']`); + // bit assert.equal(bitValue('').toString(), ''); assert.equal(bitValue('10101').toString(), '10101'); assert.equal(bitValue('0010001001011100010101011010111').toString(), '0010001001011100010101011010111'); + // blob assert.equal(DuckDBBlobValue.fromString('').toString(), ''); assert.equal(DuckDBBlobValue.fromString('thisisalongblob\x00withnullbytes').toString(), 'thisisalongblob\\x00withnullbytes'); assert.equal(DuckDBBlobValue.fromString('\x00\x00\x00a').toString(), '\\x00\\x00\\x00a'); + // date assert.equal(DuckDBDateValue.Epoch.toString(), '1970-01-01'); assert.equal(DuckDBDateValue.Max.toString(), '5881580-07-10'); assert.equal(DuckDBDateValue.Min.toString(), '5877642-06-25 (BC)'); + + // decimal + assert.equal(decimalNumber(4, 1, 0).toString(), '0.0'); + assert.equal(decimalNumber(4, 1, 9876).toString(), '987.6'); + assert.equal(decimalNumber(4, 1, -9876).toString(), '-987.6'); + + assert.equal(decimalNumber(9, 4, 0).toString(), '0.0000'); + assert.equal(decimalNumber(9, 4, 987654321).toString(), '98765.4321'); + assert.equal(decimalNumber(9, 4, -987654321).toString(), '-98765.4321'); + + assert.equal(decimalNumber(18, 6, 0).toString(), '0.000000'); + assert.equal(decimalBigint(18, 6, 987654321098765432n).toString(), '987654321098.765432'); + assert.equal(decimalBigint(18, 6, -987654321098765432n).toString(), '-987654321098.765432'); + + assert.equal(decimalNumber(38, 10, 0).toString(), '0.0000000000'); + assert.equal(decimalBigint(38, 10, 98765432109876543210987654321098765432n).toString(), '9876543210987654321098765432.1098765432'); + assert.equal(decimalBigint(38, 10, -98765432109876543210987654321098765432n).toString(), '-9876543210987654321098765432.1098765432'); + + // interval + assert.equal(intervalValue(0, 0, 0n).toString(), '00:00:00'); + + assert.equal(intervalValue( 1, 0, 0n).toString(), '1 month'); + assert.equal(intervalValue(-1, 0, 0n).toString(), '-1 month'); + assert.equal(intervalValue( 2, 0, 0n).toString(), '2 months'); + assert.equal(intervalValue(-2, 0, 0n).toString(), '-2 months'); + assert.equal(intervalValue( 12, 0, 0n).toString(), '1 year'); + assert.equal(intervalValue(-12, 0, 0n).toString(), '-1 year'); + assert.equal(intervalValue( 24, 0, 0n).toString(), '2 years'); + assert.equal(intervalValue(-24, 0, 0n).toString(), '-2 years'); + assert.equal(intervalValue( 25, 0, 0n).toString(), '2 years 1 month'); + assert.equal(intervalValue(-25, 0, 0n).toString(), '-2 years -1 month'); + + assert.equal(intervalValue(0, 1, 0n).toString(), '1 day'); + assert.equal(intervalValue(0, -1, 0n).toString(), '-1 day'); + assert.equal(intervalValue(0, 2, 0n).toString(), '2 days'); + assert.equal(intervalValue(0, -2, 0n).toString(), '-2 days'); + assert.equal(intervalValue(0, 30, 0n).toString(), '30 days'); + assert.equal(intervalValue(0, 365, 0n).toString(), '365 days'); + + assert.equal(intervalValue(0, 0, 1n).toString(), '00:00:00.000001'); + assert.equal(intervalValue(0, 0, -1n).toString(), '-00:00:00.000001'); + assert.equal(intervalValue(0, 0, 987654n).toString(), '00:00:00.987654'); + assert.equal(intervalValue(0, 0, -987654n).toString(), '-00:00:00.987654'); + assert.equal(intervalValue(0, 0, 1000000n).toString(), '00:00:01'); + assert.equal(intervalValue(0, 0, -1000000n).toString(), '-00:00:01'); + assert.equal(intervalValue(0, 0, 59n * 1000000n).toString(), '00:00:59'); + assert.equal(intervalValue(0, 0, -59n * 1000000n).toString(), '-00:00:59'); + assert.equal(intervalValue(0, 0, 60n * 1000000n).toString(), '00:01:00'); + assert.equal(intervalValue(0, 0, -60n * 1000000n).toString(), '-00:01:00'); + assert.equal(intervalValue(0, 0, 59n * 60n * 1000000n).toString(), '00:59:00'); + assert.equal(intervalValue(0, 0, -59n * 60n * 1000000n).toString(), '-00:59:00'); + assert.equal(intervalValue(0, 0, 60n * 60n * 1000000n).toString(), '01:00:00'); + assert.equal(intervalValue(0, 0, -60n * 60n * 1000000n).toString(), '-01:00:00'); + assert.equal(intervalValue(0, 0, 24n * 60n * 60n * 1000000n).toString(), '24:00:00'); + assert.equal(intervalValue(0, 0, -24n * 60n * 60n * 1000000n).toString(), '-24:00:00'); + assert.equal(intervalValue(0, 0, 2147483647n * 60n * 60n * 1000000n).toString(), '2147483647:00:00'); + assert.equal(intervalValue(0, 0, -2147483647n * 60n * 60n * 1000000n).toString(), '-2147483647:00:00'); + assert.equal(intervalValue(0, 0, 2147483647n * 60n * 60n * 1000000n + 1n ).toString(), '2147483647:00:00.000001'); + assert.equal(intervalValue(0, 0, -(2147483647n * 60n * 60n * 1000000n + 1n)).toString(), '-2147483647:00:00.000001'); + + assert.equal(intervalValue(2 * 12 + 3, 5, (7n * 60n * 60n + 11n * 60n + 13n) * 1000000n + 17n).toString(), + '2 years 3 months 5 days 07:11:13.000017'); + assert.equal(intervalValue(-(2 * 12 + 3), -5, -((7n * 60n * 60n + 11n * 60n + 13n) * 1000000n + 17n)).toString(), + '-2 years -3 months -5 days -07:11:13.000017'); + + // list + assert.equal(listValue([]).toString(), '[]'); + assert.equal(listValue([1, 2, 3]).toString(), '[1, 2, 3]'); + assert.equal(listValue(['a', 'b', 'c']).toString(), `['a', 'b', 'c']`); + + // map + assert.equal(mapValue([]).toString(), '{}'); + assert.equal(mapValue([{ key: 1, value: 'a' }, { key: 2, value: 'b' }]).toString(), `{1: 'a', 2: 'b'}`); + + // struct + assert.equal(structValue({}).toString(), '{}'); + assert.equal(structValue({a: 1, b: 2}).toString(), `{'a': 1, 'b': 2}`); + + // timestamp milliseconds + assert.equal(DuckDBTimestampMillisecondsValue.Epoch.toString(), '1970-01-01 00:00:00'); + assert.equal(DuckDBTimestampMillisecondsValue.Max.toString(), '294247-01-10 04:00:54.775'); + assert.equal(DuckDBTimestampMillisecondsValue.Min.toString(), '290309-12-22 (BC) 00:00:00'); + + // timestamp nanoseconds + assert.equal(DuckDBTimestampNanosecondsValue.Epoch.toString(), '1970-01-01 00:00:00'); + assert.equal(DuckDBTimestampNanosecondsValue.Max.toString(), '2262-04-11 23:47:16.854775806'); + assert.equal(DuckDBTimestampNanosecondsValue.Min.toString(), '1677-09-22 00:00:00'); + + // timestamp seconds + assert.equal(DuckDBTimestampSecondsValue.Epoch.toString(), '1970-01-01 00:00:00'); + assert.equal(DuckDBTimestampSecondsValue.Max.toString(), '294247-01-10 04:00:54'); + assert.equal(DuckDBTimestampSecondsValue.Min.toString(), '290309-12-22 (BC) 00:00:00'); + + // timestamp tz + assert.equal(DuckDBTimestampTZValue.Epoch.toString(), '1970-01-01 00:00:00'); + // assert.equal(DuckDBTimestampTZValue.Max.toString(), '294247-01-09 20:00:54.775806-08'); // in PST + assert.equal(DuckDBTimestampTZValue.Max.toString(), '294247-01-10 04:00:54.775806'); // TODO TZ + // assert.equal(DuckDBTimestampTZValue.Min.toString(), '290309-12-21 (BC) 16:00:00-08'); // in PST + assert.equal(DuckDBTimestampTZValue.Min.toString(), '290309-12-22 (BC) 00:00:00'); // TODO TZ + assert.equal(DuckDBTimestampTZValue.PosInf.toString(), 'infinity'); + assert.equal(DuckDBTimestampTZValue.NegInf.toString(), '-infinity'); + + // timestamp + assert.equal(DuckDBTimestampValue.Epoch.toString(), '1970-01-01 00:00:00'); + assert.equal(DuckDBTimestampValue.Max.toString(), '294247-01-10 04:00:54.775806'); + assert.equal(DuckDBTimestampValue.Min.toString(), '290309-12-22 (BC) 00:00:00'); + assert.equal(DuckDBTimestampValue.PosInf.toString(), 'infinity'); + assert.equal(DuckDBTimestampValue.NegInf.toString(), '-infinity'); + + // time tz + assert.equal(timeTZValue(0, 0).toString(), '00:00:00'); + // assert.equal(DuckDBTimeTZValue.Max.toString(), '24:00:00-15:59:59'); + assert.equal(DuckDBTimeTZValue.Max.toString(), '24:00:00'); // TODO TZ + // assert.equal(DuckDBTimeTZValue.Max.toString(), '00:00:00+15:59:59'); + assert.equal(DuckDBTimeTZValue.Min.toString(), '00:00:00'); // TODO TZ + + // time + assert.equal(DuckDBTimeValue.Max.toString(), '24:00:00'); + assert.equal(DuckDBTimeValue.Min.toString(), '00:00:00'); + assert.equal(timeValue((12n * 60n * 60n + 34n * 60n + 56n) * 1000000n + 987654n).toString(), '12:34:56.987654'); + + // union + assert.equal(unionValue('a', 42).toString(), '42'); + assert.equal(unionValue('b', 'duck').toString(), 'duck'); + + // uuid + assert.equal(uuidValue(0n).toString(), '00000000-0000-0000-0000-000000000000'); + assert.equal(uuidValue(2n ** 128n - 1n).toString(), 'ffffffff-ffff-ffff-ffff-ffffffffffff'); }); });