From 5098216306048d6ebe9e24ccc7aef6e09985a686 Mon Sep 17 00:00:00 2001 From: Kent Wu Date: Sun, 16 Nov 2025 12:58:41 -0500 Subject: [PATCH] feat: Add Schema and Field withMetadata methods --- src/schema.ts | 30 ++++++++++++++++++ test/unit/schema-tests.ts | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 test/unit/schema-tests.ts diff --git a/src/schema.ts b/src/schema.ts index 2eb33b78..3b7f76a9 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -69,6 +69,17 @@ export class Schema { return new Schema(fields, this.metadata); } + /** + * Create a new Schema with replaced metadata. + * + * @param metadata Replacement metadata entries. Pass `null` to clear. + */ + public withMetadata(metadata?: Map | Record | null): Schema { + if (metadata === undefined) { return this; } + const next = metadata === null ? new Map() : toMetadataMap(metadata); + return new Schema(this.fields, next, this.dictionaries, this.metadataVersion); + } + public assign(schema: Schema): Schema; public assign(...fields: (Field | Field[])[]): Schema; public assign(...args: (Schema | Field | Field[])[]) { @@ -143,6 +154,18 @@ export class Field { : ({ name = this.name, type = this.type, nullable = this.nullable, metadata = this.metadata } = args[0]); return Field.new(name, type, nullable, metadata); } + + /** + * Create a new Field with replaced metadata. Accepts either a Map or a plain object. + * Pass `null` to clear existing metadata. + * + * @param metadata Replacement metadata entries. + */ + public withMetadata(metadata?: Map | Record | null): Field { + if (metadata === undefined) { return this; } + const next = metadata === null ? new Map() : toMetadataMap(metadata); + return new Field(this.name, this.type, this.nullable, next); + } } // Add these here so they're picked up by the externs creator @@ -157,6 +180,13 @@ function mergeMaps(m1?: Map | null, m2?: Map return new Map([...(m1 || new Map()), ...(m2 || new Map())]); } +/** @ignore */ +function toMetadataMap(metadata: Map | Record) { + return metadata instanceof Map + ? new Map(metadata) + : new Map(Object.entries(metadata)); +} + /** @ignore */ function generateDictionaryMap(fields: Field[], dictionaries = new Map()): Map { diff --git a/test/unit/schema-tests.ts b/test/unit/schema-tests.ts new file mode 100644 index 00000000..93216529 --- /dev/null +++ b/test/unit/schema-tests.ts @@ -0,0 +1,66 @@ +// 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 '../jest-extensions.js'; +import { Field, Int32, Schema } from 'apache-arrow'; + +describe('Field.withMetadata', () => { + test('replaces metadata from plain objects', () => { + const field = new Field('col', new Int32(), true, new Map([['foo', 'bar']])); + const updated = field.withMetadata({ baz: 'qux' }); + expect(updated).not.toBe(field); + expect(updated.metadata.get('foo')).toBeUndefined(); + expect(updated.metadata.get('baz')).toBe('qux'); + }); + + test('replaces metadata from Maps', () => { + const field = new Field('col', new Int32(), true, new Map([['foo', 'bar']])); + const updated = field.withMetadata(new Map([['foo', 'baz']])); + expect(updated.metadata.get('foo')).toBe('baz'); + expect(updated.metadata.size).toBe(1); + }); + + test('clears metadata when null is passed', () => { + const field = new Field('col', new Int32(), true, new Map([['foo', 'bar']])); + const updated = field.withMetadata(null); + expect(updated.metadata.size).toBe(0); + }); +}); + +describe('Schema.withMetadata', () => { + test('replaces metadata from plain objects', () => { + const schema = new Schema([new Field('col', new Int32())], new Map([['foo', 'bar']])); + const updated = schema.withMetadata({ baz: 'qux' }); + expect(updated).not.toBe(schema); + expect(schema.metadata.get('baz')).toBeUndefined(); + expect(updated.metadata.get('foo')).toBeUndefined(); + expect(updated.metadata.get('baz')).toBe('qux'); + }); + + test('replaces metadata from Maps', () => { + const schema = new Schema([new Field('col', new Int32())], new Map([['foo', 'bar']])); + const updated = schema.withMetadata(new Map([['foo', 'baz']])); + expect(updated.metadata.get('foo')).toBe('baz'); + expect(updated.metadata.size).toBe(1); + }); + + test('clears metadata when null is passed', () => { + const schema = new Schema([new Field('col', new Int32())], new Map([['foo', 'bar']])); + const updated = schema.withMetadata(null); + expect(updated.metadata.size).toBe(0); + }); +});