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

visgl / luma.gl / 25879749619

14 May 2026 07:05PM UTC coverage: 74.881% (-0.2%) from 75.089%
25879749619

push

github

web-flow
feat(text) TextArrowModel (#2615)

6380 of 9600 branches covered (66.46%)

Branch coverage included in aggregate %.

462 of 672 new or added lines in 6 files covered. (68.75%)

123 existing lines in 9 files now uncovered.

13975 of 17583 relevant lines covered (79.48%)

782.31 hits per line

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

85.3
/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[] = [];
36✔
74
  /** GPU vectors keyed by shader attribute name. */
75
  readonly gpuVectors: Record<string, GPUVector> = {};
36✔
76
  /** Model-ready attribute buffers keyed by shader attribute name. */
77
  readonly attributes: Record<string, Buffer | DynamicBuffer> = {};
36✔
78
  /** GPU record batches preserving source Arrow table batch boundaries. */
79
  readonly batches: GPURecordBatch[] = [];
36✔
80

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

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

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

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

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

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

159
    this.schema = new arrow.Schema(fields, new Map(table.schema.metadata));
23✔
160
    this.numCols = this.schema.fields.length;
23✔
161
    for (const recordBatch of table.batches) {
23✔
162
      this.batches.push(new GPURecordBatch(device, recordBatch, props));
31✔
163
    }
164
    this.rebuildAggregateVectors();
23✔
165
  }
166

167
  /**
168
   * Replaces preserved GPU batches with fewer packed batches.
169
   *
170
   * New packed batches own their destination buffers. Superseded source batches
171
   * are destroyed after GPU copies are submitted, which only releases buffers
172
   * owned by those source vectors.
173
   */
174
  packBatches(options: GPUTablePackBatchesOptions = {}): this {
3✔
175
    if (this.batches.length <= 1) {
3!
UNCOV
176
      return this;
×
177
    }
178

179
    const batchGroups = createArrowGPUPackGroups(this.batches, options.minBatchSize);
3✔
180
    const nextBatches: GPURecordBatch[] = [];
3✔
181
    const supersededBatches: GPURecordBatch[] = [];
3✔
182

183
    for (const batchGroup of batchGroups) {
3✔
184
      if (batchGroup.length === 1) {
4✔
185
        nextBatches.push(batchGroup[0]);
1✔
186
        continue;
1✔
187
      }
188
      nextBatches.push(createPackedArrowGPURecordBatch(batchGroup, this.bufferLayout, this.schema));
3✔
189
      supersededBatches.push(...batchGroup);
3✔
190
    }
191

192
    if (supersededBatches.length === 0) {
3!
UNCOV
193
      return this;
×
194
    }
195

196
    this.batches.splice(0, this.batches.length, ...nextBatches);
3✔
197
    this.rebuildAggregateVectors();
3✔
198
    for (const batch of supersededBatches) {
3✔
199
      batch.destroy();
6✔
200
    }
201
    return this;
3✔
202
  }
203

204
  /**
205
   * Adds one already-created GPU record batch to this table.
206
   *
207
   * Ownership remains on the supplied batch and its vectors. The table only
208
   * incorporates that batch into its aggregate metadata and will later call the
209
   * batch's regular `destroy()` path when the table itself is destroyed.
210
   */
211
  addBatch(batch: GPURecordBatch): this {
212
    assertCompatibleArrowGPURecordBatch(this, batch);
1✔
213
    this.batches.push(batch);
1✔
214
    this.numRows += batch.numRows;
1✔
215
    this.nullCount += batch.nullCount;
1✔
216
    this.rebuildAggregateVectors();
1✔
217
    return this;
1✔
218
  }
219

220
  /**
221
   * Appends Arrow rows into the current trailing appendable GPU batch.
222
   *
223
   * Arrow tables are consumed batch-by-batch so one mutable trailing GPU batch
224
   * can absorb synchronous incremental table arrivals without changing table
225
   * ownership.
226
   */
