mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-04 12:38:38 +00:00
9d3962686e
## Summary - accept genuine Arrow metadata maps created in another JavaScript realm - validate every metadata entry and clone it into a local Map - cover an Arrow 15 VM-realm table through the public fromDataToBuffer boundary - retain structural typing for nested and dictionary Arrow data ## Root cause The sanitizer used a local-realm instanceof Map check for schema and field metadata. A genuine Map created in another JavaScript realm has the required internal Map state but fails that identity check, so fromDataToBuffer rejected the foreign table before serializing its rows. ## Scope This fixes the distinct JavaScript-realm sanitizer failure identified during review. It does not establish the cause of the S3/compaction panic reported in #1525, so that issue remains open. ## Validation - pnpm test --runInBand (707 passed, 5 skipped) - pnpm test --runInBand __test__/arrow.test.ts (189 passed) - pnpm build - pnpm lint - pnpm run docs Related to #1525 <!-- lance-gatekeeper-fix:v1 agent=b522628ad3bae914eb7266ccd899d508 generation=1 --> --------- Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com> Co-authored-by: Xuanwo <github@xuanwo.io>
789 lines
23 KiB
TypeScript
789 lines
23 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;
|
|
}
|
|
|
|
let entries: IterableIterator<[unknown, unknown]>;
|
|
try {
|
|
entries = Map.prototype.entries.call(metadataLike);
|
|
} catch {
|
|
throw Error("Expected metadata, if present, to be a Map<string, string>");
|
|
}
|
|
|
|
const metadata = new Map<string, string>();
|
|
for (const [key, value] of entries) {
|
|
if (typeof key !== "string" || typeof value !== "string") {
|
|
throw Error(
|
|
"Expected metadata, if present, to be a Map<string, string> but it had non-string keys or values",
|
|
);
|
|
}
|
|
metadata.set(key, value);
|
|
}
|
|
return metadata;
|
|
}
|
|
|
|
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();
|
|
}
|