@duckdb/react-duckdb 1.13.1-dev260.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +30 -0
- package/src/connection_provider.tsx +75 -0
- package/src/database_provider.tsx +90 -0
- package/src/epoch_contexts.tsx +7 -0
- package/src/index.ts +6 -0
- package/src/platform_provider.tsx +42 -0
- package/src/table_schema.ts +117 -0
- package/src/table_schema_provider.tsx +70 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duckdb/react-duckdb",
|
|
3
|
+
"version": "1.13.1-dev260.0",
|
|
4
|
+
"description": "React components for DuckDB-Wasm",
|
|
5
|
+
"license": "MPL-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/duckdb/duckdb-wasm.git"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"sql",
|
|
12
|
+
"duckdb",
|
|
13
|
+
"relational",
|
|
14
|
+
"database",
|
|
15
|
+
"data",
|
|
16
|
+
"query",
|
|
17
|
+
"wasm",
|
|
18
|
+
"analytics",
|
|
19
|
+
"olap",
|
|
20
|
+
"arrow",
|
|
21
|
+
"parquet",
|
|
22
|
+
"json",
|
|
23
|
+
"csv"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"main": "src/index.ts",
|
|
29
|
+
"types": "src/index.ts"
|
|
30
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as imm from 'immutable';
|
|
3
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
4
|
+
import { useDuckDB } from './database_provider';
|
|
5
|
+
import { useDuckDBLauncher } from './platform_provider';
|
|
6
|
+
|
|
7
|
+
type DialerFn = (id?: number) => void;
|
|
8
|
+
|
|
9
|
+
export const poolCtx = React.createContext<imm.Map<number, duckdb.AsyncDuckDBConnection>>(imm.Map());
|
|
10
|
+
export const dialerCtx = React.createContext<DialerFn | null>(null);
|
|
11
|
+
|
|
12
|
+
export const useDuckDBConnection = (id?: number): duckdb.AsyncDuckDBConnection | null =>
|
|
13
|
+
React.useContext(poolCtx)?.get(id || 0) || null;
|
|
14
|
+
export const useDuckDBConnectionDialer = (): DialerFn => React.useContext(dialerCtx)!;
|
|
15
|
+
|
|
16
|
+
type DuckDBConnectionProps = {
|
|
17
|
+
/// The children
|
|
18
|
+
children: React.ReactElement | React.ReactElement[];
|
|
19
|
+
/// The epoch
|
|
20
|
+
epoch?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const DuckDBConnectionProvider: React.FC<DuckDBConnectionProps> = (props: DuckDBConnectionProps) => {
|
|
24
|
+
const db = useDuckDB();
|
|
25
|
+
const launchDB = useDuckDBLauncher();
|
|
26
|
+
const launched = React.useRef<boolean>();
|
|
27
|
+
const [pending, setPending] = React.useState<imm.List<number>>(imm.List());
|
|
28
|
+
const [pool, setPool] = React.useState<imm.Map<number, duckdb.AsyncDuckDBConnection>>(imm.Map());
|
|
29
|
+
|
|
30
|
+
const inFlight = React.useRef<Map<number, boolean>>(new Map());
|
|
31
|
+
|
|
32
|
+
// Resolve request assuming that the database is ready
|
|
33
|
+
const dialer = async (id: number) => {
|
|
34
|
+
if (inFlight.current.get(id)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const conn = await db!.connect();
|
|
38
|
+
setPool(p => p.set(id, conn));
|
|
39
|
+
inFlight.current.delete(id);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Resolve request or remember as pending
|
|
43
|
+
const dialerCallback = React.useCallback(
|
|
44
|
+
(id?: number) => {
|
|
45
|
+
if (db != null) {
|
|
46
|
+
dialer(id || 0);
|
|
47
|
+
} else {
|
|
48
|
+
if (!launched.current) {
|
|
49
|
+
launched.current = true;
|
|
50
|
+
launchDB();
|
|
51
|
+
}
|
|
52
|
+
setPending(pending.push(id || 0));
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
[db],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Process pending if possible
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
if (!db) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const claimed = pending;
|
|
64
|
+
setPending(imm.List());
|
|
65
|
+
for (const id of claimed) {
|
|
66
|
+
dialer(id);
|
|
67
|
+
}
|
|
68
|
+
}, [db, pending]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<dialerCtx.Provider value={dialerCallback}>
|
|
72
|
+
<poolCtx.Provider value={pool}>{props.children}</poolCtx.Provider>
|
|
73
|
+
</dialerCtx.Provider>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React, { ReactElement } from 'react';
|
|
2
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
3
|
+
import { useDuckDBLogger, useDuckDBBundle } from './platform_provider';
|
|
4
|
+
|
|
5
|
+
export interface DuckDBStatus {
|
|
6
|
+
instantiationProgress: duckdb.InstantiationProgress | null;
|
|
7
|
+
instantiationError: any | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const dbCtx = React.createContext<duckdb.AsyncDuckDB | null>(null);
|
|
11
|
+
const statusCtx = React.createContext<DuckDBStatus | null>(null);
|
|
12
|
+
|
|
13
|
+
export const useDuckDB = (): duckdb.AsyncDuckDB | null => React.useContext(dbCtx);
|
|
14
|
+
export const useDuckDBStatus = (): DuckDBStatus | null => React.useContext(statusCtx);
|
|
15
|
+
|
|
16
|
+
type DuckDBProps = {
|
|
17
|
+
children: React.ReactElement | ReactElement[];
|
|
18
|
+
config?: duckdb.DuckDBConfig;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const DuckDBProvider: React.FC<DuckDBProps> = (props: DuckDBProps) => {
|
|
22
|
+
const logger = useDuckDBLogger();
|
|
23
|
+
const bundle = useDuckDBBundle();
|
|
24
|
+
const [db, setDb] = React.useState<duckdb.AsyncDuckDB | null>(null);
|
|
25
|
+
const [status, setStatus] = React.useState<DuckDBStatus | null>(null);
|
|
26
|
+
|
|
27
|
+
// Reinitialize the worker and the database when the bundle changes
|
|
28
|
+
const lock = React.useRef<boolean>(false);
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
// No bundle available?
|
|
31
|
+
if (!bundle) return;
|
|
32
|
+
// Is updating?
|
|
33
|
+
if (lock.current) return;
|
|
34
|
+
lock.current = true;
|
|
35
|
+
|
|
36
|
+
// Create worker and next database
|
|
37
|
+
let worker: Worker;
|
|
38
|
+
let next: duckdb.AsyncDuckDB;
|
|
39
|
+
try {
|
|
40
|
+
worker = new Worker(bundle.mainWorker!);
|
|
41
|
+
next = new duckdb.AsyncDuckDB(logger, worker);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
lock.current = false;
|
|
44
|
+
setStatus({
|
|
45
|
+
instantiationProgress: null,
|
|
46
|
+
instantiationError: e,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Instantiate the database asynchronously
|
|
52
|
+
const init = async () => {
|
|
53
|
+
try {
|
|
54
|
+
await next.instantiate(bundle.mainModule, bundle.pthreadWorker, (p: duckdb.InstantiationProgress) => {
|
|
55
|
+
try {
|
|
56
|
+
setStatus({
|
|
57
|
+
instantiationProgress: p,
|
|
58
|
+
instantiationError: null,
|
|
59
|
+
});
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
console.warn(`progress handler failed with error: ${e.toString()}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (props.config !== undefined) {
|
|
65
|
+
await next.open(props.config!);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
lock.current = false;
|
|
69
|
+
setStatus({
|
|
70
|
+
instantiationProgress: null,
|
|
71
|
+
instantiationError: e,
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
lock.current = false;
|
|
76
|
+
setDb(next);
|
|
77
|
+
};
|
|
78
|
+
init();
|
|
79
|
+
// Terminate the worker on destruction
|
|
80
|
+
return () => {
|
|
81
|
+
worker.terminate();
|
|
82
|
+
};
|
|
83
|
+
}, [logger, bundle]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<dbCtx.Provider value={db}>
|
|
87
|
+
<statusCtx.Provider value={status}>{props.children}</statusCtx.Provider>
|
|
88
|
+
</dbCtx.Provider>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export const TABLE_SCHEMA_EPOCH = React.createContext<number | null>(null);
|
|
4
|
+
export const useTableSchemaEpoch = (): number | null => React.useContext(TABLE_SCHEMA_EPOCH);
|
|
5
|
+
|
|
6
|
+
export const TABLE_DATA_EPOCH = React.createContext<number | null>(null);
|
|
7
|
+
export const useTableDataEpoch = (): number | null => React.useContext(TABLE_DATA_EPOCH);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
3
|
+
|
|
4
|
+
type PlatformProps = {
|
|
5
|
+
children: React.ReactElement | React.ReactElement[];
|
|
6
|
+
logger: duckdb.Logger;
|
|
7
|
+
bundles: duckdb.DuckDBBundles;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const loggerCtx = React.createContext<duckdb.Logger | null>(null);
|
|
11
|
+
const bundleCtx = React.createContext<duckdb.DuckDBBundle | null>(null);
|
|
12
|
+
const launcherCtx = React.createContext<(() => void) | null>(null);
|
|
13
|
+
export const useDuckDBLauncher = (): (() => void) => React.useContext(launcherCtx)!;
|
|
14
|
+
export const useDuckDBLogger = (): duckdb.Logger => React.useContext(loggerCtx)!;
|
|
15
|
+
export const useDuckDBBundle = (): duckdb.DuckDBBundle => React.useContext(bundleCtx)!;
|
|
16
|
+
|
|
17
|
+
export const DuckDBPlatform: React.FC<PlatformProps> = (props: PlatformProps) => {
|
|
18
|
+
const [bundle, setBundle] = React.useState<duckdb.DuckDBBundle | null>(null);
|
|
19
|
+
const [launched, setLaunched] = React.useState<boolean>(false);
|
|
20
|
+
const lock = React.useRef<boolean>(false);
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
if (!launched || bundle != null || lock.current) return;
|
|
23
|
+
lock.current = true;
|
|
24
|
+
(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const b = await duckdb.selectBundle(props.bundles);
|
|
27
|
+
setBundle(b);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(e);
|
|
30
|
+
}
|
|
31
|
+
lock.current = false;
|
|
32
|
+
})();
|
|
33
|
+
}, [launched, props.bundles]);
|
|
34
|
+
const launcher = React.useCallback(() => setLaunched(true), [setLaunched]);
|
|
35
|
+
return (
|
|
36
|
+
<loggerCtx.Provider value={props.logger}>
|
|
37
|
+
<launcherCtx.Provider value={launcher}>
|
|
38
|
+
<bundleCtx.Provider value={bundle}>{props.children}</bundleCtx.Provider>
|
|
39
|
+
</launcherCtx.Provider>
|
|
40
|
+
</loggerCtx.Provider>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as arrow from 'apache-arrow';
|
|
2
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
3
|
+
|
|
4
|
+
/// A column group
|
|
5
|
+
export interface TableSchemaColumnGroup {
|
|
6
|
+
/// The group title
|
|
7
|
+
title: string;
|
|
8
|
+
/// The begin of the column span
|
|
9
|
+
spanBegin: number;
|
|
10
|
+
/// The size of the column span
|
|
11
|
+
spanSize: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// A table metadatStorea
|
|
15
|
+
export interface TableSchema {
|
|
16
|
+
/// The table schema
|
|
17
|
+
readonly tableSchema: string;
|
|
18
|
+
/// The table name
|
|
19
|
+
readonly tableName: string;
|
|
20
|
+
|
|
21
|
+
/// The column names
|
|
22
|
+
readonly columnNames: string[];
|
|
23
|
+
/// The column name indices
|
|
24
|
+
readonly columnNameMapping: Map<string, number>;
|
|
25
|
+
/// The column types
|
|
26
|
+
readonly columnTypes: arrow.DataType[];
|
|
27
|
+
/// The number of data columns.
|
|
28
|
+
/// Allows to append compute metadata columns that are not rendered in the table viewer.
|
|
29
|
+
readonly dataColumns: number;
|
|
30
|
+
|
|
31
|
+
/// The column aliases (if any)
|
|
32
|
+
readonly columnAliases: (string | null)[];
|
|
33
|
+
/// The column grouping sets (if any)
|
|
34
|
+
readonly columnGroupingSets: TableSchemaColumnGroup[][];
|
|
35
|
+
/// The row grouping sets (if any)
|
|
36
|
+
readonly rowGroupingSets: number[][];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Get raw qualified name
|
|
40
|
+
export function getQualifiedNameRaw(schema: string, name: string) {
|
|
41
|
+
return `${schema || 'main'}.${name}`;
|
|
42
|
+
}
|
|
43
|
+
/// Get qualified name
|
|
44
|
+
export function getQualifiedName(table: TableSchema) {
|
|
45
|
+
return `${table.tableSchema}.${table.tableName}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Collect table info
|
|
49
|
+
export async function collectTableSchema(
|
|
50
|
+
conn: duckdb.AsyncDuckDBConnection,
|
|
51
|
+
info: Partial<TableSchema> & { tableSchema?: string; tableName: string },
|
|
52
|
+
): Promise<TableSchema> {
|
|
53
|
+
// Use DESCRIBE to find all column types
|
|
54
|
+
const columnNames: string[] = [];
|
|
55
|
+
const columnNameMapping: Map<string, number> = new Map();
|
|
56
|
+
const columnTypes: arrow.DataType[] = [];
|
|
57
|
+
const describe = await conn.query<{ Field: arrow.Utf8; Type: arrow.Utf8 }>(
|
|
58
|
+
`DESCRIBE ${info.tableSchema || 'main'}.${info.tableName}`,
|
|
59
|
+
);
|
|
60
|
+
let column = 0;
|
|
61
|
+
for (const row of describe) {
|
|
62
|
+
columnNames.push(row!.Field!);
|
|
63
|
+
columnNameMapping.set(row!.Field!, column++);
|
|
64
|
+
const mapType = (type: string): arrow.DataType => {
|
|
65
|
+
switch (type) {
|
|
66
|
+
case 'BOOLEAN':
|
|
67
|
+
return new arrow.Bool();
|
|
68
|
+
case 'TINYINT':
|
|
69
|
+
return new arrow.Int8();
|
|
70
|
+
case 'SMALLINT':
|
|
71
|
+
return new arrow.Int16();
|
|
72
|
+
case 'INTEGER':
|
|
73
|
+
return new arrow.Int32();
|
|
74
|
+
case 'BIGINT':
|
|
75
|
+
return new arrow.Int64();
|
|
76
|
+
case 'UTINYINT':
|
|
77
|
+
return new arrow.Uint8();
|
|
78
|
+
case 'USMALLINT':
|
|
79
|
+
return new arrow.Uint16();
|
|
80
|
+
case 'UINTEGER':
|
|
81
|
+
return new arrow.Uint32();
|
|
82
|
+
case 'UBIGINT':
|
|
83
|
+
return new arrow.Uint64();
|
|
84
|
+
case 'FLOAT':
|
|
85
|
+
return new arrow.Float32();
|
|
86
|
+
case 'HUGEINT':
|
|
87
|
+
return new arrow.Decimal(32, 0);
|
|
88
|
+
case 'DOUBLE':
|
|
89
|
+
return new arrow.Float64();
|
|
90
|
+
case 'VARCHAR':
|
|
91
|
+
return new arrow.Utf8();
|
|
92
|
+
case 'DATE':
|
|
93
|
+
return new arrow.DateDay();
|
|
94
|
+
case 'TIME':
|
|
95
|
+
return new arrow.Time(arrow.TimeUnit.MILLISECOND, 32);
|
|
96
|
+
case 'TIMESTAMP':
|
|
97
|
+
return new arrow.TimeNanosecond();
|
|
98
|
+
default:
|
|
99
|
+
return new arrow.Null();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
columnTypes.push(mapType(row!.Type!));
|
|
103
|
+
}
|
|
104
|
+
const table: TableSchema = {
|
|
105
|
+
...info,
|
|
106
|
+
tableSchema: info.tableSchema || 'main',
|
|
107
|
+
columnNames,
|
|
108
|
+
columnTypes,
|
|
109
|
+
dataColumns: columnTypes.length,
|
|
110
|
+
columnNameMapping,
|
|
111
|
+
columnAliases: [],
|
|
112
|
+
columnGroupingSets: [],
|
|
113
|
+
rowGroupingSets: [],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return table;
|
|
117
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { TableSchema, collectTableSchema } from './table_schema';
|
|
3
|
+
import { useTableSchemaEpoch } from './epoch_contexts';
|
|
4
|
+
import { useDuckDBConnection } from './connection_provider';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/// The children
|
|
8
|
+
children: React.ReactElement | React.ReactElement[];
|
|
9
|
+
/// The schema
|
|
10
|
+
schema?: string;
|
|
11
|
+
/// The name
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface State {
|
|
16
|
+
/// The schema
|
|
17
|
+
schema: string | null;
|
|
18
|
+
/// The name
|
|
19
|
+
name: string | null;
|
|
20
|
+
/// The metadata
|
|
21
|
+
metadata: TableSchema | null;
|
|
22
|
+
/// The own epoch
|
|
23
|
+
ownEpoch: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const TABLE_METADATA = React.createContext<TableSchema | null>(null);
|
|
27
|
+
export const useTableSchema = (): TableSchema | null => React.useContext(TABLE_METADATA);
|
|
28
|
+
|
|
29
|
+
export const DuckDBTableSchemaProvider: React.FC<Props> = (props: Props) => {
|
|
30
|
+
const conn = useDuckDBConnection();
|
|
31
|
+
const epoch = useTableSchemaEpoch();
|
|
32
|
+
const [state, setState] = React.useState<State>({
|
|
33
|
+
schema: null,
|
|
34
|
+
name: null,
|
|
35
|
+
ownEpoch: epoch,
|
|
36
|
+
metadata: null,
|
|
37
|
+
});
|
|
38
|
+
const inFlight = React.useRef<boolean>(false);
|
|
39
|
+
|
|
40
|
+
// Detect unmount
|
|
41
|
+
const isMounted = React.useRef(true);
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
return () => void (isMounted.current = false);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// Resolve the metadata
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
if (!isMounted.current || !conn || inFlight.current) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
inFlight.current = true;
|
|
52
|
+
const resolve = async (schema: string, name: string, epoch: number | null) => {
|
|
53
|
+
const metadata = await collectTableSchema(conn!, {
|
|
54
|
+
tableSchema: schema,
|
|
55
|
+
tableName: name,
|
|
56
|
+
});
|
|
57
|
+
inFlight.current = false;
|
|
58
|
+
if (!isMounted.current) return;
|
|
59
|
+
setState({
|
|
60
|
+
schema,
|
|
61
|
+
name,
|
|
62
|
+
ownEpoch: epoch,
|
|
63
|
+
metadata,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
resolve(props.schema || 'main', props.name, epoch).catch(e => console.error(e));
|
|
67
|
+
}, [conn, props.schema, props.name, epoch]);
|
|
68
|
+
|
|
69
|
+
return <TABLE_METADATA.Provider value={state.metadata}>{props.children}</TABLE_METADATA.Provider>;
|
|
70
|
+
};
|