227
  addToLastBatch(recordBatchOrTable: arrow.RecordBatch | arrow.Table): this {
228
    if (recordBatchOrTable instanceof arrow.Table) {
20✔
229
      for (const recordBatch of recordBatchOrTable.batches) {
2✔
230
        this.addToLastBatch(recordBatch);
3✔
231
      }
232
      return this;
2✔
233
    }
234

235
    const lastBatch = this.batches[this.batches.length - 1];
18✔
236
    if (!lastBatch) {
18!
UNCOV
237
      throw new Error('GPUTable.addToLastBatch() requires an existing trailing batch');
×
238
    }
239
    lastBatch.addToLastBatch(recordBatchOrTable);
18✔
240
    this.numRows += recordBatchOrTable.numRows;
18✔
241
    this.nullCount += recordBatchOrTable.nullCount;
18✔
242
    this.rebuildAggregateVectors();
18✔
243
    return this;
18✔
244
  }
245

246
  /** Clears only the current trailing appendable GPU batch while retaining its allocations. */
247
  resetLastBatch(): this {
248
    const lastBatch = this.batches[this.batches.length - 1];
2✔
249
    if (!lastBatch) {
2!
UNCOV
250
      throw new Error('GPUTable.resetLastBatch() requires an existing trailing batch');
×
251
    }
252
    this.numRows -= lastBatch.numRows;
2✔
253
    this.nullCount -= lastBatch.nullCount;
2✔
254
    lastBatch.resetLastBatch();
2✔
255
    this.rebuildAggregateVectors();
2✔
256
    return this;
2✔
257
  }
258

259
  /**
260
   * Keeps only the requested columns and destroys the dropped batch-local vectors.
261
   *
262
   * Use {@link detachVector} first when a removed column should stay alive.
263
   */
264
  select(...columnNames: string[]): this {
265
    const selectedColumnNames = normalizeSelectedColumnNames(this, columnNames);
1✔
266
    const selectedColumnSet = new Set(selectedColumnNames);
1✔
267

268
    for (const batch of this.batches) {
1✔
269
      const droppedVectors = Object.entries(batch.gpuVectors)
1✔
270
        .filter(([name]) => !selectedColumnSet.has(name))
2✔
271
        .map(([, vector]) => vector);
1✔
272
      for (const vector of droppedVectors) {
1✔
273
        vector.destroy();
1✔
274
      }
275
      rebuildArrowGPURecordBatchColumns(batch, selectedColumnNames);
1✔
276
    }
277

278
    rebuildArrowGPUTableColumns(this, selectedColumnNames);
1✔
279
    this.rebuildAggregateVectors();
1✔
280
    return this;
1✔
281
  }
282

283
  /**
284
   * Removes one column and returns an aggregate GPU vector that now owns its detached batch vectors.
285
   */
286
  detachVector(columnName: string): GPUVector {
287
    assertArrowGPUTableColumn(this, columnName);
1✔
288
    const detachedVectors = this.batches.map(batch => batch.gpuVectors[columnName]);
2✔
289
    const firstVector = detachedVectors[0];
1✔
290
    if (!firstVector) {
1!
UNCOV
291
      throw new Error(`GPUTable.detachVector() column "${columnName}" has no GPU data`);
×
292
    }
293

294
    const detachedVector = new GPUVector({
1✔
295
      type: 'data',
296
      name: columnName,
297
      arrowType: firstVector.type,
298
      data: [...firstVector.data],
299
      byteStride: firstVector.byteStride,
300
      bufferLayout: firstVector.bufferLayout
301
    });
302
    for (const batchVector of detachedVectors.slice(1)) {
1✔
303
      for (const data of batchVector.data) {
1✔
304
        detachedVector.addData(data);
1✔
305
      }
306
    }
307
    detachedVector.retainOwnedVectors(detachedVectors);
1✔
308

309
    const remainingColumnNames = this.bufferLayout
1✔
310
      .map(layout => layout.name)
2✔
311
      .filter(name => name !== columnName);
2✔
312
    for (const batch of this.batches) {
1✔
313
      rebuildArrowGPURecordBatchColumns(batch, remainingColumnNames);
2✔
314
    }
315
    rebuildArrowGPUTableColumns(this, remainingColumnNames);
1✔
316
    this.rebuildAggregateVectors();
1✔
317
    return detachedVector;
1✔
318
  }
319

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

339
    const detachedBatches = this.batches.splice(first, last - first);
1✔
340
    if (detachedBatches.length === 0) {
1!
UNCOV
341
      return detachedBatches;
×
342
    }
343
    this.numRows -= detachedBatches.reduce((numRows, batch) => numRows + batch.numRows, 0);
