// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import { DataType, TypeMap } from './type.js'; export class Schema { public readonly fields: Field[]; public readonly metadata: Map; public readonly dictionaries: Map; constructor( fields: Field[] = [], metadata?: Map | null, dictionaries?: Map | null) { this.fields = (fields || []) as Field[]; this.metadata = metadata || new Map(); if (!dictionaries) { dictionaries = generateDictionaryMap(fields); } this.dictionaries = dictionaries; } public get [Symbol.toStringTag]() { return 'Schema'; } public get names(): (keyof T)[] { return this.fields.map((f) => f.name); } public toString() { return `Schema<{ ${this.fields.map((f, i) => `${i}: ${f}`).join(', ')} }>`; } /** * Construct a new Schema containing only specified fields. * * @param fieldNames Names of fields to keep. * @returns A new Schema of fields matching the specified names. */ public select(fieldNames: K[]) { const names = new Set(fieldNames); const fields = this.fields.filter((f) => names.has(f.name)) as Field[]; return new Schema<{ [P in K]: T[P] }>(fields, this.metadata); } /** * Construct a new Schema containing only fields at the specified indices. * * @param fieldIndices Indices of fields to keep. * @returns A new Schema of fields at the specified indices. */ public selectAt(fieldIndices: number[]) { const fields = fieldIndices.map((i) => this.fields[i]).filter(Boolean) as Field[]; return new Schema(fields, this.metadata); } public assign(schema: Schema): Schema; public assign(...fields: (Field | Field[])[]): Schema; public assign(...args: (Schema | Field | Field[])[]) { const other = (args[0] instanceof Schema ? args[0] as Schema : Array.isArray(args[0]) ? new Schema([]>args[0]) : new Schema([]>args)); const curFields = [...this.fields] as Field[]; const metadata = mergeMaps(mergeMaps(new Map(), this.metadata), other.metadata); const newFields = other.fields.filter((f2) => { const i = curFields.findIndex((f) => f.name === f2.name); return ~i ? (curFields[i] = f2.clone({ metadata: mergeMaps(mergeMaps(new Map(), curFields[i].metadata), f2.metadata) })) && false : true; }) as Field[]; const newDictionaries = generateDictionaryMap(newFields, new Map()); return new Schema( [...curFields, ...newFields], metadata, new Map([...this.dictionaries, ...newDictionaries]) ); } } // Add these here so they're picked up by the externs creator // in the build, and closure-compiler doesn't minify them away (Schema.prototype as any).fields = null; (Schema.prototype as any).metadata = null; (Schema.prototype as any).dictionaries = null; export class Field { public static new(props: { name: string | number; type: T; nullable?: boolean; metadata?: Map | null }): Field; public static new(name: string | number | Field, type: T, nullable?: boolean, metadata?: Map | null): Field; /** @nocollapse */ public static new(...args: any[]) { let [name, type, nullable, metadata] = args; if (args[0] && typeof args[0] === 'object') { ({ name } = args[0]); (type === undefined) && (type = args[0].type); (nullable === undefined) && (nullable = args[0].nullable); (metadata === undefined) && (metadata = args[0].metadata); } return new Field(`${name}`, type, nullable, metadata); } public readonly type: T; public readonly name: string; public readonly nullable: boolean; public readonly metadata: Map; constructor(name: string, type: T, nullable = false, metadata?: Map | null) { this.name = name; this.type = type; this.nullable = nullable; this.metadata = metadata || new Map(); } public get typeId() { return this.type.typeId; } public get [Symbol.toStringTag]() { return 'Field'; } public toString() { return `${this.name}: ${this.type}`; } public clone(props: { name?: string | number; type?: R; nullable?: boolean; metadata?: Map | null }): Field; public clone(name?: string | number | Field, type?: R, nullable?: boolean, metadata?: Map | null): Field; public clone(...args: any[]) { let [name, type, nullable, metadata] = args; (!args[0] || typeof args[0] !== 'object') ? ([name = this.name, type = this.type, nullable = this.nullable, metadata = this.metadata] = args) : ({ name = this.name, type = this.type, nullable = this.nullable, metadata = this.metadata } = args[0]); return Field.new(name, type, nullable, metadata); } } // Add these here so they're picked up by the externs creator // in the build, and closure-compiler doesn't minify them away (Field.prototype as any).type = null; (Field.prototype as any).name = null; (Field.prototype as any).nullable = null; (Field.prototype as any).metadata = null; /** @ignore */ function mergeMaps(m1?: Map | null, m2?: Map | null): Map { return new Map([...(m1 || new Map()), ...(m2 || new Map())]); } /** @ignore */ function generateDictionaryMap(fields: Field[], dictionaries = new Map()): Map { for (let i = -1, n = fields.length; ++i < n;) { const field = fields[i]; const type = field.type; if (DataType.isDictionary(type)) { if (!dictionaries.has(type.id)) { dictionaries.set(type.id, type.dictionary); } else if (dictionaries.get(type.id) !== type.dictionary) { throw new Error(`Cannot create Schema containing two different dictionaries with the same Id`); } } if (type.children && type.children.length > 0) { generateDictionaryMap(type.children, dictionaries); } } return dictionaries; }