diff --git a/js/perf/index.ts b/js/perf/index.ts index 2869470b469..d96d73429be 100755 --- a/js/perf/index.ts +++ b/js/perf/index.ts @@ -205,6 +205,10 @@ for (const { name, table, counts } of config) { table.toArray(); }), + b.add(`toArrayView, dataset: ${name}, numRows: ${formatNumber(table.numRows)}`, () => { + table.toArrayView(); + }), + b.add(`get, dataset: ${name}, numRows: ${formatNumber(table.numRows)}`, () => { for (let i = -1, n = table.numRows; ++i < n;) { table.get(i); diff --git a/js/src/row/struct.ts b/js/src/row/struct.ts index bc3869cb8d0..074ec91fd64 100644 --- a/js/src/row/struct.ts +++ b/js/src/row/struct.ts @@ -39,7 +39,7 @@ export class StructRow { constructor(parent: Data>, rowIndex: number) { this[kParent] = parent; this[kRowIndex] = rowIndex; - return new Proxy(this, new StructRowProxyHandler()); + return new Proxy(this, structRowProxyHandler); } public toArray() { return Object.values(this.toJSON()); } @@ -157,3 +157,5 @@ class StructRowProxyHandler implements ProxyHandler { return [...this]; } + /** + * Return a JavaScript Array view of the Table rows. + * + * It is a lightweight read-only proxy that delegates to the table. Accessing elements has some + * overhead compared to the regular array returned by `toArray()` because of this indirection, + * but it avoids potentially large memory allocation. + * + * @returns An Array proxy to the Table rows. + */ + public toArrayView() { + return new Proxy([] as Array['TValue']>, new TableArrayProxyHandler(this)); + } + /** * Returns a string representation of the Table rows. * * @returns A string representation of the Table rows. */ public toString() { - return `[\n ${this.toArray().join(',\n ')}\n]`; + return `[\n ${this.toArrayView().join(',\n ')}\n]`; } /** @@ -444,3 +457,77 @@ export function tableFromArrays(vecs); } + +class TableArrayProxyHandler implements ProxyHandler['TValue']>> { + table: Table; + + constructor(table: Table) { + this.table = table; + } + + // Traps that aren't implemented: + // - apply + // - construct + // - defineProperty + // - deleteProperty + // - getPrototypeOf + // - isExtensible + // - preventExtensions + // - set + // - setPrototypeOf + + get(target: Array['TValue']>, p: string | symbol, receiver: any): any { + if (typeof p === 'string') { + const i = Number(p); + if (Number.isInteger(i)) { + return this.table.get(i); + } + if (p === 'at') { + return (i: number): Struct['TValue'] | undefined => { + return this.table.at(i) ?? undefined; + }; + } + if (p === 'length') { + return this.table.numRows; + } + } else if (p === Symbol('keys')) { + const end = this.table.numRows; + return function * () { + let i = 0; + while(i < end) { + yield i++; + } + return; + }; + } + return Reflect.get(target, p, receiver); + } + + getOwnPropertyDescriptor(target: Array['TValue']>, p: string | symbol): PropertyDescriptor | undefined { + if (typeof p === 'string') { + const i = Number(p); + if (Number.isInteger(i) && i >= 0 && i < this.table.numRows) { + return { enumerable: true, configurable: true }; + } + } + return Reflect.getOwnPropertyDescriptor(target, p); + } + + has(target: Array['TValue']>, p: string | symbol): boolean { + if (typeof p === 'string') { + const i = Number(p); + if (Number.isInteger(i)) { + return i >= 0 && i < this.table.numRows; + } + } + return Reflect.has(target, p); + } + + ownKeys(_target: Array['TValue']>): ArrayLike { + // Can be expensive as we allocate an array with all index numbers as strings + const keys = Array.from({length: this.table.numRows}, (_, i) => String(i)); + keys.push('length'); + return keys; + } +} + diff --git a/js/test/unit/table/table-test.ts b/js/test/unit/table/table-test.ts index 01af4090987..908526892a4 100644 --- a/js/test/unit/table/table-test.ts +++ b/js/test/unit/table/table-test.ts @@ -79,3 +79,61 @@ describe('tableFromJSON()', () => { expect(table.getChild('c')!.type).toBeInstanceOf(Dictionary); }); }); + +describe('table views', () => { + const table = tableFromJSON([{ + a: 42, + b: true, + c: 'foo', + }, { + a: 12, + b: false, + c: 'bar', + }]); + + function checkArrayValues(arr: Array) { + expect(arr).toHaveLength(2); + expect(arr[0].a).toBe(42); + expect(arr[0].b).toBe(true); + expect(arr[0].c).toBe('foo'); + expect(arr[1].a).toBe(12); + expect(arr[1].b).toBe(false); + expect(arr[1].c).toBe('bar'); + } + + function checkArray(arr: Array) { + test('Wrapper', () => checkArrayValues(arr)); + + test('Iterator', () => { + const arr2 = []; + for (let item of arr) { + arr2.push(item); + } + checkArrayValues(arr); + }); + + test('Array index', () => { + const arr2 = new Array(arr.length); + for (let i = 0; i < arr2.length; i++) { + arr2[i] = arr[i]; + } + checkArrayValues(arr2); + }); + + test('Keys', () => { + const arr2: any[] = new Array(arr.length); + let keys = Object.keys(arr); + for (let k in keys) { + arr2[k] = arr[k]; + } + checkArrayValues(arr2); + }); + } + + describe('table.toArray()', () => { + checkArray(table.toArray()); + }); + describe('table.toArrayView()', () => { + checkArray(table.toArrayView()); + }); +});