1✔
344
    this.nullCount -= detachedBatches.reduce((nullCount, batch) => nullCount + batch.nullCount, 0);
1✔
345
    this.rebuildAggregateVectors();
1✔
346
    return detachedBatches;
1✔
347
  }
348

349
  destroy(): void {
350
    for (const batch of this.batches) {
36✔
351
      batch.destroy();
41✔
352
    }
353
  }
354

355
  private rebuildAggregateVectors(): void {
356
    for (const name of Object.keys(this.gpuVectors)) {
60✔
357
      delete this.gpuVectors[name];
50✔
358
    }
359
    for (const name of Object.keys(this.attributes)) {
60✔
360
      delete this.attributes[name];
52✔
361
    }
362

363
    const firstBatch = this.batches[0];
60✔
364
    if (!firstBatch) {
60!
UNCOV
365
      return;
×
366
    }
367
    if (this.batches.length === 1) {
60✔
368
      Object.assign(this.gpuVectors, firstBatch.gpuVectors);
50✔
369
      Object.assign(this.attributes, firstBatch.attributes);
50✔
370
      return;
50✔
371
    }
372

373
    for (const bufferLayout of this.bufferLayout) {
10✔
374
      const batchVectors = this.batches.map(batch => batch.gpuVectors[bufferLayout.name]);
42✔
375
      const firstVector = batchVectors[0];
19✔
376
      if (!firstVector) {
19!
UNCOV
377
        throw new Error(`GPUTable batch is missing GPU vector "${bufferLayout.name}"`);
×
378
      }
379
      const aggregateVector = new GPUVector({
19✔
380
        type: 'data',
381
        name: bufferLayout.name,
382
        arrowType: firstVector.type,
383
        data: [...firstVector.data],
384
        byteStride: firstVector.byteStride,
385
        bufferLayout: firstVector.bufferLayout
386
      });
387
      for (const batchVector of batchVectors.slice(1)) {
19✔
388
        for (const data of batchVector.data) {
23✔
389
          aggregateVector.addData(data);
23✔
390
        }
391
      }
392
      this.gpuVectors[bufferLayout.name] = aggregateVector;
19✔
393
    }
394

395
    Object.assign(this.attributes, firstBatch.attributes);
10✔
396
  }
397
}
398

399
function createArrowGPUPackGroups(
400
  batches: GPURecordBatch[],
401
  minBatchSize?: number
402
): GPURecordBatch[][] {
403
  if (minBatchSize === undefined) {
3✔
404
    return [batches];
2✔
405
  }
406
  if (!Number.isFinite(minBatchSize) || minBatchSize <= 0) {
1!
UNCOV
407
    throw new Error('GPUTable.packBatches() minBatchSize must be a positive number');
×
408
  }
409

410
  const batchGroups: GPURecordBatch[][] = [];
1✔
411
  let currentGroup: GPURecordBatch[] = [];
1✔
412
  let currentRowCount = 0;
1✔
413

414
  for (const batch of batches) {
1✔
415
    currentGroup.push(batch);
3✔
416
    currentRowCount += batch.numRows;
3✔
417
    if (currentRowCount >= minBatchSize) {
3✔
418
      batchGroups.push(currentGroup);
1✔
419
      currentGroup = [];
1✔
420
      currentRowCount = 0;
1✔
421
    }
422
  }
423

424
  if (currentGroup.length > 0) {
1!
425
    batchGroups.push(currentGroup);
1✔
426
  }
427
  return batchGroups;
1✔
428
}
429

