mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-18 12:08:35 +00:00
36054be576
<!-- lance-gatekeeper-fix:v1 agent=613a074d606e626c5169d601373a32d8 generation=1 --> ## Root cause When LanceDB accepted an Arrow table created by a different installed Arrow package, its compatibility sanitizer rebuilt each Data node without converting the foreign type or preserving nested children. It also dropped the separate dictionary vector payload and did not preserve identity shared by dictionary schema types, vector wrappers, or growing dictionary chunks. ## Fix Recursively sanitize nested Arrow data types and child data. Use one table-scoped sanitization context to rebuild and memoize source type objects, dictionary vectors, and Data nodes in the local Arrow realm, preserving all identities required by Arrow IPC. Add Arrow 15 through 18 regressions for list serialization, ordinary dictionaries, dictionaries shared across fields and batches, growing dictionaries, and IPC round trips. ## Validation - pnpm test __test__/arrow.test.ts --runInBand (188 passed) - pnpm lint - pnpm build - pnpm test --runInBand (706 passed, 5 skipped) - pnpm run docs Fixes #2256 --------- Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
782 lines
22 KiB
TypeScript
782 lines
22 KiB
TypeScript
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
// The utilities in this file help sanitize data from the user's arrow
|
|
// library into the types expected by vectordb's arrow library. Node
|
|
// generally allows for mulitple versions of the same library (and sometimes
|
|
// even multiple copies of the same version) to be installed at the same
|
|
// time. However, arrow-js uses instanceof which expected that the input
|
|
// comes from the exact same library instance. This is not always the case
|
|
// and so we must sanitize the input to ensure that it is compatible.
|
|
|
|
import { BufferType, Data, Vector } from "apache-arrow";
|
|
import type { IntBitWidth, TKeys, TimeBitWidth } from "apache-arrow/type";
|
|
import {
|
|
Binary,
|
|
Bool,
|
|
DataLike,
|
|
DataType,
|
|
DateDay,
|
|
DateMillisecond,
|
|
type DateUnit,
|
|
Date_,
|
|
Decimal,
|
|
DenseUnion,
|
|
Dictionary,
|
|
Duration,
|
|
DurationMicrosecond,
|
|
DurationMillisecond,
|
|
DurationNanosecond,
|
|
DurationSecond,
|
|
Field,
|
|
FixedSizeBinary,
|
|
FixedSizeList,
|
|
Float,
|
|
Float16,
|
|
Float32,
|
|
Float64,
|
|
Int,
|
|
Int8,
|
|
Int16,
|
|
Int32,
|
|
Int64,
|
|
Interval,
|
|
IntervalDayTime,
|
|
IntervalYearMonth,
|
|
List,
|
|
Map_,
|
|
Null,
|
|
type Precision,
|
|
RecordBatch,
|
|
RecordBatchLike,
|
|
Schema,
|
|
SchemaLike,
|
|
SparseUnion,
|
|
Struct,
|
|
Table,
|
|
TableLike,
|
|
Time,
|
|
TimeMicrosecond,
|
|
TimeMillisecond,
|
|
TimeNanosecond,
|
|
TimeSecond,
|
|
Timestamp,
|
|
TimestampMicrosecond,
|
|
TimestampMillisecond,
|
|
TimestampNanosecond,
|
|
TimestampSecond,
|
|
Type,
|
|
Uint8,
|
|
Uint16,
|
|
Uint32,
|
|
Uint64,
|
|
Union,
|
|
Utf8,
|
|
} from "./arrow";
|
|
|
|
type SanitizationContext = {
|
|
types: WeakMap<object, DataType>;
|
|
vectors: WeakMap<object, Vector>;
|
|
data: WeakMap<object, Data<DataType>>;
|
|
};
|
|
|
|
function createSanitizationContext(): SanitizationContext {
|
|
return {
|
|
types: new WeakMap(),
|
|
vectors: new WeakMap(),
|
|
data: new WeakMap(),
|
|
};
|
|
}
|
|
|
|
export function sanitizeMetadata(
|
|
metadataLike?: unknown,
|
|
): Map<string, string> | undefined {
|
|
if (metadataLike === undefined || metadataLike === null) {
|
|
return undefined;
|
|
}
|
|
if (!(metadataLike instanceof Map)) {
|
|
throw Error("Expected metadata, if present, to be a Map<string, string>");
|
|
}
|
|
for (const item of metadataLike) {
|
|
if (typeof item[0] !== "string" || typeof item[1] !== "string") {
|
|
throw Error(
|
|
"Expected metadata, if present, to be a Map<string, string> but it had non-string keys or values",
|
|
);
|
|
}
|
|
}
|
|
return metadataLike as Map<string, string>;
|
|
}
|
|
|
|
export function sanitizeInt(typeLike: object) {
|
|
if (
|
|
!("bitWidth" in typeLike) ||
|
|
typeof typeLike.bitWidth !== "number" ||
|
|
!("isSigned" in typeLike) ||
|
|
typeof typeLike.isSigned !== "boolean"
|
|
) {
|
|
throw Error(
|
|
"Expected an Int Type to have a `bitWidth` and `isSigned` property",
|
|
);
|
|
}
|
|
return new Int(typeLike.isSigned, typeLike.bitWidth as IntBitWidth);
|
|
}
|
|
|
|
export function sanitizeFloat(typeLike: object) {
|
|
if (!("precision" in typeLike) || typeof typeLike.precision !== "number") {
|
|
throw Error("Expected a Float Type to have a `precision` property");
|
|
}
|
|
return new Float(typeLike.precision as Precision);
|
|
}
|
|
|
|
export function sanitizeDecimal(typeLike: object) {
|
|
if (
|
|
!("scale" in typeLike) ||
|
|
typeof typeLike.scale !== "number" ||
|
|
!("precision" in typeLike) ||
|
|
typeof typeLike.precision !== "number" ||
|
|
!("bitWidth" in typeLike) ||
|
|
typeof typeLike.bitWidth !== "number"
|
|
) {
|
|
throw Error(
|
|
"Expected a Decimal Type to have `scale`, `precision`, and `bitWidth` properties",
|
|
);
|
|
}
|
|
return new Decimal(typeLike.scale, typeLike.precision, typeLike.bitWidth);
|
|
}
|
|
|
|
export function sanitizeDate(typeLike: object) {
|
|
if (!("unit" in typeLike) || typeof typeLike.unit !== "number") {
|
|
throw Error("Expected a Date type to have a `unit` property");
|
|
}
|
|
return new Date_(typeLike.unit as DateUnit);
|
|
}
|
|
|
|
export function sanitizeTime(typeLike: object) {
|
|
if (
|
|
!("unit" in typeLike) ||
|
|
typeof typeLike.unit !== "number" ||
|
|
!("bitWidth" in typeLike) ||
|
|
typeof typeLike.bitWidth !== "number"
|
|
) {
|
|
throw Error(
|
|
"Expected a Time type to have `unit` and `bitWidth` properties",
|
|
);
|
|
}
|
|
return new Time(typeLike.unit, typeLike.bitWidth as TimeBitWidth);
|
|
}
|
|
|
|
export function sanitizeTimestamp(typeLike: object) {
|
|
if (!("unit" in typeLike) || typeof typeLike.unit !== "number") {
|
|
throw Error("Expected a Timestamp type to have a `unit` property");
|
|
}
|
|
let timezone = null;
|
|
if ("timezone" in typeLike && typeof typeLike.timezone === "string") {
|
|
timezone = typeLike.timezone;
|
|
}
|
|
return new Timestamp(typeLike.unit, timezone);
|
|
}
|
|
|
|
export function sanitizeTypedTimestamp(
|
|
typeLike: object,
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
Datatype:
|
|
| typeof TimestampNanosecond
|
|
| typeof TimestampMicrosecond
|
|
| typeof TimestampMillisecond
|
|
| typeof TimestampSecond,
|
|
) {
|
|
let timezone = null;
|
|
if ("timezone" in typeLike && typeof typeLike.timezone === "string") {
|
|
timezone = typeLike.timezone;
|
|
}
|
|
return new Datatype(timezone);
|
|
}
|
|
|
|
export function sanitizeInterval(typeLike: object) {
|
|
if (!("unit" in typeLike) || typeof typeLike.unit !== "number") {
|
|
throw Error("Expected an Interval type to have a `unit` property");
|
|
}
|
|
return new Interval(typeLike.unit);
|
|
}
|
|
|
|
export function sanitizeList(typeLike: object) {
|
|
return sanitizeListWithContext(typeLike, createSanitizationContext());
|
|
}
|
|
|
|
function sanitizeListWithContext(
|
|
typeLike: object,
|
|
context: SanitizationContext,
|
|
) {
|
|
if (!("children" in typeLike) || !Array.isArray(typeLike.children)) {
|
|
throw Error(
|
|
"Expected a List type to have an array-like `children` property",
|
|
);
|
|
}
|
|
if (typeLike.children.length !== 1) {
|
|
throw Error("Expected a List type to have exactly one child");
|
|
}
|
|
return new List(sanitizeFieldWithContext(typeLike.children[0], context));
|
|
}
|
|
|
|
export function sanitizeStruct(typeLike: object) {
|
|
return sanitizeStructWithContext(typeLike, createSanitizationContext());
|
|
}
|
|
|
|
function sanitizeStructWithContext(
|
|
typeLike: object,
|
|
context: SanitizationContext,
|
|
) {
|
|
if (!("children" in typeLike) || !Array.isArray(typeLike.children)) {
|
|
throw Error(
|
|
"Expected a Struct type to have an array-like `children` property",
|
|
);
|
|
}
|
|
return new Struct(
|
|
typeLike.children.map((child) => sanitizeFieldWithContext(child, context)),
|
|
);
|
|
}
|
|
|
|
export function sanitizeUnion(typeLike: object) {
|
|
return sanitizeUnionWithContext(typeLike, createSanitizationContext());
|
|
}
|
|
|
|
function sanitizeUnionWithContext(
|
|
typeLike: object,
|
|
context: SanitizationContext,
|
|
) {
|
|
if (
|
|
!("typeIds" in typeLike) ||
|
|
!("mode" in typeLike) ||
|
|
typeof typeLike.mode !== "number"
|
|
) {
|
|
throw Error(
|
|
"Expected a Union type to have `typeIds` and `mode` properties",
|
|
);
|
|
}
|
|
if (!("children" in typeLike) || !Array.isArray(typeLike.children)) {
|
|
throw Error(
|
|
"Expected a Union type to have an array-like `children` property",
|
|
);
|
|
}
|
|
|
|
return new Union(
|
|
typeLike.mode,
|
|
// biome-ignore lint/suspicious/noExplicitAny: skip
|
|
typeLike.typeIds as any,
|
|
typeLike.children.map((child) => sanitizeFieldWithContext(child, context)),
|
|
);
|
|
}
|
|
|
|
export function sanitizeTypedUnion(
|
|
typeLike: object,
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
UnionType: typeof DenseUnion | typeof SparseUnion,
|
|
) {
|
|
return sanitizeTypedUnionWithContext(
|
|
typeLike,
|
|
UnionType,
|
|
createSanitizationContext(),
|
|
);
|
|
}
|
|
|
|
function sanitizeTypedUnionWithContext(
|
|
typeLike: object,
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
UnionType: typeof DenseUnion | typeof SparseUnion,
|
|
context: SanitizationContext,
|
|
) {
|
|
if (!("typeIds" in typeLike)) {
|
|
throw Error(
|
|
"Expected a DenseUnion/SparseUnion type to have a `typeIds` property",
|
|
);
|
|
}
|
|
if (!("children" in typeLike) || !Array.isArray(typeLike.children)) {
|
|
throw Error(
|
|
"Expected a DenseUnion/SparseUnion type to have an array-like `children` property",
|
|
);
|
|
}
|
|
|
|
return new UnionType(
|
|
typeLike.typeIds as Int32Array | number[],
|
|
typeLike.children.map((child) => sanitizeFieldWithContext(child, context)),
|
|
);
|
|
}
|
|
|
|
export function sanitizeFixedSizeBinary(typeLike: object) {
|
|
if (!("byteWidth" in typeLike) || typeof typeLike.byteWidth !== "number") {
|
|
throw Error(
|
|
"Expected a FixedSizeBinary type to have a `byteWidth` property",
|
|
);
|
|
}
|
|
return new FixedSizeBinary(typeLike.byteWidth);
|
|
}
|
|
|
|
export function sanitizeFixedSizeList(typeLike: object) {
|
|
return sanitizeFixedSizeListWithContext(
|
|
typeLike,
|
|
createSanitizationContext(),
|
|
);
|
|
}
|
|
|
|
function sanitizeFixedSizeListWithContext(
|
|
typeLike: object,
|
|
context: SanitizationContext,
|
|
) {
|
|
if (!("listSize" in typeLike) || typeof typeLike.listSize !== "number") {
|
|
throw Error("Expected a FixedSizeList type to have a `listSize` property");
|
|
}
|
|
if (!("children" in typeLike) || !Array.isArray(typeLike.children)) {
|
|
throw Error(
|
|
"Expected a FixedSizeList type to have an array-like `children` property",
|
|
);
|
|
}
|
|
if (typeLike.children.length !== 1) {
|
|
throw Error("Expected a FixedSizeList type to have exactly one child");
|
|
}
|
|
return new FixedSizeList(
|
|
typeLike.listSize,
|
|
sanitizeFieldWithContext(typeLike.children[0], context),
|
|
);
|
|
}
|
|
|
|
export function sanitizeMap(typeLike: object) {
|
|
return sanitizeMapWithContext(typeLike, createSanitizationContext());
|
|
}
|
|
|
|
function sanitizeMapWithContext(
|
|
typeLike: object,
|
|
context: SanitizationContext,
|
|
) {
|
|
if (!("children" in typeLike) || !Array.isArray(typeLike.children)) {
|
|
throw Error(
|
|
"Expected a Map type to have an array-like `children` property",
|
|
);
|
|
}
|
|
if (!("keysSorted" in typeLike) || typeof typeLike.keysSorted !== "boolean") {
|
|
throw Error("Expected a Map type to have a `keysSorted` property");
|
|
}
|
|
if (typeLike.children.length !== 1) {
|
|
throw Error("Expected a Map type to have exactly one child");
|
|
}
|
|
|
|
return new Map_(
|
|
sanitizeFieldWithContext(typeLike.children[0], context),
|
|
typeLike.keysSorted,
|
|
);
|
|
}
|
|
|
|
export function sanitizeDuration(typeLike: object) {
|
|
if (!("unit" in typeLike) || typeof typeLike.unit !== "number") {
|
|
throw Error("Expected a Duration type to have a `unit` property");
|
|
}
|
|
return new Duration(typeLike.unit);
|
|
}
|
|
|
|
export function sanitizeDictionary(typeLike: object) {
|
|
return sanitizeDictionaryWithContext(typeLike, createSanitizationContext());
|
|
}
|
|
|
|
function sanitizeDictionaryWithContext(
|
|
typeLike: object,
|
|
context: SanitizationContext,
|
|
) {
|
|
if (!("id" in typeLike) || typeof typeLike.id !== "number") {
|
|
throw Error("Expected a Dictionary type to have an `id` property");
|
|
}
|
|
if (!("indices" in typeLike) || typeof typeLike.indices !== "object") {
|
|
throw Error("Expected a Dictionary type to have an `indices` property");
|
|
}
|
|
if (!("dictionary" in typeLike) || typeof typeLike.dictionary !== "object") {
|
|
throw Error("Expected a Dictionary type to have an `dictionary` property");
|
|
}
|
|
if (!("isOrdered" in typeLike) || typeof typeLike.isOrdered !== "boolean") {
|
|
throw Error("Expected a Dictionary type to have an `isOrdered` property");
|
|
}
|
|
return new Dictionary(
|
|
sanitizeTypeWithContext(typeLike.dictionary, context),
|
|
sanitizeTypeWithContext(typeLike.indices, context) as TKeys,
|
|
typeLike.id,
|
|
typeLike.isOrdered,
|
|
);
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: skip
|
|
export function sanitizeType(typeLike: unknown): DataType<any> {
|
|
return sanitizeTypeWithContext(typeLike, createSanitizationContext());
|
|
}
|
|
|
|
function sanitizeTypeWithContext(
|
|
typeLike: unknown,
|
|
context: SanitizationContext,
|
|
): DataType {
|
|
if (typeof typeLike === "string") {
|
|
return dataTypeFromName(typeLike);
|
|
}
|
|
if (typeof typeLike !== "object" || typeLike === null) {
|
|
throw Error("Expected a Type but object was null/undefined");
|
|
}
|
|
const cached = context.types.get(typeLike);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
if (
|
|
!("typeId" in typeLike) ||
|
|
!(
|
|
typeof typeLike.typeId !== "function" ||
|
|
typeof typeLike.typeId !== "number"
|
|
)
|
|
) {
|
|
throw Error("Expected a Type to have a typeId property");
|
|
}
|
|
let typeId: Type;
|
|
if (typeof typeLike.typeId === "function") {
|
|
typeId = (typeLike.typeId as () => unknown)() as Type;
|
|
} else if (typeof typeLike.typeId === "number") {
|
|
typeId = typeLike.typeId as Type;
|
|
} else {
|
|
throw Error("Type's typeId property was not a function or number");
|
|
}
|
|
|
|
const type = sanitizeTypeById(typeLike, typeId, context);
|
|
context.types.set(typeLike, type);
|
|
return type;
|
|
}
|
|
|
|
function sanitizeTypeById(
|
|
typeLike: object,
|
|
typeId: Type,
|
|
context: SanitizationContext,
|
|
): DataType {
|
|
switch (typeId) {
|
|
case Type.NONE:
|
|
throw Error("Received a Type with a typeId of NONE");
|
|
case Type.Null:
|
|
return new Null();
|
|
case Type.Int:
|
|
return sanitizeInt(typeLike);
|
|
case Type.Float:
|
|
return sanitizeFloat(typeLike);
|
|
case Type.Binary:
|
|
return new Binary();
|
|
case Type.Utf8:
|
|
return new Utf8();
|
|
case Type.Bool:
|
|
return new Bool();
|
|
case Type.Decimal:
|
|
return sanitizeDecimal(typeLike);
|
|
case Type.Date:
|
|
return sanitizeDate(typeLike);
|
|
case Type.Time:
|
|
return sanitizeTime(typeLike);
|
|
case Type.Timestamp:
|
|
return sanitizeTimestamp(typeLike);
|
|
case Type.Interval:
|
|
return sanitizeInterval(typeLike);
|
|
case Type.List:
|
|
return sanitizeListWithContext(typeLike, context);
|
|
case Type.Struct:
|
|
return sanitizeStructWithContext(typeLike, context);
|
|
case Type.Union:
|
|
return sanitizeUnionWithContext(typeLike, context);
|
|
case Type.FixedSizeBinary:
|
|
return sanitizeFixedSizeBinary(typeLike);
|
|
case Type.FixedSizeList:
|
|
return sanitizeFixedSizeListWithContext(typeLike, context);
|
|
case Type.Map:
|
|
return sanitizeMapWithContext(typeLike, context);
|
|
case Type.Duration:
|
|
return sanitizeDuration(typeLike);
|
|
case Type.Dictionary:
|
|
return sanitizeDictionaryWithContext(typeLike, context);
|
|
case Type.Int8:
|
|
return new Int8();
|
|
case Type.Int16:
|
|
return new Int16();
|
|
case Type.Int32:
|
|
return new Int32();
|
|
case Type.Int64:
|
|
return new Int64();
|
|
case Type.Uint8:
|
|
return new Uint8();
|
|
case Type.Uint16:
|
|
return new Uint16();
|
|
case Type.Uint32:
|
|
return new Uint32();
|
|
case Type.Uint64:
|
|
return new Uint64();
|
|
case Type.Float16:
|
|
return new Float16();
|
|
case Type.Float32:
|
|
return new Float32();
|
|
case Type.Float64:
|
|
return new Float64();
|
|
case Type.DateMillisecond:
|
|
return new DateMillisecond();
|
|
case Type.DateDay:
|
|
return new DateDay();
|
|
case Type.TimeNanosecond:
|
|
return new TimeNanosecond();
|
|
case Type.TimeMicrosecond:
|
|
return new TimeMicrosecond();
|
|
case Type.TimeMillisecond:
|
|
return new TimeMillisecond();
|
|
case Type.TimeSecond:
|
|
return new TimeSecond();
|
|
case Type.TimestampNanosecond:
|
|
return sanitizeTypedTimestamp(typeLike, TimestampNanosecond);
|
|
case Type.TimestampMicrosecond:
|
|
return sanitizeTypedTimestamp(typeLike, TimestampMicrosecond);
|
|
case Type.TimestampMillisecond:
|
|
return sanitizeTypedTimestamp(typeLike, TimestampMillisecond);
|
|
case Type.TimestampSecond:
|
|
return sanitizeTypedTimestamp(typeLike, TimestampSecond);
|
|
case Type.DenseUnion:
|
|
return sanitizeTypedUnionWithContext(typeLike, DenseUnion, context);
|
|
case Type.SparseUnion:
|
|
return sanitizeTypedUnionWithContext(typeLike, SparseUnion, context);
|
|
case Type.IntervalDayTime:
|
|
return new IntervalDayTime();
|
|
case Type.IntervalYearMonth:
|
|
return new IntervalYearMonth();
|
|
case Type.DurationNanosecond:
|
|
return new DurationNanosecond();
|
|
case Type.DurationMicrosecond:
|
|
return new DurationMicrosecond();
|
|
case Type.DurationMillisecond:
|
|
return new DurationMillisecond();
|
|
case Type.DurationSecond:
|
|
return new DurationSecond();
|
|
default:
|
|
throw new Error("Unrecognized type id in schema: " + typeId);
|
|
}
|
|
}
|
|
|
|
export function sanitizeField(fieldLike: unknown): Field {
|
|
return sanitizeFieldWithContext(fieldLike, createSanitizationContext());
|
|
}
|
|
|
|
function sanitizeFieldWithContext(
|
|
fieldLike: unknown,
|
|
context: SanitizationContext,
|
|
): Field {
|
|
if (fieldLike instanceof Field) {
|
|
return fieldLike;
|
|
}
|
|
if (typeof fieldLike !== "object" || fieldLike === null) {
|
|
throw Error("Expected a Field but object was null/undefined");
|
|
}
|
|
if (
|
|
!("type" in fieldLike) ||
|
|
!("name" in fieldLike) ||
|
|
!("nullable" in fieldLike)
|
|
) {
|
|
throw Error(
|
|
"The field passed in is missing a `type`/`name`/`nullable` property",
|
|
);
|
|
}
|
|
let type: DataType;
|
|
try {
|
|
type = sanitizeTypeWithContext(fieldLike.type, context);
|
|
} catch (error: unknown) {
|
|
throw Error(
|
|
`Unable to sanitize type for field: ${fieldLike.name} due to error: ${error}`,
|
|
{ cause: error },
|
|
);
|
|
}
|
|
const name = fieldLike.name;
|
|
if (!(typeof name === "string")) {
|
|
throw Error("The field passed in had a non-string `name` property");
|
|
}
|
|
const nullable = fieldLike.nullable;
|
|
if (!(typeof nullable === "boolean")) {
|
|
throw Error("The field passed in had a non-boolean `nullable` property");
|
|
}
|
|
let metadata;
|
|
if ("metadata" in fieldLike) {
|
|
metadata = sanitizeMetadata(fieldLike.metadata);
|
|
}
|
|
return new Field(name, type, nullable, metadata);
|
|
}
|
|
|
|
/**
|
|
* Convert something schemaLike into a Schema instance
|
|
*
|
|
* This method is often needed even when the caller is using a Schema
|
|
* instance because they might be using a different instance of apache-arrow
|
|
* than lancedb is using.
|
|
*/
|
|
export function sanitizeSchema(schemaLike: SchemaLike): Schema {
|
|
return sanitizeSchemaWithContext(schemaLike, createSanitizationContext());
|
|
}
|
|
|
|
function sanitizeSchemaWithContext(
|
|
schemaLike: SchemaLike,
|
|
context: SanitizationContext,
|
|
): Schema {
|
|
if (schemaLike instanceof Schema) {
|
|
return schemaLike;
|
|
}
|
|
if (typeof schemaLike !== "object" || schemaLike === null) {
|
|
throw Error("Expected a Schema but object was null/undefined");
|
|
}
|
|
if (!("fields" in schemaLike)) {
|
|
throw Error(
|
|
"The schema passed in does not appear to be a schema (no 'fields' property)",
|
|
);
|
|
}
|
|
let metadata;
|
|
if ("metadata" in schemaLike) {
|
|
metadata = sanitizeMetadata(schemaLike.metadata);
|
|
}
|
|
if (!Array.isArray(schemaLike.fields)) {
|
|
throw Error(
|
|
"The schema passed in had a 'fields' property but it was not an array",
|
|
);
|
|
}
|
|
const sanitizedFields = schemaLike.fields.map((field) =>
|
|
sanitizeFieldWithContext(field, context),
|
|
);
|
|
return new Schema(sanitizedFields, metadata);
|
|
}
|
|
|
|
export function sanitizeTable(tableLike: TableLike): Table {
|
|
if (tableLike instanceof Table) {
|
|
return tableLike;
|
|
}
|
|
if (typeof tableLike !== "object" || tableLike === null) {
|
|
throw Error("Expected a Table but object was null/undefined");
|
|
}
|
|
if (!("schema" in tableLike)) {
|
|
throw Error(
|
|
"The table passed in does not appear to be a table (no 'schema' property)",
|
|
);
|
|
}
|
|
if (!("batches" in tableLike)) {
|
|
throw Error(
|
|
"The table passed in does not appear to be a table (no 'columns' property)",
|
|
);
|
|
}
|
|
const context = createSanitizationContext();
|
|
const schema = sanitizeSchemaWithContext(tableLike.schema, context);
|
|
const batches = tableLike.batches.map((batch) =>
|
|
sanitizeRecordBatch(batch, context),
|
|
);
|
|
return new Table(schema, batches);
|
|
}
|
|
|
|
function sanitizeRecordBatch(
|
|
batchLike: RecordBatchLike,
|
|
context: SanitizationContext,
|
|
): RecordBatch {
|
|
if (batchLike instanceof RecordBatch) {
|
|
return batchLike;
|
|
}
|
|
if (typeof batchLike !== "object" || batchLike === null) {
|
|
throw Error("Expected a RecordBatch but object was null/undefined");
|
|
}
|
|
if (!("schema" in batchLike)) {
|
|
throw Error(
|
|
"The record batch passed in does not appear to be a record batch (no 'schema' property)",
|
|
);
|
|
}
|
|
if (!("data" in batchLike)) {
|
|
throw Error(
|
|
"The record batch passed in does not appear to be a record batch (no 'data' property)",
|
|
);
|
|
}
|
|
const schema = sanitizeSchemaWithContext(batchLike.schema, context);
|
|
const data = sanitizeData(batchLike.data, context) as Data<Struct>;
|
|
return new RecordBatch(schema, data);
|
|
}
|
|
|
|
type DictionaryVectorLike = {
|
|
data: readonly DataLike[];
|
|
};
|
|
|
|
type DictionaryDataLike = DataLike & {
|
|
dictionary?: DictionaryVectorLike;
|
|
};
|
|
|
|
function sanitizeData(
|
|
dataLike: DataLike,
|
|
context: SanitizationContext,
|
|
): Data<DataType> {
|
|
if (dataLike instanceof Data) {
|
|
return dataLike;
|
|
}
|
|
const cachedData = context.data.get(dataLike);
|
|
if (cachedData !== undefined) {
|
|
return cachedData;
|
|
}
|
|
const dictionaryLike = (dataLike as DictionaryDataLike).dictionary;
|
|
let dictionary: Vector | undefined;
|
|
if (dictionaryLike !== undefined) {
|
|
dictionary = context.vectors.get(dictionaryLike);
|
|
if (dictionary === undefined) {
|
|
dictionary = new Vector(
|
|
dictionaryLike.data.map((data) => sanitizeData(data, context)),
|
|
);
|
|
context.vectors.set(dictionaryLike, dictionary);
|
|
}
|
|
}
|
|
const data = new Data(
|
|
sanitizeTypeWithContext(dataLike.type, context),
|
|
dataLike.offset,
|
|
dataLike.length,
|
|
dataLike.nullCount,
|
|
{
|
|
[BufferType.OFFSET]: dataLike.valueOffsets,
|
|
[BufferType.DATA]: dataLike.values,
|
|
[BufferType.VALIDITY]: dataLike.nullBitmap,
|
|
[BufferType.TYPE]: dataLike.typeIds,
|
|
},
|
|
dataLike.children.map((child) => sanitizeData(child, context)),
|
|
dictionary,
|
|
);
|
|
context.data.set(dataLike, data);
|
|
return data;
|
|
}
|
|
|
|
const constructorsByTypeName = {
|
|
null: () => new Null(),
|
|
binary: () => new Binary(),
|
|
utf8: () => new Utf8(),
|
|
bool: () => new Bool(),
|
|
int8: () => new Int8(),
|
|
int16: () => new Int16(),
|
|
int32: () => new Int32(),
|
|
int64: () => new Int64(),
|
|
uint8: () => new Uint8(),
|
|
uint16: () => new Uint16(),
|
|
uint32: () => new Uint32(),
|
|
uint64: () => new Uint64(),
|
|
float16: () => new Float16(),
|
|
float32: () => new Float32(),
|
|
float64: () => new Float64(),
|
|
datemillisecond: () => new DateMillisecond(),
|
|
dateday: () => new DateDay(),
|
|
timenanosecond: () => new TimeNanosecond(),
|
|
timemicrosecond: () => new TimeMicrosecond(),
|
|
timemillisecond: () => new TimeMillisecond(),
|
|
timesecond: () => new TimeSecond(),
|
|
intervaldaytime: () => new IntervalDayTime(),
|
|
intervalyearmonth: () => new IntervalYearMonth(),
|
|
durationnanosecond: () => new DurationNanosecond(),
|
|
durationmicrosecond: () => new DurationMicrosecond(),
|
|
durationmillisecond: () => new DurationMillisecond(),
|
|
durationsecond: () => new DurationSecond(),
|
|
} as const;
|
|
|
|
type MappableTypeName = keyof typeof constructorsByTypeName;
|
|
|
|
export function dataTypeFromName(typeName: string): DataType {
|
|
const normalizedTypeName = typeName.toLowerCase() as MappableTypeName;
|
|
const _constructor = constructorsByTypeName[normalizedTypeName];
|
|
|
|
if (!_constructor) {
|
|
throw new Error("Unrecognized type name in schema: " + typeName);
|
|
}
|
|
|
|
return _constructor();
|
|
}
|