Skip to content

Commit c3247d0

Browse files
authored
Basic helper for ES|QL's Apache Arrow output format (#2391)
1 parent e9fdcb0 commit c3247d0

File tree

4 files changed

+89
-8
lines changed

4 files changed

+89
-8
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@
8787
"zx": "^7.2.2"
8888
},
8989
"dependencies": {
90-
"@elastic/transport": "^8.8.1",
90+
"@elastic/transport": "^8.9.0",
91+
"@apache-arrow/esnext-cjs": "^17.0.0",
9192
"tslib": "^2.4.0"
9293
},
9394
"tap": {

src/helpers.ts

+22-5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import assert from 'node:assert'
2525
import * as timersPromises from 'node:timers/promises'
2626
import { Readable } from 'node:stream'
2727
import { errors, TransportResult, TransportRequestOptions, TransportRequestOptionsWithMeta } from '@elastic/transport'
28+
import { Table, TypeMap, tableFromIPC } from '@apache-arrow/esnext-cjs'
2829
import Client from './client'
2930
import * as T from './api/types'
3031

@@ -155,6 +156,7 @@ export interface EsqlResponse {
155156

156157
export interface EsqlHelper {
157158
toRecords: <TDocument>() => Promise<EsqlToRecords<TDocument>>
159+
toArrow: () => Promise<Table<TypeMap>>
158160
}
159161

160162
export interface EsqlToRecords<TDocument> {
@@ -965,11 +967,6 @@ export default class Helpers {
965967
* @returns {object} EsqlHelper instance
966968
*/
967969
esql (params: T.EsqlQueryRequest, reqOptions: TransportRequestOptions = {}): EsqlHelper {
968-
if (this[kMetaHeader] !== null) {
969-
reqOptions.headers = reqOptions.headers ?? {}
970-
reqOptions.headers['x-elastic-client-meta'] = `${this[kMetaHeader] as string},h=qo`
971-
}
972-
973970
const client = this[kClient]
974971

975972
function toRecords<TDocument> (response: EsqlResponse): TDocument[] {
@@ -985,17 +982,37 @@ export default class Helpers {
985982
})
986983
}
987984

985+
const metaHeader = this[kMetaHeader]
986+
988987
const helper: EsqlHelper = {
989988
/**
990989
* Pivots ES|QL query results into an array of row objects, rather than the default format where each row is an array of values.
991990
*/
992991
async toRecords<TDocument>(): Promise<EsqlToRecords<TDocument>> {
992+
if (metaHeader !== null) {
993+
reqOptions.headers = reqOptions.headers ?? {}
994+
reqOptions.headers['x-elastic-client-meta'] = `${metaHeader as string},h=qo`
995+
}
996+
993997
params.format = 'json'
998+
params.columnar = false
994999
// @ts-expect-error it's typed as ArrayBuffer but we know it will be JSON
9951000
const response: EsqlResponse = await client.esql.query(params, reqOptions)
9961001
const records: TDocument[] = toRecords(response)
9971002
const { columns } = response
9981003
return { records, columns }
1004+
},
1005+
1006+
async toArrow (): Promise<Table<TypeMap>> {
1007+
if (metaHeader !== null) {
1008+
reqOptions.headers = reqOptions.headers ?? {}
1009+
reqOptions.headers['x-elastic-client-meta'] = `${metaHeader as string},h=qa`
1010+
}
1011+
1012+
params.format = 'arrow'
1013+
1014+
const response = await client.esql.query(params, reqOptions)
1015+
return tableFromIPC(response)
9991016
}
10001017
}
10011018

test/unit/helpers/esql.test.ts

+62
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919

2020
import { test } from 'tap'
21+
import { Table } from '@apache-arrow/esnext-cjs'
2122
import { connection } from '../../utils'
2223
import { Client } from '../../../'
2324

@@ -109,5 +110,66 @@ test('ES|QL helper', t => {
109110

110111
t.end()
111112
})
113+
114+
test('toArrow', t => {
115+
t.test('Parses a binary response into an Arrow table', async t => {
116+
const binaryContent = '/////zABAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAIAAAB8AAAABAAAAJ7///8UAAAARAAAAEQAAAAAAAoBRAAAAAEAAAAEAAAAjP///wgAAAAQAAAABAAAAGRhdGUAAAAADAAAAGVsYXN0aWM6dHlwZQAAAAAAAAAAgv///wAAAQAEAAAAZGF0ZQAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABMAAAAVAAAAAAAAwFUAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAYAAABkb3VibGUAAAwAAABlbGFzdGljOnR5cGUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgAGAAAAYW1vdW50AAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAOABUAEAAEAAwAAABgAAAAAAAAAAAABAAQAAAAAAMKABgADAAIAAQACgAAABQAAABYAAAABQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAACgAAAAAAAAAMAAAAAAAAAABAAAAAAAAADgAAAAAAAAAKAAAAAAAAAAAAAAAAgAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAACgmZkTQAAAAGBmZiBAAAAAAAAAL0AAAADAzMwjQAAAAMDMzCtAHwAAAAAAAADV6yywkgEAANWPBquSAQAA1TPgpZIBAADV17mgkgEAANV7k5uSAQAA/////wAAAAA='
117+
118+
const MockConnection = connection.buildMockConnection({
119+
onRequest (_params) {
120+
return {
121+
body: Buffer.from(binaryContent, 'base64'),
122+
statusCode: 200,
123+
headers: {
124+
'content-type': 'application/vnd.elasticsearch+arrow+stream'
125+
}
126+
}
127+
}
128+
})
129+
130+
const client = new Client({
131+
node: 'http://localhost:9200',
132+
Connection: MockConnection
133+
})
134+
135+
const result = await client.helpers.esql({ query: 'FROM sample_data' }).toArrow()
136+
t.ok(result instanceof Table)
137+
138+
const table = [...result]
139+
t.same(table[0], [
140+
["amount", 4.900000095367432],
141+
["date", 1729532586965],
142+
])
143+
t.end()
144+
})
145+
146+
t.test('ESQL helper uses correct x-elastic-client-meta helper value', async t => {
147+
const binaryContent = '/////zABAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAIAAAB8AAAABAAAAJ7///8UAAAARAAAAEQAAAAAAAoBRAAAAAEAAAAEAAAAjP///wgAAAAQAAAABAAAAGRhdGUAAAAADAAAAGVsYXN0aWM6dHlwZQAAAAAAAAAAgv///wAAAQAEAAAAZGF0ZQAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABMAAAAVAAAAAAAAwFUAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAYAAABkb3VibGUAAAwAAABlbGFzdGljOnR5cGUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgAGAAAAYW1vdW50AAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAOABUAEAAEAAwAAABgAAAAAAAAAAAABAAQAAAAAAMKABgADAAIAAQACgAAABQAAABYAAAABQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAACgAAAAAAAAAMAAAAAAAAAABAAAAAAAAADgAAAAAAAAAKAAAAAAAAAAAAAAAAgAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAHwAAAAAAAAAAAACgmZkTQAAAAGBmZiBAAAAAAAAAL0AAAADAzMwjQAAAAMDMzCtAHwAAAAAAAADV6yywkgEAANWPBquSAQAA1TPgpZIBAADV17mgkgEAANV7k5uSAQAA/////wAAAAA='
148+
149+
const MockConnection = connection.buildMockConnection({
150+
onRequest (params) {
151+
const header = params.headers?.['x-elastic-client-meta'] ?? ''
152+
t.ok(header.includes('h=qa'), `Client meta header does not include ESQL helper value: ${header}`)
153+
return {
154+
body: Buffer.from(binaryContent, 'base64'),
155+
statusCode: 200,
156+
headers: {
157+
'content-type': 'application/vnd.elasticsearch+arrow+stream'
158+
}
159+
}
160+
}
161+
})
162+
163+
const client = new Client({
164+
node: 'http://localhost:9200',
165+
Connection: MockConnection
166+
})
167+
168+
await client.helpers.esql({ query: 'FROM sample_data' }).toArrow()
169+
t.end()
170+
})
171+
172+
t.end()
173+
})
112174
t.end()
113175
})

tsconfig.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"compilerOptions": {
3-
"target": "es2019",
3+
"target": "ES2019",
44
"module": "commonjs",
55
"moduleResolution": "node",
66
"declaration": true,
@@ -21,7 +21,8 @@
2121
"importHelpers": true,
2222
"outDir": "lib",
2323
"lib": [
24-
"esnext"
24+
"ES2019",
25+
"dom"
2526
]
2627
},
2728
"formatCodeOptions": {

0 commit comments

Comments
 (0)