430
function createPackedArrowGPURecordBatch(
431
  batchGroup: GPURecordBatch[],
432
  bufferLayout: BufferLayout[],
433
  schema: arrow.Schema
434
): GPURecordBatch {
435
  const firstBatch = batchGroup[0];
3✔
436
  const device = getArrowGPURecordBatchDevice(firstBatch);
3✔
437
  const commandEncoder = device.createCommandEncoder();
3✔
438
  const packedVectors: Record<string, GPUVector> = {};
3✔
439

440
  for (const layout of bufferLayout) {
3✔
441
    const sourceVectors = batchGroup.map(batch => batch.gpuVectors[layout.name]);
12✔
442
    const firstVector = sourceVectors[0];
6✔
443
    if (!firstVector) {
6!
UNCOV
444
      throw new Error(`GPUTable batch is missing GPU vector "${layout.name}"`);
×
445
    }
446

447
    const byteLength = sourceVectors.reduce(
6✔
448
      (totalByteLength, vector) => totalByteLength + vector.length * vector.byteStride,
12✔
449
      0
450
    );
451
    const buffer = device.createBuffer({
6✔
452
      usage: Buffer.VERTEX | Buffer.STORAGE | Buffer.COPY_DST | Buffer.COPY_SRC,
453
      byteLength
454
    });
455
    let destinationOffset = 0;
6✔
456
    for (const vector of sourceVectors) {
6✔
457
      const copyByteLength = getArrowGPUVectorCopyByteLength(vector);
12✔
458
      if (copyByteLength > 0) {
12!
459
        commandEncoder.copyBufferToBuffer({
12✔
460
          sourceBuffer:
461
            vector.buffer instanceof DynamicBuffer ? vector.buffer.buffer : vector.buffer,
12!
462
          sourceOffset: vector.byteOffset,
463
          destinationBuffer: buffer,
464
          destinationOffset,
465
          size: copyByteLength
466
        });
467
      }
468
      destinationOffset += vector.length * vector.byteStride;
12✔
469
    }
470

471
    packedVectors[layout.name] = firstVector.bufferLayout
6!
472
      ? new GPUVector({
473
          type: 'interleaved',
474
          name: firstVector.name,
475
          buffer,
UNCOV
476
          length: sourceVectors.reduce((length, vector) => length + vector.length, 0),
×
477
          byteStride: firstVector.byteStride,
478
          attributes: firstVector.bufferLayout.attributes ?? [],
×
479
          ownsBuffer: true
480
        })
481
      : new GPUVector({
482
          type: 'buffer',
483
          name: firstVector.name,
484
          buffer,
485
          arrowType: firstVector.type as AttributeArrowType,
486
          length: sourceVectors.reduce((length, vector) => length + vector.length, 0),
12✔
487
          byteStride: firstVector.byteStride,
488
          ownsBuffer: true
489
        } as any);
490
  }
491

492
  device.submit(commandEncoder.finish());
3✔
493
  return new GPURecordBatch({
3✔
494
    vectors: packedVectors,
495
    bufferLayout,
496
    fields: schema.fields,
497
    metadata: new Map(schema.metadata),
498
    nullCount: batchGroup.reduce((nullCount, batch) => nullCount + batch.nullCount, 0)
6✔
499
  });
500
}
501

502
function getArrowGPURecordBatchDevice(batch: GPURecordBatch): Device {
503
  const firstVector = Object.values(batch.gpuVectors)[0];
3✔
504
  if (!firstVector) {
3!
UNCOV
505
    throw new Error('GPUTable cannot pack an empty GPU record batch');
×
506
  }
507
  return firstVector.buffer.device;
3✔
508
}
509

510
function getArrowGPUVectorCopyByteLength(vector: GPUVector): number {
511
  if (vector.length === 0) {
12!
UNCOV
512
    return 0;
×
513
  }
514
  const rowByteWidth = vector.bufferLayout
12!
515
    ? vector.byteStride
516
    : getArrowTypeByteStride(vector.type);
517
  return (vector.length - 1) * vector.byteStride + rowByteWidth;
12✔
518
}
519

520
function assertCompatibleArrowGPURecordBatch(table: GPUTable, batch: GPURecordBatch): void {
521
  if (!deepEqualBufferLayouts(table.bufferLayout, batch.bufferLayout)) {
1!
UNCOV
522
    throw new Error('GPUTable.addBatch() requires matching buffer layouts');
×
523
  }
524
  if (table.schema.fields.length !== batch.schema.fields.length) {
1!
UNCOV
525
    throw new Error('GPUTable.addBatch() requires matching selected schema fields');
×
526
  }
527
  for (let fieldIndex = 0; fieldIndex < table.schema.fields.length; fieldIndex++) {
1✔
528
    const tableField = table.schema.fields[fieldIndex];
2✔
529
    const batchField = batch.schema.fields[fieldIndex];
2✔
530
    if (
2!
531
      !batchField ||
6✔
532
      tableField.name !== batchField.name ||
533
      !arrow.util.compareTypes(tableField.type, batchField.type)
534
    ) {
UNCOV
535
      throw new Error('GPUTable.addBatch() requires matching selected schema fields');
×
536
    }
537
  }
538
}
539

540
function deepEqualBufferLayouts(
541
  expectedBufferLayout: BufferLayout[],
542
  candidateBufferLayout: BufferLayout[]
543
): boolean {
544
  return JSON.stringify(expectedBufferLayout) === JSON.stringify(candidateBufferLayout);
1✔
545
}
546

547
function normalizeSelectedColumnNames(table: GPUTable, columnNames: string[]): string[] {
548
  const knownColumnNames = new Set(table.bufferLayout.map(layout => layout.name));
2✔
549
  const selectedColumnNames = Array.from(new Set(columnNames));
1✔
550
  for (const columnName of selectedColumnNames) {
1✔
551
    if (!knownColumnNames.has(columnName)) {
1!
UNCOV
552
      throw new Error(`GPUTable column "${columnName}" does not exist`);
×
553
    }
554
  }
555
  return selectedColumnNames;
1✔
556
}
557

558
function assertArrowGPUTableColumn(table: GPUTable, columnName: string): void {
559
  if (!table.bufferLayout.some(layout => layout.name === columnName)) {
2!
UNCOV
560
    throw new Error(`GPUTable column "${columnName}" does not exist`);
×
561
  }
562
}
563

564
function rebuildArrowGPUTableColumns(table: GPUTable, columnNames: string[]): void {
565
  const selectedColumnSet = new Set(columnNames);
2✔
566
  const selectedLayouts = columnNames
2✔
567
    .map(columnName => table.bufferLayout.find(layout => layout.name === columnName))
2✔
568
    .filter((layout): layout is BufferLayout => Boolean(layout));
2✔
569
  const selectedFields = columnNames
2✔
570
    .map(columnName => table.schema.fields.find(field => field.name === columnName))
2✔
571
    .filter((field): field is arrow.Field => Boolean(field));
2✔
572

573
  table.bufferLayout.splice(0, table.bufferLayout.length, ...selectedLayouts);
2✔
574
  table.schema = new arrow.Schema(selectedFields, new Map(table.schema.metadata));
2✔
575
  table.numCols = selectedFields.length;
2✔
576

577
  for (const name of Object.keys(table.gpuVectors)) {
2✔
578
    if (!selectedColumnSet.has(name)) {
4✔
579
      delete table.gpuVectors[name];
2✔
580
    }
581
  }
582
}
583

584
function rebuildArrowGPURecordBatchColumns(batch: GPURecordBatch, columnNames: string[]): void {
585
  const selectedColumnSet = new Set(columnNames);
3✔
586
  const selectedLayouts = columnNames
3✔
587
    .map(columnName => batch.bufferLayout.find(layout => layout.name === columnName))
3✔
588
    .filter((layout): layout is BufferLayout => Boolean(layout));
3✔
589
  const selectedFields = columnNames
3✔
590
    .map(columnName => batch.schema.fields.find(field => field.name === columnName))
3✔
591
    .filter((field): field is arrow.Field => Boolean(field));
3✔
592

593
  for (const name of Object.keys(batch.gpuVectors)) {
3✔
594
    if (!selectedColumnSet.has(name)) {
6✔
595
      delete batch.gpuVectors[name];
3✔
596
    }
597
  }
598
  for (const name of Object.keys(batch.attributes)) {
3✔
599
    delete batch.attributes[name];
6✔
600
  }
601
  for (const layout of selectedLayouts) {
3✔
602
    const vector = batch.gpuVectors[layout.name];
3✔
603
    if (!vector) {
3!
UNCOV
604
      throw new Error(`GPURecordBatch column "${layout.name}" has no GPU vector`);
×
605
    }
606
    if (layout.attributes) {
3!
UNCOV
607
      for (const attribute of layout.attributes) {
×
UNCOV
608
        batch.attributes[attribute.attribute] = vector.buffer;
×
609
      }
610
    } else {
611
      batch.attributes[layout.name] = vector.buffer;
3✔
612
    }
613
  }
614

615
  batch.bufferLayout.splice(0, batch.bufferLayout.length, ...selectedLayouts);
3✔
616
  batch.schema = new arrow.Schema(selectedFields, new Map(batch.schema.metadata));
3✔
617
  batch.numCols = selectedFields.length;
3✔
618
}
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