Skip to content

Commit 2288bc3

Browse files
ilyaboCopilot
andauthored
fix: Allow passing arrow tables to ArrowDataContainer (#3247)
* fix: Allow passing arrow tables to ArrowDataContainer Signed-off-by: Ilya Boyandin <ilyabo@gmail.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ilya Boyandin <ilyabo@gmail.com> * Add comment Signed-off-by: Ilya Boyandin <ilyabo@gmail.com> * fix: Yarn start failed Signed-off-by: Ilya Boyandin <ilyabo@gmail.com> * Replace instanceof arrow.* with isArrow* type guards Signed-off-by: Ilya Boyandin <ilyabo@gmail.com> --------- Signed-off-by: Ilya Boyandin <ilyabo@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a0a4eef commit 2288bc3

13 files changed

Lines changed: 140 additions & 43 deletions

File tree

src/deckgl-arrow-layers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"dependencies": {
3333
"@geoarrow/geoarrow-js": "^0.3.0",
3434
"@kepler.gl/constants": "^3.2.2",
35+
"@kepler.gl/utils": "^3.2.2",
3536
"@math.gl/core": "^4.0.0",
3637
"@math.gl/polygon": "^4.0.0",
3738
"@math.gl/types": "^4.0.0",

src/deckgl-arrow-layers/src/utils/utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {assert} from '@deck.gl/core/typed';
99
import * as arrow from 'apache-arrow';
1010
import * as ga from '@geoarrow/geoarrow-js';
1111
import {AccessorContext, AccessorFunction, _InternalAccessorContext} from '../types';
12+
import {isArrowFixedSizeList, isArrowStruct, isArrowVector} from '@kepler.gl/utils';
1213

1314
export type TypedArray =
1415
| Uint8Array
@@ -45,14 +46,14 @@ function isDataInterleavedCoords(
4546
data: arrow.Data
4647
): data is arrow.Data<arrow.FixedSizeList<arrow.Float64>> {
4748
// TODO: also check 2 or 3d? Float64?
48-
return data.type instanceof arrow.FixedSizeList;
49+
return isArrowFixedSizeList(data.type);
4950
}
5051

5152
function isDataSeparatedCoords(
5253
data: arrow.Data
5354
): data is arrow.Data<arrow.Struct<{x: arrow.Float64; y: arrow.Float64}>> {
5455
// TODO: also check child names? Float64?
55-
return data.type instanceof arrow.Struct;
56+
return isArrowStruct(data.type);
5657
}
5758

5859
/**
@@ -162,7 +163,7 @@ export function assignAccessor(args: AssignAccessorProps) {
162163
return;
163164
}
164165

165-
if (propInput instanceof arrow.Vector) {
166+
if (isArrowVector(propInput)) {
166167
const columnData = propInput.data[chunkIdx];
167168

168169
if (arrow.DataType.isFixedSizeList(columnData)) {

src/deckgl-arrow-layers/src/utils/validate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// Copyright (c) vis.gl contributors
77

88
import {assert} from '@deck.gl/core/typed';
9+
import {isArrowVector} from '@kepler.gl/utils';
910
import * as arrow from 'apache-arrow';
1011

1112
export function validateAccessors(props: Record<string, any>, table: arrow.Table): void {
@@ -15,7 +16,7 @@ export function validateAccessors(props: Record<string, any>, table: arrow.Table
1516
// Is it an accessor
1617
if (accessorName.startsWith('get')) {
1718
// Is it a vector accessor
18-
if (accessorValue instanceof arrow.Vector) {
19+
if (isArrowVector(accessorValue)) {
1920
vectorAccessors.push(accessorValue);
2021

2122
// Is it a color vector accessor

src/duckdb/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@kepler.gl/processors": "3.2.2",
3232
"@kepler.gl/table": "3.2.2",
3333
"@kepler.gl/types": "3.2.2",
34+
"@kepler.gl/utils": "3.2.2",
3435
"@monaco-editor/react": "^4.6.0",
3536
"@radix-ui/react-collapsible": "^1.1.0",
3637
"apache-arrow": ">=15.0.0",

src/duckdb/src/table/duckdb-table.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import {
1919
} from '@kepler.gl/processors';
2020
import {KeplerTable} from '@kepler.gl/table';
2121
import {Field} from '@kepler.gl/types';
22-
import {getApplicationConfig, DatabaseAdapter, DatabaseConnection} from '@kepler.gl/utils';
22+
import {
23+
getApplicationConfig,
24+
DatabaseAdapter,
25+
DatabaseConnection,
26+
isArrowTable,
27+
isArrowVector
28+
} from '@kepler.gl/utils';
2329

2430
import {
2531
processCsvRowObject,
@@ -161,10 +167,9 @@ export class KeplerGlDuckDbTable extends KeplerTable {
161167
try {
162168
// 1) data.rows contains an arrow table created by Add to Map data from DuckDb query.
163169
// 2) arrow table is in cols & fields when a file is dragged & dropped into Add Data To Map dialog.
164-
const arrowTable =
165-
data.rows instanceof arrow.Table
166-
? data.rows
167-
: restoreArrowTable(data.cols || [], data.fields, data.arrowSchema);
170+
const arrowTable = isArrowTable(data.rows)
171+
? data.rows
172+
: restoreArrowTable(data.cols || [], data.fields, data.arrowSchema);
168173

169174
// remove unsupported extensions from an arrow table that throw exceptions in DuckDB.
170175
adjustedMetadata = removeUnsupportedExtensions(arrowTable);
@@ -215,7 +220,7 @@ export class KeplerGlDuckDbTable extends KeplerTable {
215220
format = DATASET_FORMATS.row;
216221
} else if (data.rows?.type === 'FeatureCollection') {
217222
format = DATASET_FORMATS.geojson;
218-
} else if (data.cols?.[0] instanceof arrow.Vector) {
223+
} else if (isArrowVector(data.cols?.[0])) {
219224
format = DATASET_FORMATS.arrow;
220225
}
221226
}

src/layers/src/base-layer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import {
4141
hexToRgb,
4242
isPlainObject,
4343
isDomainStops,
44-
updateColorRangeByMatchingPalette
44+
updateColorRangeByMatchingPalette,
45+
isArrowTable
4546
} from '@kepler.gl/utils';
4647
import {generateHashId, toArray, notNullorUndefined} from '@kepler.gl/common-utils';
4748
import {Datasets, GpuFilter, KeplerTable} from '@kepler.gl/table';
@@ -1585,7 +1586,7 @@ class Layer implements KeplerLayer {
15851586
const {data, mapState} = renderOpts;
15861587
const {textLabel} = this.config;
15871588

1588-
const TextLayerClass = data.data instanceof arrow.Table ? GeoArrowTextLayer : TextLayer;
1589+
const TextLayerClass = isArrowTable(data.data) ? GeoArrowTextLayer : TextLayer;
15891590

15901591
return data.textLabels.reduce((accu, d, i) => {
15911592
if (d.getText) {

src/layers/src/layer-text-label.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import * as arrow from 'apache-arrow';
55
import {getDistanceScales} from 'viewport-mercator-project';
6-
import {DataContainerInterface, ArrowDataContainer} from '@kepler.gl/utils';
6+
import {DataContainerInterface, ArrowDataContainer, isArrowTable} from '@kepler.gl/utils';
77
import {notNullorUndefined} from '@kepler.gl/common-utils';
88
import uniq from 'lodash/uniq';
99

@@ -82,7 +82,7 @@ export const formatTextLabelData = ({
8282
getText = oldLayerData.textLabels[i].getText;
8383
rebuildArrowTextVector = false;
8484
} else {
85-
if (data instanceof arrow.Table) {
85+
if (isArrowTable(data)) {
8686
// we don't filter out arrow tables,
8787
// so we use filteredIndex array instead
8888
const allLabels: string[] = [];
@@ -108,7 +108,7 @@ export const formatTextLabelData = ({
108108
// For now check here for ArrowTable, not ArrowDataContainer.
109109
if (
110110
rebuildArrowTextVector &&
111-
data instanceof arrow.Table &&
111+
isArrowTable(data) &&
112112
dataContainer instanceof ArrowDataContainer
113113
) {
114114
getText = dataContainer.getColumn(tl.field.fieldIdx);

src/processors/src/file-handler.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright contributors to the kepler.gl project
33

4-
import * as arrow from 'apache-arrow';
54
import {parseInBatches} from '@loaders.gl/core';
65
import {JSONLoader, _JSONPath} from '@loaders.gl/json';
76
import {CSVLoader} from '@loaders.gl/csv';
@@ -12,7 +11,8 @@ import {
1211
isPlainObject,
1312
generateHashIdFromString,
1413
getApplicationConfig,
15-
getError
14+
getError,
15+
isArrowTable
1616
} from '@kepler.gl/utils';
1717
import {generateHashId} from '@kepler.gl/common-utils';
1818
import {DATASET_FORMATS} from '@kepler.gl/constants';
@@ -68,14 +68,7 @@ export type ProcessFileDataContent = {
6868
metadata?: Map<string, string>;
6969
};
7070

71-
/**
72-
* check if table is an ArrowTable object
73-
* @param table - object to check
74-
* @returns {boolean} - true if table is an ArrowTable object type guarded
75-
*/
76-
export function isArrowTable(table: any): table is arrow.Table {
77-
return Boolean(table instanceof arrow.Table);
78-
}
71+
export {isArrowTable};
7972

8073
/**
8174
* check if data is an ArrowData object, which is an array of RecordBatch

src/table/src/kepler-table.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ class KeplerTable<F extends Field = Field> {
241241
* @param data - new data e.g. the arrow data with new batches loaded
242242
*/
243243
async update(data: ProtoDataset['data']) {
244-
const dataContainerData = data.cols ? data.cols : data.rows;
244+
const dataContainerData = data.arrowTable ?? data.cols ?? data.rows;
245245
this.dataContainer.update?.(dataContainerData);
246246
this.allIndexes = this.dataContainer.getPlainIndex();
247247
this.filteredIndex = this.allIndexes;

src/utils/src/arrow-data-container.ts

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,102 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright contributors to the kepler.gl project
33

4-
import { ALL_FIELD_TYPES } from '@kepler.gl/constants';
5-
import { ProtoDatasetField } from '@kepler.gl/types';
4+
import {ALL_FIELD_TYPES} from '@kepler.gl/constants';
5+
import {ProtoDatasetField} from '@kepler.gl/types';
66
import * as arrow from 'apache-arrow';
7-
import { console as globalConsole } from 'global/window';
8-
import { DATA_TYPES as AnalyzerDATA_TYPES } from 'type-analyzer';
7+
import {console as globalConsole} from 'global/window';
8+
import {DATA_TYPES as AnalyzerDATA_TYPES} from 'type-analyzer';
99

10-
import { DataContainerInterface, RangeOptions } from './data-container-interface';
11-
import { DataRow, SharedRowOptions } from './data-row';
10+
import {DataContainerInterface, RangeOptions} from './data-container-interface';
11+
import {DataRow, SharedRowOptions} from './data-row';
1212

1313
type ArrowDataContainerInput = {
1414
cols: arrow.Vector[];
1515
fields?: ProtoDatasetField[];
1616
arrowTable?: arrow.Table;
1717
};
1818

19+
/**
20+
* Check if table is an ArrowTable object.
21+
*
22+
* We use duck-typing instead of `instanceof arrow.Table` because DuckDB loads its own
23+
* bundled version of Apache Arrow. When DuckDB creates Arrow tables, they are instances
24+
* of DuckDB's Arrow.Table class, not the Arrow.Table class from our application's
25+
* apache-arrow package. This causes `instanceof` checks to fail even though the objects
26+
* are functionally equivalent Arrow tables.
27+
*
28+
* @param data - object to check
29+
* @returns true if data is an ArrowTable object (type guarded)
30+
*/
31+
export function isArrowTable(data: any): data is arrow.Table {
32+
return (
33+
typeof data === 'object' &&
34+
data !== null &&
35+
'schema' in data &&
36+
'getChildAt' in data &&
37+
typeof data.getChildAt === 'function' &&
38+
'batches' in data &&
39+
Array.isArray(data.batches)
40+
);
41+
}
42+
43+
/**
44+
* Check if data is an ArrowVector object.
45+
* Uses duck-typing instead of `instanceof` to handle DuckDB's bundled Arrow version.
46+
*
47+
* @param data - object to check
48+
* @returns true if data is an ArrowVector object (type guarded)
49+
*/
50+
export function isArrowVector(data: any): data is arrow.Vector {
51+
return (
52+
typeof data === 'object' &&
53+
data !== null &&
54+
'type' in data &&
55+
'length' in data &&
56+
typeof data.length === 'number' &&
57+
'get' in data &&
58+
typeof data.get === 'function' &&
59+
'data' in data &&
60+
Array.isArray(data.data)
61+
);
62+
}
63+
64+
/**
65+
* Check if data is an Arrow FixedSizeList DataType.
66+
* Uses duck-typing instead of `instanceof` to handle DuckDB's bundled Arrow version.
67+
*
68+
* @param data - object to check
69+
* @returns true if data is an Arrow FixedSizeList DataType (type guarded)
70+
*/
71+
export function isArrowFixedSizeList(data: any): data is arrow.FixedSizeList {
72+
return (
73+
typeof data === 'object' &&
74+
data !== null &&
75+
'typeId' in data &&
76+
'listSize' in data &&
77+
typeof data.listSize === 'number' &&
78+
'children' in data &&
79+
Array.isArray(data.children)
80+
);
81+
}
82+
83+
/**
84+
* Check if data is an Arrow Struct DataType.
85+
* Uses duck-typing instead of `instanceof` to handle DuckDB's bundled Arrow version.
86+
*
87+
* @param data - object to check
88+
* @returns true if data is an Arrow Struct DataType (type guarded)
89+
*/
90+
export function isArrowStruct(data: any): data is arrow.Struct {
91+
return (
92+
typeof data === 'object' &&
93+
data !== null &&
94+
'typeId' in data &&
95+
'children' in data &&
96+
Array.isArray(data.children) &&
97+
!('listSize' in data)
98+
);
99+
}
19100

20101
/**
21102
* @param dataContainer
@@ -89,13 +170,20 @@ export class ArrowDataContainer implements DataContainerInterface {
89170
return this._arrowTable;
90171
}
91172

92-
update(updateData: arrow.Vector<any>[]) {
93-
this._cols = updateData;
94-
this._numColumns = this._cols.length;
95-
this._numRows = this._cols[0].length;
96-
this._numChunks = this._cols[0].data.length;
97-
98-
this._arrowTable = this._createTable();
173+
update(updateData: arrow.Vector<any>[] | arrow.Table) {
174+
const isArrow = isArrowTable(updateData);
175+
if (isArrow) {
176+
this._cols = Array.from(
177+
{length: updateData.numCols},
178+
(_, i) => updateData.getChildAt(i) as arrow.Vector
179+
).filter(col => col);
180+
} else {
181+
this._cols = updateData;
182+
}
183+
this._numColumns = this._cols?.length ?? 0;
184+
this._numRows = this._cols?.[0]?.length ?? 0;
185+
this._numChunks = this._cols?.[0]?.data?.length ?? 0;
186+
this._arrowTable = isArrow ? updateData : this._createTable();
99187

100188
// cache column data to make valueAt() faster
101189
// this._colData = this._cols.map(c => c.toArray());

0 commit comments

Comments
 (0)