• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 25990278810

17 May 2026 12:01PM UTC coverage: 75.092% (+0.2%) from 74.881%
25990278810

push

github

web-flow
feat: Columnar GPU-data stack (#2616)

6711 of 10084 branches covered (66.55%)

Branch coverage included in aggregate %.

625 of 865 new or added lines in 22 files covered. (72.25%)

1 existing line in 1 file now uncovered.

14631 of 18337 relevant lines covered (79.79%)

792.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.2
/modules/arrow/src/arrow/plain-gpu-table.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {Buffer, Device, type BufferLayout, type ShaderLayout} from '@luma.gl/core';
6
import {DynamicBuffer} from '@luma.gl/engine';
7
import * as arrow from 'apache-arrow';
8
import {getArrowFieldByPath, getArrowVectorByPath} from './arrow-paths';
9
import {getArrowBufferLayout, type ArrowVertexFormatOptions} from './arrow-shader-layout';
10
import type {AttributeArrowType} from './arrow-types';
11
import {GPUVector, type GPUVectorProps} from './arrow-gpu-vector';
12
import {GPURecordBatch, type GPURecordBatchAppendableProps} from './arrow-gpu-record-batch';
13
import {createGPUVectorCollection} from './arrow-gpu-vector-collection';
14
import {getArrowTypeByteStride} from './arrow-gpu-data';
15

16
/** Options for creating GPU buffers from shader-compatible Arrow table columns. */
17
export type GPUTableProps = ArrowVertexFormatOptions & {
18
  /** Shader layout that selects which Arrow columns should be uploaded. */
19
  shaderLayout: ShaderLayout;
20
  /** Maps shader attribute names to Arrow column paths. Defaults to using the attribute name. */
21
  arrowPaths?: Record<string, string>;
22
  /** Buffer props applied to every Arrow-backed GPU vector. */
23
  bufferProps?: GPUVectorProps;
24
};
25

26
/** Options for constructing an {@link GPUTable} from existing GPU vectors. */
27
export type GPUTableFromVectorsProps = {
28
  /** GPU vectors keyed by name, or a list of named GPU vectors. */
29
  vectors: Record<string, GPUVector> | GPUVector[];
30
  /** Optional table-level schema metadata. */
31
  metadata?: Map<string, string>;
32
  /** Number of null rows in the generated GPU table. */
33
  nullCount?: number;
34
};
35

36
/** Options for constructing a table with one appendable trailing GPU batch. */
37
export type GPUTableAppendableProps = Omit<GPURecordBatchAppendableProps, 'type'> & {
38
  /** Discriminator for appendable table construction. */
39
  type: 'appendable';
40
};
41

42
/** Options for replacing preserved GPU batches with larger packed batches. */
43
export type GPUTablePackBatchesOptions = {
44
  /** Greedily merge adjacent batches until each emitted batch reaches this row count. */
45
  minBatchSize?: number;
46
};
47

48
/** Half-open batch index range used by {@link GPUTable.detachBatches}. */
49
export type GPUTableDetachBatchesOptions = {
50
  /** First batch index to detach. Defaults to `0`. */
51
  first?: number;
52
  /** Batch index after the last detached batch. Defaults to `batches.length`. */
53
  last?: number;
54
};
55

56
/**
57
 * GPU memory and Arrow schema metadata derived from selected Arrow table columns.
58
 *
59
 * The Arrow table is a construction input only. GPUTable does not retain
60
 * the source table; it owns GPU buffers, a BufferLayout, and a GPU-facing Arrow
61
 * schema that describes the selected columns.
62
 */
63
export class GPUTable {
64
  /** GPU-facing schema for the selected shader attribute columns. */
65
  schema: arrow.Schema;
66
  /** Number of rows in the source Arrow table at construction time. */
67
  numRows: number;
68
  /** Number of selected GPU columns in {@link schema}. */
69
  numCols: number;
70
  /** Number of null rows in the source Arrow table at construction time. */
71
  nullCount: number;
72
  /** Buffer layout derived from the selected Arrow columns and shader layout. */
73
  readonly bufferLayout: BufferLayout[] = [];
38✔
74
  /** GPU vectors keyed by shader attribute name. */
75
  readonly gpuVectors: Record<string, GPUVector> = {};
38✔
76
  /** Model-ready attribute buffers keyed by shader attribute name. */
77
  readonly attributes: Record<string, Buffer | DynamicBuffer> = {};
38✔
78
  /** Model-ready storage buffers keyed by shader binding name. */
79
  readonly bindings: Record<string, Buffer | DynamicBuffer> = {};
38✔
80
  /** GPU record batches preserving source Arrow table batch boundaries. */
81
  readonly batches: GPURecordBatch[] = [];
38✔
82

83
  /** Creates GPU buffers and a GPU-facing schema from an Arrow table. */
84
  constructor(device: Device, table: arrow.Table, props: GPUTableProps);
85
  /** Creates a GPU-facing table from existing named GPU vectors. */
86
  constructor(props: GPUTableFromVectorsProps);
87
  /** Creates an empty table with one appendable trailing GPU batch. */
88
  constructor(props: GPUTableAppendableProps);
89
  constructor(
90
    deviceOrProps: Device | GPUTableFromVectorsProps | GPUTableAppendableProps,
91
    table?: arrow.Table,
92
    props?: GPUTableProps
93
  ) {
94
    if (!(deviceOrProps instanceof Device)) {
38✔
95
      if ('type' in deviceOrProps && deviceOrProps.type === 'appendable') {
13✔
96
        const batch = new GPURecordBatch({
11✔
97
          ...deviceOrProps,
98
          type: 'appendable'
99
        });
100
        this.numRows = 0;
11✔
101
        this.nullCount = 0;
11✔
102
        this.schema = new arrow.Schema(batch.schema.fields, new Map(deviceOrProps.schema.metadata));
11✔
103
        this.numCols = batch.numCols;
11✔
104
        this.bufferLayout.push(...batch.bufferLayout);
11✔
105
        this.batches.push(batch);
11✔
106
        this.rebuildAggregateVectors();
11✔
107
        return;
11✔
108
      }
109

110
      const {vectors, metadata, nullCount = 0} = deviceOrProps;
2✔
111
      const vectorCollection = createGPUVectorCollection({
2✔
112
        ownerName: 'GPUTable',
113
        vectors
114
      });
115

116
      this.numRows = vectorCollection.numRows;
2✔
117
      this.nullCount = nullCount;
2✔
118
      Object.assign(this.gpuVectors, vectorCollection.gpuVectors);
2✔
119
      Object.assign(this.attributes, vectorCollection.attributes);
2✔
120
      this.bufferLayout.push(...vectorCollection.bufferLayout);
2✔
121

122
      this.schema = new arrow.Schema(vectorCollection.fields, metadata);
2✔
123
      this.numCols = vectorCollection.fields.length;
2✔
124
      this.batches.push(
2✔
125
        new GPURecordBatch({
126
          vectors: vectorCollection.gpuVectors,
127
          bufferLayout: this.bufferLayout,
128
          fields: vectorCollection.fields,
129
          metadata,
130
          nullCount
131
        })
132
      );
133
      return;
2✔
134
    }
135

136
    const device = deviceOrProps;
25✔
137
    props = props!;
25✔
138
    table = table!;
25✔
139
    this.numRows = table.numRows;
25✔
140
    this.nullCount = table.nullCount;
25✔
141
    this.bufferLayout = getArrowBufferLayout(props.shaderLayout, {
25✔
142
      arrowTable: table,
143
      arrowPaths: props.arrowPaths,
144
      allowWebGLOnlyFormats: props.allowWebGLOnlyFormats
145
    });
146

147
    const fields: arrow.Field[] = [];
25✔
148
    for (const bufferLayout of this.bufferLayout) {
25✔
149
      const arrowPath = props.arrowPaths?.[bufferLayout.name] || bufferLayout.name;
46✔
150
      const vector = getArrowVectorByPath(table, arrowPath);
46✔
151
      const sourceField = getArrowFieldByPath(table, arrowPath);
46✔
152
      const field = new arrow.Field(
46✔
153
        bufferLayout.name,
154
        vector.type,
155
        sourceField.nullable,
156
        new Map(sourceField.metadata)
157
      );
158
      fields.push(field);
46✔
159
    }
160

161
    this.schema = new arrow.Schema(fields, new Map(table.schema.metadata));
25✔
162
    this.numCols = this.schema.fields.length;
25✔
163
    for (const recordBatch of table.batches) {
25✔
164
      this.batches.push(new GPURecordBatch(device, recordBatch, props));
33✔
165
    }
166
    const firstBatch = this.batches[0];
25✔
167
    if (firstBatch) {
25!
168
      // Storage-backed table columns are selected at the batch upload layer,
169
      // so adopt that complete selected schema once at least one batch exists.
170
      this.schema = new arrow.Schema(firstBatch.schema.fields, new Map(table.schema.metadata));
25✔
171
      this.numCols = this.schema.fields.length;
25✔
172
    }
173
    this.rebuildAggregateVectors();
25✔
174
  }
175

176
  /**
177
   * Replaces preserved GPU batches with fewer packed batches.
178
   *
179
   * New packed batches own their destination buffers. Superseded source batches
180
   * are destroyed after GPU copies are submitted, which only releases buffers
181
   * owned by those source vectors.
182
   */
183
  packBatches(options: GPUTablePackBatchesOptions = {}): this {
3✔
184
    if (this.batches.length <= 1) {
3!
185
      return this;
×
186
    }
187

188
    const batchGroups = createArrowGPUPackGroups(this.batches, options.minBatchSize);
3✔
189
    const nextBatches: GPURecordBatch[] = [];
3✔
190
    const supersededBatches: GPURecordBatch[] = [];
3✔
191

192
    for (const batchGroup of batchGroups) {
3✔
193
      if (batchGroup.length === 1) {
4✔
194
        nextBatches.push(batchGroup[0]);
1✔
195
        continue;
1✔
196
      }
197
      nextBatches.push(createPackedArrowGPURecordBatch(batchGroup, this.bufferLayout, this.schema));
3✔
198
      supersededBatches.push(...batchGroup);
3✔
199
    }
200

201
    if (supersededBatches.length === 0) {
3!
202
      return this;
×
203
    }
204

205
    this.batches.splice(0, this.batches.length, ...nextBatches);
3✔
206
    this.rebuildAggregateVectors();
3✔
207
    for (const batch of supersededBatches) {
3✔
208
      batch.destroy();
6✔
209
    }
210
    return this;
3✔
211
  }
212

213
  /**
214
   * Adds one already-created GPU record batch to this table.
215
   *
216
   * Ownership remains on the supplied batch and its vectors. The table only
217
   * incorporates that batch into its aggregate metadata and will later call the
218
   * batch's regular `destroy()` path when the table itself is destroyed.
219
   */
220
  addBatch(batch: GPURecordBatch): this {
221
    assertCompatibleArrowGPURecordBatch(this, batch);
1✔
222
    this.batches.push(batch);
1✔
223
    this.numRows += batch.numRows;
1✔
224
    this.nullCount += batch.nullCount;
1✔
225
    this.rebuildAggregateVectors();
1✔
226
    return this;
1✔
227
  }
228

229
  /**
230
   * Appends Arrow rows into the current trailing appendable GPU batch.
231
   *
232
   * Arrow tables are consumed batch-by-batch so one mutable trailing GPU batch
233
   * can absorb synchronous incremental table arrivals without changing table
234
   * ownership.
235
   */
236
  addToLastBatch(recordBatchOrTable: arrow.RecordBatch | arrow.Table): this {
237
    if (recordBatchOrTable instanceof arrow.Table) {
20✔
238
      for (const recordBatch of recordBatchOrTable.batches) {
2✔
239
        this.addToLastBatch(recordBatch);
3✔
240
      }
241
      return this;
2✔
242
    }
243

244
    const lastBatch = this.batches[this.batches.length - 1];
18✔
245
    if (!lastBatch) {
18!
246
      throw new Error('GPUTable.addToLastBatch() requires an existing trailing batch');
×
247
    }
248
    lastBatch.addToLastBatch(recordBatchOrTable);
18✔
249
    this.numRows += recordBatchOrTable.numRows;
18✔
250
    this.nullCount += recordBatchOrTable.nullCount;
18✔
251
    this.rebuildAggregateVectors();
18✔
252
    return this;
18✔
253
  }
254

255
  /** Clears only the current trailing appendable GPU batch while retaining its allocations. */
256
  resetLastBatch(): this {
257
    const lastBatch = this.batches[this.batches.length - 1];
2✔
258
    if (!lastBatch) {
2!
259
      throw new Error('GPUTable.resetLastBatch() requires an existing trailing batch');
×
260
    }
261
    this.numRows -= lastBatch.numRows;
2✔
262
    this.nullCount -= lastBatch.nullCount;
2✔
263
    lastBatch.resetLastBatch();
2✔
264
    this.rebuildAggregateVectors();
2✔
265
    return this;
2✔
266
  }
267

268
  /**
269
   * Keeps only the requested columns and destroys the dropped batch-local vectors.
270
   *
271
   * Use {@link detachVector} first when a removed column should stay alive.
272
   */
273
  select(...columnNames: string[]): this {
274
    const selectedColumnNames = normalizeSelectedColumnNames(this, columnNames);
1✔
275
    const selectedColumnSet = new Set(selectedColumnNames);
1✔
276

277
    for (const batch of this.batches) {
1✔
278
      const droppedVectors = Object.entries(batch.gpuVectors)
1✔
279
        .filter(([name]) => !selectedColumnSet.has(name))
2✔
280
        .map(([, vector]) => vector);
1✔
281
      for (const vector of droppedVectors) {
1✔
282
        vector.destroy();
1✔
283
      }
284
      rebuildArrowGPURecordBatchColumns(batch, selectedColumnNames);
1✔
285
    }
286

287
    rebuildArrowGPUTableColumns(this, selectedColumnNames);
1✔
288
    this.rebuildAggregateVectors();
1✔
289
    return this;
1✔
290
  }
291

292
  /**
293
   * Removes one column and returns an aggregate GPU vector that now owns its detached batch vectors.
294
   */
295
  detachVector(columnName: string): GPUVector {
296
    assertArrowGPUTableColumn(this, columnName);
1✔
297
    const detachedVectors = this.batches.map(batch => batch.gpuVectors[columnName]);
2✔
298
    const firstVector = detachedVectors[0];
1✔
299
    if (!firstVector) {
1!
300
      throw new Error(`GPUTable.detachVector() column "${columnName}" has no GPU data`);
×
301
    }
302

303
    const detachedVector = new GPUVector({
1✔
304
      type: 'data',
305
      name: columnName,
306
      arrowType: firstVector.type,
307
      data: [...firstVector.data],
308
      byteStride: firstVector.byteStride,
309
      bufferLayout: firstVector.bufferLayout
310
    });
311
    for (const batchVector of detachedVectors.slice(1)) {
1✔
312
      for (const data of batchVector.data) {
1✔
313
        detachedVector.addData(data);
1✔
314
      }
315
    }
316
    detachedVector.retainOwnedVectors(detachedVectors);
1✔
317

318
    const remainingColumnNames = this.bufferLayout
1✔
319
      .map(layout => layout.name)
2✔
320
      .filter(name => name !== columnName);
2✔
321
    for (const batch of this.batches) {
1✔
322
      rebuildArrowGPURecordBatchColumns(batch, remainingColumnNames);
2✔
323
    }
324
    rebuildArrowGPUTableColumns(this, remainingColumnNames);
1✔
325
    this.rebuildAggregateVectors();
1✔
326
    return detachedVector;
1✔
327
  }
328

329
  /**
330
   * Removes and returns a half-open range of GPU record batches.
331
   *
332
   * Detached batches retain their own vector ownership and are no longer
333
   * destroyed by this table.
334
   */
335
  detachBatches(options: GPUTableDetachBatchesOptions = {}): GPURecordBatch[] {
1✔
336
    const first = options.first ?? 0;
1!
337
    const last = options.last ?? this.batches.length;
1!
338
    if (
1!
339
      !Number.isInteger(first) ||
5✔
340
      !Number.isInteger(last) ||
341
      first < 0 ||
342
      last < first ||
343
      last > this.batches.length
344
    ) {
345
      throw new Error('GPUTable.detachBatches() requires a valid half-open batch range');
×
346
    }
347

348
    const detachedBatches = this.batches.splice(first, last - first);
1✔
349
    if (detachedBatches.length === 0) {
1!
350
      return detachedBatches;
×
351
    }
352
    this.numRows -= detachedBatches.reduce((numRows, batch) => numRows + batch.numRows, 0);
1✔
353
    this.nullCount -= detachedBatches.reduce((nullCount, batch) => nullCount + batch.nullCount, 0);
1✔
354
    this.rebuildAggregateVectors();
1✔
355
    return detachedBatches;
1✔
356
  }
357

358
  destroy(): void {
359
    for (const batch of this.batches) {
38✔
360
      batch.destroy();
43✔
361
    }
362
  }
363

364
  private rebuildAggregateVectors(): void {
365
    for (const name of Object.keys(this.gpuVectors)) {
62✔
366
      delete this.gpuVectors[name];
50✔
367
    }
368
    for (const name of Object.keys(this.attributes)) {
62✔
369
      delete this.attributes[name];
52✔
370
    }
371
    for (const name of Object.keys(this.bindings)) {
62✔
NEW
372
      delete this.bindings[name];
×
373
    }
374

375
    const firstBatch = this.batches[0];
62✔
376
    if (!firstBatch) {
62!
377
      return;
×
378
    }
379
    if (this.batches.length === 1) {
62✔
380
      Object.assign(this.gpuVectors, firstBatch.gpuVectors);
52✔
381
      Object.assign(this.attributes, firstBatch.attributes);
52✔
382
      Object.assign(this.bindings, firstBatch.bindings);
52✔
383
      return;
52✔
384
    }
385

386
    for (const vectorName of Object.keys(firstBatch.gpuVectors)) {
10✔
387
      const batchVectors = this.batches.map(batch => batch.gpuVectors[vectorName]);
42✔
388
      const firstVector = batchVectors[0];
19✔
389
      if (!firstVector) {
19!
NEW
390
        throw new Error(`GPUTable batch is missing GPU vector "${vectorName}"`);
×
391
      }
392
      const aggregateVector = new GPUVector({
19✔
393
        type: 'data',
394
        name: vectorName,
395
        arrowType: firstVector.type,
396
        data: [...firstVector.data],
397
        byteStride: firstVector.byteStride,
398
        bufferLayout: firstVector.bufferLayout
399
      });
400
      for (const batchVector of batchVectors.slice(1)) {
19✔
401
        for (const data of batchVector.data) {
23✔
402
          aggregateVector.addData(data);
23✔
403
        }
404
      }
405
      this.gpuVectors[vectorName] = aggregateVector;
19✔
406
    }
407

408
    Object.assign(this.attributes, firstBatch.attributes);
10✔
409
    Object.assign(this.bindings, firstBatch.bindings);
10✔
410
  }
411
}
412

413
function createArrowGPUPackGroups(
414
  batches: GPURecordBatch[],
415
  minBatchSize?: number
416
): GPURecordBatch[][] {
417
  if (minBatchSize === undefined) {
3✔
418
    return [batches];
2✔
419
  }
420
  if (!Number.isFinite(minBatchSize) || minBatchSize <= 0) {
1!
421
    throw new Error('GPUTable.packBatches() minBatchSize must be a positive number');
×
422
  }
423

424
  const batchGroups: GPURecordBatch[][] = [];
1✔
425
  let currentGroup: GPURecordBatch[] = [];
1✔
426
  let currentRowCount = 0;
1✔
427

428
  for (const batch of batches) {
1✔
429
    currentGroup.push(batch);
3✔
430
    currentRowCount += batch.numRows;
3✔
431
    if (currentRowCount >= minBatchSize) {
3✔
432
      batchGroups.push(currentGroup);
1✔
433
      currentGroup = [];
1✔
434
      currentRowCount = 0;
1✔
435
    }
436
  }
437

438
  if (currentGroup.length > 0) {
1!
439
    batchGroups.push(currentGroup);
1✔
440
  }
441
  return batchGroups;
1✔
442
}
443

444
function createPackedArrowGPURecordBatch(
445
  batchGroup: GPURecordBatch[],
446
  bufferLayout: BufferLayout[],
447
  schema: arrow.Schema
448
): GPURecordBatch {
449
  const firstBatch = batchGroup[0];
3✔
450
  const device = getArrowGPURecordBatchDevice(firstBatch);
3✔
451
  const commandEncoder = device.createCommandEncoder();
3✔
452
  const packedVectors: Record<string, GPUVector> = {};
3✔
453

454
  for (const layout of bufferLayout) {
3✔
455
    const sourceVectors = batchGroup.map(batch => batch.gpuVectors[layout.name]);
12✔
456
    const firstVector = sourceVectors[0];
6✔
457
    if (!firstVector) {
6!
458
      throw new Error(`GPUTable batch is missing GPU vector "${layout.name}"`);
×
459
    }
460

461
    const byteLength = sourceVectors.reduce(
6✔
462
      (totalByteLength, vector) => totalByteLength + vector.length * vector.byteStride,
12✔
463
      0
464
    );
465
    const buffer = device.createBuffer({
6✔
466
      usage: Buffer.VERTEX | Buffer.STORAGE | Buffer.COPY_DST | Buffer.COPY_SRC,
467
      byteLength
468
    });
469
    let destinationOffset = 0;
6✔
470
    for (const vector of sourceVectors) {
6✔
471
      const copyByteLength = getArrowGPUVectorCopyByteLength(vector);
12✔
472
      if (copyByteLength > 0) {
12!
473
        commandEncoder.copyBufferToBuffer({
12✔
474
          sourceBuffer:
475
            vector.buffer instanceof DynamicBuffer ? vector.buffer.buffer : vector.buffer,
12!
476
          sourceOffset: vector.byteOffset,
477
          destinationBuffer: buffer,
478
          destinationOffset,
479
          size: copyByteLength
480
        });
481
      }
482
      destinationOffset += vector.length * vector.byteStride;
12✔
483
    }
484

485
    packedVectors[layout.name] = firstVector.bufferLayout
6!
486
      ? new GPUVector({
487
          type: 'interleaved',
488
          name: firstVector.name,
489
          buffer,
490
          length: sourceVectors.reduce((length, vector) => length + vector.length, 0),
×
491
          byteStride: firstVector.byteStride,
492
          attributes: firstVector.bufferLayout.attributes ?? [],
×
493
          ownsBuffer: true
494
        })
495
      : new GPUVector({
496
          type: 'buffer',
497
          name: firstVector.name,
498
          buffer,
499
          arrowType: firstVector.type as AttributeArrowType,
500
          length: sourceVectors.reduce((length, vector) => length + vector.length, 0),
12✔
501
          byteStride: firstVector.byteStride,
502
          ownsBuffer: true
503
        } as any);
504
  }
505

506
  device.submit(commandEncoder.finish());
3✔
507
  return new GPURecordBatch({
3✔
508
    vectors: packedVectors,
509
    bufferLayout,
510
    fields: schema.fields,
511
    metadata: new Map(schema.metadata),
512
    nullCount: batchGroup.reduce((nullCount, batch) => nullCount + batch.nullCount, 0)
6✔
513
  });
514
}
515

516
function getArrowGPURecordBatchDevice(batch: GPURecordBatch): Device {
517
  const firstVector = Object.values(batch.gpuVectors)[0];
3✔
518
  if (!firstVector) {
3!
519
    throw new Error('GPUTable cannot pack an empty GPU record batch');
×
520
  }
521
  return firstVector.buffer.device;
3✔
522
}
523

524
function getArrowGPUVectorCopyByteLength(vector: GPUVector): number {
525
  if (vector.length === 0) {
12!
526
    return 0;
×
527
  }
528
  const rowByteWidth = vector.bufferLayout
12!
529
    ? vector.byteStride
530
    : getArrowTypeByteStride(vector.type);
531
  return (vector.length - 1) * vector.byteStride + rowByteWidth;
12✔
532
}
533

534
function assertCompatibleArrowGPURecordBatch(table: GPUTable, batch: GPURecordBatch): void {
535
  if (!deepEqualBufferLayouts(table.bufferLayout, batch.bufferLayout)) {
1!
536
    throw new Error('GPUTable.addBatch() requires matching buffer layouts');
×
537
  }
538
  if (table.schema.fields.length !== batch.schema.fields.length) {
1!
539
    throw new Error('GPUTable.addBatch() requires matching selected schema fields');
×
540
  }
541
  for (let fieldIndex = 0; fieldIndex < table.schema.fields.length; fieldIndex++) {
1✔
542
    const tableField = table.schema.fields[fieldIndex];
2✔
543
    const batchField = batch.schema.fields[fieldIndex];
2✔
544
    if (
2!
545
      !batchField ||
6✔
546
      tableField.name !== batchField.name ||
547
      !arrow.util.compareTypes(tableField.type, batchField.type)
548
    ) {
549
      throw new Error('GPUTable.addBatch() requires matching selected schema fields');
×
550
    }
551
  }
552
}
553

554
function deepEqualBufferLayouts(
555
  expectedBufferLayout: BufferLayout[],
556
  candidateBufferLayout: BufferLayout[]
557
): boolean {
558
  return JSON.stringify(expectedBufferLayout) === JSON.stringify(candidateBufferLayout);
1✔
559
}
560

561
function normalizeSelectedColumnNames(table: GPUTable, columnNames: string[]): string[] {
562
  const knownColumnNames = new Set(table.bufferLayout.map(layout => layout.name));
2✔
563
  const selectedColumnNames = Array.from(new Set(columnNames));
1✔
564
  for (const columnName of selectedColumnNames) {
1✔
565
    if (!knownColumnNames.has(columnName)) {
1!
566
      throw new Error(`GPUTable column "${columnName}" does not exist`);
×
567
    }
568
  }
569
  return selectedColumnNames;
1✔
570
}
571

572
function assertArrowGPUTableColumn(table: GPUTable, columnName: string): void {
573
  if (!table.bufferLayout.some(layout => layout.name === columnName)) {
2!
574
    throw new Error(`GPUTable column "${columnName}" does not exist`);
×
575
  }
576
}
577

578
function rebuildArrowGPUTableColumns(table: GPUTable, columnNames: string[]): void {
579
  const selectedColumnSet = new Set(columnNames);
2✔
580
  const selectedLayouts = columnNames
2✔
581
    .map(columnName => table.bufferLayout.find(layout => layout.name === columnName))
2✔
582
    .filter((layout): layout is BufferLayout => Boolean(layout));
2✔
583
  const selectedFields = columnNames
2✔
584
    .map(columnName => table.schema.fields.find(field => field.name === columnName))
2✔
585
    .filter((field): field is arrow.Field => Boolean(field));
2✔
586

587
  table.bufferLayout.splice(0, table.bufferLayout.length, ...selectedLayouts);
2✔
588
  table.schema = new arrow.Schema(selectedFields, new Map(table.schema.metadata));
2✔
589
  table.numCols = selectedFields.length;
2✔
590

591
  for (const name of Object.keys(table.gpuVectors)) {
2✔
592
    if (!selectedColumnSet.has(name)) {
4✔
593
      delete table.gpuVectors[name];
2✔
594
    }
595
  }
596
}
597

598
function rebuildArrowGPURecordBatchColumns(batch: GPURecordBatch, columnNames: string[]): void {
599
  const selectedColumnSet = new Set(columnNames);
3✔
600
  const selectedLayouts = columnNames
3✔
601
    .map(columnName => batch.bufferLayout.find(layout => layout.name === columnName))
3✔
602
    .filter((layout): layout is BufferLayout => Boolean(layout));
3✔
603
  const selectedFields = columnNames
3✔
604
    .map(columnName => batch.schema.fields.find(field => field.name === columnName))
3✔
605
    .filter((field): field is arrow.Field => Boolean(field));
3✔
606

607
  for (const name of Object.keys(batch.gpuVectors)) {
3✔
608
    if (!selectedColumnSet.has(name)) {
6✔
609
      delete batch.gpuVectors[name];
3✔
610
    }
611
  }
612
  for (const name of Object.keys(batch.attributes)) {
3✔
613
    delete batch.attributes[name];
6✔
614
  }
615
  for (const layout of selectedLayouts) {
3✔
616
    const vector = batch.gpuVectors[layout.name];
3✔
617
    if (!vector) {
3!
618
      throw new Error(`GPURecordBatch column "${layout.name}" has no GPU vector`);
×
619
    }
620
    if (layout.attributes) {
3!
621
      for (const attribute of layout.attributes) {
×
622
        batch.attributes[attribute.attribute] = vector.buffer;
×
623
      }
624
    } else {
625
      batch.attributes[layout.name] = vector.buffer;
3✔
626
    }
627
  }
628

629
  batch.bufferLayout.splice(0, batch.bufferLayout.length, ...selectedLayouts);
3✔
630
  batch.schema = new arrow.Schema(selectedFields, new Map(batch.schema.metadata));
3✔
631
  batch.numCols = selectedFields.length;
3✔
632
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc