Skip to content

Commit 59ccc45

Browse files
authored
feat(javascript): return Arrow Table by default in AdbcConnection (#4091)
This PR cleans up the AdbcConnection API so that the common usage is more ergonomic. The high-level connection methods now return an Arrow Table by default, consistent with how most TypeScript database clients behave. Users who need streaming or fine-grained control can use queryStream() or drop down to the statement-level API. AdbcConnection Interface changes | Method | Before | After | |--------|--------|-------| | `query(sql, params?)` | `params?: RecordBatch \| Table` → `Promise<RecordBatchReader>` | `params?: Table` → `Promise<Table>` | | `queryStream(sql, params?)` | _(new)_ | `params?: Table` → `Promise<RecordBatchReader>` | | `execute(sql, params?)` | `params?: RecordBatch \| Table` → `Promise<number>` | `params?: Table` → `Promise<number>` | | `getObjects(options?)` | `Promise<RecordBatchReader>` | `Promise<Table>` | | `getTableTypes()` | `Promise<RecordBatchReader>` | `Promise<Table>` | | `getInfo(infoCodes?)` | `Promise<RecordBatchReader>` | `Promise<Table>` | Closes #4090
1 parent 3115969 commit 59ccc45

7 files changed

Lines changed: 229 additions & 71 deletions

File tree

javascript/README.md

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Node.js bindings for the [Arrow Database Connectivity (ADBC)](https://arrow.apac
2323
Built on a native [NAPI](https://nodejs.org/api/n-api.html) addon — requires Node.js 22+ and does not
2424
support browser or Deno environments. Bun is not officially tested.
2525

26-
**Alpha: APIs may change without notice.**
26+
> **Alpha.** APIs may change without notice.
27+
> If you try this and run into issues or have feedback, please [open an issue](https://github.com/apache/arrow-adbc/issues).
2728
2829
## Installation
2930

@@ -40,39 +41,52 @@ paths for a matching ADBC driver manifest or library.
4041
```typescript
4142
import { AdbcDatabase } from 'adbc-driver-manager'
4243

43-
// Full path to a driver shared library
44-
const db = new AdbcDatabase({ driver: '/path/to/libadbc_driver_sqlite.dylib' })
44+
// Short name (resolves from system/user paths)
45+
const db = new AdbcDatabase({ driver: 'sqlite' })
4546
```
4647

4748
```typescript
48-
// Short name (resolves from system/user paths)
49-
const db = new AdbcDatabase({ driver: 'sqlite' })
49+
// Or a full path to a driver shared library
50+
const db = new AdbcDatabase({ driver: '/path/to/libadbc_driver_sqlite.dylib' })
5051
```
5152

5253
Once you have a database, open a connection and run queries:
5354

5455
```typescript
5556
const connection = await db.connect()
5657

57-
// Execute a query and iterate Arrow RecordBatches
58-
const reader = await connection.query('SELECT 1 AS value')
59-
for await (const batch of reader) {
60-
console.log(batch.toArray())
61-
}
58+
// Execute a query — returns an Apache Arrow Table
59+
const table = await connection.query('SELECT 1 AS value')
60+
console.log(table.toArray())
6261

63-
// Or use the lower-level statement API
64-
const stmt = await connection.createStatement()
65-
await stmt.setSqlQuery('SELECT 1 AS value')
66-
const result = await stmt.executeQuery()
67-
for await (const batch of result) {
62+
// For large result sets, stream record batches instead
63+
const reader = await connection.queryStream('SELECT * FROM large_table')
64+
for await (const batch of reader) {
6865
console.log(`Received batch with ${batch.numRows} rows`)
6966
}
70-
await stmt.close()
67+
68+
// DML — returns the number of affected rows
69+
const affected = await connection.execute('DELETE FROM my_table WHERE id = 1')
7170

7271
await connection.close()
7372
await db.close()
7473
```
7574

75+
For finer-grained control, use the statement API directly:
76+
77+
```typescript
78+
import { tableFromArrays } from 'apache-arrow'
79+
80+
const stmt = await connection.createStatement()
81+
await stmt.setSqlQuery('SELECT * FROM my_table WHERE id = ?')
82+
await stmt.bind(tableFromArrays({ id: [42] }))
83+
const reader = await stmt.executeQuery()
84+
for await (const batch of reader) {
85+
console.log(batch.toArray())
86+
}
87+
await stmt.close()
88+
```
89+
7690
## Development
7791

7892
### Prerequisites

javascript/__test__/error_handling.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,79 @@ test('error: unsupported option', () => {
114114
assert.strictEqual(error.vendorCode, undefined)
115115
assert.strictEqual(error.sqlState, undefined)
116116
})
117+
118+
test('error: conn.query() invalid SQL throws AdbcError', async () => {
119+
await assert.rejects(
120+
() => conn.query('SELECT * FROM'),
121+
(e: unknown) => {
122+
assert.ok(e instanceof AdbcError)
123+
assert.match(e.message, /syntax error|incomplete input/i)
124+
assert.strictEqual(e.code, 'InvalidArguments')
125+
return true
126+
},
127+
)
128+
})
129+
130+
test('error: conn.query() table not found throws AdbcError', async () => {
131+
await assert.rejects(
132+
() => conn.query('SELECT * FROM non_existent_table'),
133+
(e: unknown) => {
134+
assert.ok(e instanceof AdbcError)
135+
assert.match(e.message, /no such table/i)
136+
assert.strictEqual(e.code, 'InvalidArguments')
137+
return true
138+
},
139+
)
140+
})
141+
142+
test('error: conn.execute() invalid SQL throws AdbcError', async () => {
143+
await assert.rejects(
144+
() => conn.execute('INSERT INTO'),
145+
(e: unknown) => {
146+
assert.ok(e instanceof AdbcError)
147+
assert.match(e.message, /syntax error|incomplete input/i)
148+
assert.strictEqual(e.code, 'InvalidArguments')
149+
return true
150+
},
151+
)
152+
})
153+
154+
test('error: conn.execute() table not found throws AdbcError', async () => {
155+
await assert.rejects(
156+
() => conn.execute('INSERT INTO non_existent_table (id) VALUES (1)'),
157+
(e: unknown) => {
158+
assert.ok(e instanceof AdbcError)
159+
assert.match(e.message, /no such table/i)
160+
assert.strictEqual(e.code, 'InvalidArguments')
161+
return true
162+
},
163+
)
164+
})
165+
166+
test('error: conn.queryStream() invalid SQL throws AdbcError', async () => {
167+
await assert.rejects(
168+
() => conn.queryStream('SELECT * FROM'),
169+
(e: unknown) => {
170+
assert.ok(e instanceof AdbcError)
171+
assert.match(e.message, /syntax error|incomplete input/i)
172+
assert.strictEqual(e.code, 'InvalidArguments')
173+
return true
174+
},
175+
)
176+
})
177+
178+
test('error: conn.queryStream() error during iteration throws AdbcError', async () => {
179+
// Verifies that errors surfacing through the async iterator are wrapped as AdbcError,
180+
// not just errors thrown at query time.
181+
let error: unknown
182+
try {
183+
const reader = await conn.queryStream('SELECT * FROM non_existent_table')
184+
for await (const _ of reader) {
185+
}
186+
} catch (e) {
187+
error = e
188+
}
189+
190+
assert.ok(error instanceof AdbcError)
191+
assert.match((error as AdbcError).message, /no such table/i)
192+
})

javascript/__test__/metadata.spec.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
import { test, before, after } from 'node:test'
1919
import assert from 'node:assert/strict'
20-
import { createSqliteDatabase, createTestTable, dumpReader } from './test_utils'
20+
import { createSqliteDatabase, createTestTable } from './test_utils'
2121
import { AdbcDatabase, AdbcConnection, AdbcStatement } from '../lib/index.js'
22+
import { Table } from 'apache-arrow'
2223

2324
let db: AdbcDatabase
2425
let conn: AdbcConnection
@@ -42,13 +43,14 @@ after(async () => {
4243
})
4344

4445
test('metadata: getTableTypes', async () => {
45-
const tableTypes = await dumpReader(await conn.getTableTypes())
46+
const table = await conn.getTableTypes()
47+
assert.ok(table instanceof Table)
4648

47-
// Sort actual results for consistent comparison
48-
tableTypes.sort((a, b) => (a.table_type || '').localeCompare(b.table_type || ''))
49+
const types = Array.from({ length: table.numRows }, (_, i) => table.getChild('table_type')?.get(i) as string)
50+
types.sort()
4951

50-
assert.strictEqual(tableTypes[0].table_type, 'table')
51-
assert.strictEqual(tableTypes[1].table_type, 'view')
52+
assert.ok(types.includes('table'))
53+
assert.ok(types.includes('view'))
5254
})
5355

5456
test('metadata: getTableSchema', async () => {
@@ -62,21 +64,29 @@ test('metadata: getTableSchema', async () => {
6264

6365
test('metadata: getObjects', async () => {
6466
// SQLite structure: Catalog (null/main) -> Schemas (null/main) -> Tables
65-
const objects = await dumpReader(
66-
await conn.getObjects({
67-
depth: 3,
68-
tableName: 'metadata_test',
69-
tableType: ['table', 'view'],
70-
}),
71-
)
67+
const table = await conn.getObjects({
68+
depth: 3,
69+
tableName: 'metadata_test',
70+
tableType: ['table', 'view'],
71+
})
72+
assert.ok(table instanceof Table)
73+
assert.ok(table.numRows > 0)
7274

73-
const tables = objects[0].catalog_db_schemas[0].db_schema_tables
74-
assert.ok(tables.some((t: { table_name: string }) => t.table_name === 'metadata_test'))
75+
// Navigate the nested Arrow structure: catalog -> db_schemas -> tables
76+
const dbSchemas = table.getChild('catalog_db_schemas')?.get(0)
77+
const dbTables = dbSchemas?.get(0)?.db_schema_tables
78+
const tableNames = Array.from({ length: dbTables?.length ?? 0 }, (_, i) => dbTables?.get(i)?.table_name as string)
79+
assert.ok(tableNames.includes('metadata_test'))
7580
})
7681

7782
test('metadata: getInfo', async () => {
78-
const info = await dumpReader(await conn.getInfo())
83+
const table = await conn.getInfo()
84+
assert.ok(table instanceof Table)
85+
assert.ok(table.numRows > 0)
7986

80-
assert.strictEqual(info[0].info_name, 0)
81-
assert.strictEqual(info[0].info_value, 'SQLite')
87+
// Find the VendorName row (info_name === 0)
88+
const infoNames = table.getChild('info_name')
89+
const vendorNameRow = Array.from({ length: table.numRows }, (_, i) => i).find((i) => infoNames?.get(i) === 0)
90+
assert.ok(vendorNameRow !== undefined)
91+
assert.strictEqual(table.getChild('info_value')?.get(vendorNameRow), 'SQLite')
8292
})

javascript/__test__/profile.spec.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
2121
import { join, isAbsolute } from 'node:path'
2222
import { tmpdir } from 'node:os'
2323
import { AdbcDatabase } from '../lib/index.js'
24-
import { dumpReader } from './test_utils.js'
2524

2625
const testLib = process.env.ADBC_DRIVER_MANAGER_TEST_LIB
2726

@@ -52,10 +51,10 @@ test('profile: load database from profile:// URI', async () => {
5251
})
5352
const conn = await db.connect()
5453

55-
const rows = await dumpReader(await conn.query('SELECT id, value FROM profile_marker'))
56-
assert.strictEqual(rows.length, 1)
57-
assert.strictEqual(rows[0].id, 42n)
58-
assert.strictEqual(rows[0].value, 'from_profile')
54+
const table = await conn.query('SELECT id, value FROM profile_marker')
55+
assert.strictEqual(table.numRows, 1)
56+
assert.strictEqual(table.getChild('id')?.get(0), 42n)
57+
assert.strictEqual(table.getChild('value')?.get(0), 'from_profile')
5958

6059
await conn.close()
6160
await db.close()

javascript/__test__/query.spec.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { test, before, after } from 'node:test'
1919
import assert from 'node:assert/strict'
2020
import { createSqliteDatabase, createTestTable, dumpReader } from './test_utils'
2121
import { AdbcDatabase, AdbcConnection, AdbcStatement } from '../lib/index.js'
22-
import { tableFromArrays } from 'apache-arrow'
22+
import { Table, tableFromArrays } from 'apache-arrow'
2323

2424
let db: AdbcDatabase
2525
let conn: AdbcConnection
@@ -59,9 +59,9 @@ test('query: SELECT returns correct rows', async () => {
5959
}
6060

6161
assert.strictEqual(rows.length, 3)
62-
assert.strictEqual(rows[0].name, 'alice')
63-
assert.strictEqual(rows[1].name, 'bob')
64-
assert.strictEqual(rows[2].name, 'carol')
62+
assert.deepStrictEqual(rows[0], { id: 1n, name: 'alice' })
63+
assert.deepStrictEqual(rows[1], { id: 2n, name: 'bob' })
64+
assert.deepStrictEqual(rows[2], { id: 3n, name: 'carol' })
6565
})
6666

6767
test('query: executeUpdate returns affected row count', async () => {
@@ -71,31 +71,66 @@ test('query: executeUpdate returns affected row count', async () => {
7171
const affected = await stmt.executeUpdate()
7272
assert.strictEqual(typeof affected, 'number')
7373
assert.strictEqual(affected, 1)
74+
75+
// Verify the change was applied
76+
const table = await conn.query('SELECT id, name FROM query_test WHERE id = 1')
77+
assert.strictEqual(table.numRows, 1)
78+
assert.strictEqual(table.getChild('id')?.get(0), 1n)
79+
assert.strictEqual(table.getChild('name')?.get(0), 'updated')
7480
} finally {
7581
await stmt.close()
7682
}
7783
})
7884

79-
test('query: conn.query() returns correct rows', async () => {
85+
test('query: conn.query() returns an Arrow Table', async () => {
8086
// id=2 (bob) is never mutated by other tests in this file
81-
const rows = await dumpReader(await conn.query('SELECT id, name FROM query_test WHERE id = 2'))
87+
const table = await conn.query('SELECT id, name FROM query_test WHERE id = 2')
88+
assert.ok(table instanceof Table)
89+
assert.strictEqual(table.numCols, 2)
90+
assert.strictEqual(table.numRows, 1)
91+
assert.strictEqual(table.getChild('id')?.get(0), 2n)
92+
assert.strictEqual(table.getChild('name')?.get(0), 'bob')
93+
})
94+
95+
test('query: conn.query() with bound params', async () => {
96+
// id=2 (bob) is never mutated by other tests in this file
97+
const params = tableFromArrays({ id: [2] })
98+
const table = await conn.query('SELECT id, name FROM query_test WHERE id = ?', params)
99+
assert.ok(table instanceof Table)
100+
assert.strictEqual(table.numRows, 1)
101+
assert.strictEqual(table.getChild('id')?.get(0), 2n)
102+
assert.strictEqual(table.getChild('name')?.get(0), 'bob')
103+
})
104+
105+
test('query: conn.queryStream() returns a RecordBatchReader', async () => {
106+
// id=2 (bob) is never mutated by other tests in this file
107+
const reader = await conn.queryStream('SELECT id, name FROM query_test WHERE id = 2')
108+
const rows = await dumpReader(reader)
82109
assert.strictEqual(rows.length, 1)
110+
assert.strictEqual(rows[0].id, 2n)
83111
assert.strictEqual(rows[0].name, 'bob')
84112
})
85113

86114
test('query: conn.execute() returns affected row count', async () => {
87115
const affected = await conn.execute(`UPDATE query_test SET name = 'via_execute' WHERE id = 3`)
88116
assert.strictEqual(affected, 1)
117+
118+
// Verify the change was applied
119+
const table = await conn.query('SELECT id, name FROM query_test WHERE id = 3')
120+
assert.strictEqual(table.numRows, 1)
121+
assert.strictEqual(table.getChild('id')?.get(0), 3n)
122+
assert.strictEqual(table.getChild('name')?.get(0), 'via_execute')
89123
})
90124

91125
test('query: conn.execute() with bound params inserts a row', async () => {
92126
const params = tableFromArrays({ id: [99], name: ['bound_insert'] })
93127
const affected = await conn.execute('INSERT INTO query_test (id, name) VALUES (?, ?)', params)
94128
assert.strictEqual(affected, 1)
95129

96-
const rows = await dumpReader(await conn.query('SELECT name FROM query_test WHERE id = 99'))
97-
assert.strictEqual(rows.length, 1)
98-
assert.strictEqual(rows[0].name, 'bound_insert')
130+
const table = await conn.query('SELECT id, name FROM query_test WHERE id = 99')
131+
assert.strictEqual(table.numRows, 1)
132+
assert.strictEqual(table.getChild('id')?.get(0), 99n)
133+
assert.strictEqual(table.getChild('name')?.get(0), 'bound_insert')
99134
})
100135

101136
test('query: empty result set', async () => {

0 commit comments

Comments
 (0)