Skip to content

Commit f2781ba

Browse files
authored
JSONEachRowWithProgress (#334)
1 parent 1c9b28b commit f2781ba

File tree

15 files changed

+173
-27
lines changed

15 files changed

+173
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
## New features
88

9+
- Added `JSONEachRowWithProgress` format support, `ProgressRow` interface, and `isProgressRow` type guard. See [this Node.js example](./examples/node/select_json_each_row_with_progress.ts) for more details. It should work similarly with the Web version.
910
- (Experimental) Exposed the `parseColumnType` function that takes a string representation of a ClickHouse type (e.g., `FixedString(16)`, `Nullable(Int32)`, etc.) and returns an AST-like object that represents the type. For example:
1011

1112
```ts

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ If something is missing, or you found a mistake in one of these examples, please
5757
- [select_streaming_json_each_row.ts](node/select_streaming_json_each_row.ts) - (Node.js only) streaming JSON\* formats from ClickHouse and processing it with `on('data')` event.
5858
- [select_streaming_json_each_row_for_await.ts](node/select_streaming_json_each_row_for_await.ts) - (Node.js only) similar to [select_streaming_json_each_row.ts](node/select_streaming_json_each_row.ts), but using the `for await` loop syntax.
5959
- [select_streaming_text_line_by_line.ts](node/select_streaming_text_line_by_line.ts) - (Node.js only) streaming text formats from ClickHouse and processing it line by line. In this example, CSV format is used.
60+
- [select_json_each_row_with_progress.ts](node/select_json_each_row_with_progress.ts) - streaming using `JSONEachRowWithProgress` format, checking for the progress rows in the stream.
6061

6162
#### Data types
6263

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createClient } from '@clickhouse/client'
2+
import { isProgressRow } from '@clickhouse/client-common'
3+
4+
/** See the format spec - https://clickhouse.com/docs/en/interfaces/formats#jsoneachrowwithprogress
5+
* When JSONEachRowWithProgress format is used in TypeScript,
6+
* the ResultSet should infer the final row type as `{ row: Data } | ProgressRow`. */
7+
type Data = { number: string }
8+
9+
void (async () => {
10+
const client = createClient()
11+
const rs = await client.query({
12+
query: 'SELECT number FROM system.numbers LIMIT 100',
13+
format: 'JSONEachRowWithProgress',
14+
})
15+
16+
let totalRows = 0
17+
let totalProgressRows = 0
18+
19+
const stream = rs.stream<Data>()
20+
for await (const rows of stream) {
21+
for (const row of rows) {
22+
const decodedRow = row.json()
23+
if (isProgressRow(decodedRow)) {
24+
console.log('Got a progress row:', decodedRow)
25+
totalProgressRows++
26+
} else {
27+
totalRows++
28+
if (totalRows % 100 === 0) {
29+
console.log('Sample row:', decodedRow)
30+
}
31+
}
32+
}
33+
}
34+
35+
console.log('Total rows:', totalRows)
36+
console.log('Total progress rows:', totalProgressRows)
37+
38+
await client.close()
39+
})()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { isProgressRow } from '@clickhouse/client-common'
2+
3+
describe('ClickHouse types', () => {
4+
it('should check if a row is progress row', async () => {
5+
const row = {
6+
progress: {
7+
read_rows: '1',
8+
read_bytes: '1',
9+
written_rows: '1',
10+
written_bytes: '1',
11+
total_rows_to_read: '1',
12+
result_rows: '1',
13+
result_bytes: '1',
14+
elapsed_ns: '1',
15+
},
16+
}
17+
expect(isProgressRow(row)).toBeTruthy()
18+
expect(isProgressRow({})).toBeFalsy()
19+
expect(
20+
isProgressRow({
21+
...row,
22+
extra: 'extra',
23+
}),
24+
).toBeFalsy()
25+
expect(isProgressRow(null)).toBeFalsy()
26+
expect(isProgressRow(42)).toBeFalsy()
27+
expect(isProgressRow({ foo: 'bar' })).toBeFalsy()
28+
})
29+
})

packages/client-common/src/clickhouse_types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,19 @@ export interface WithClickHouseSummary {
4040
export interface WithResponseHeaders {
4141
response_headers: ResponseHeaders
4242
}
43+
44+
/** X-ClickHouse-Summary response header and progress rows from JSONEachRowWithProgress share the same structure */
45+
export interface ProgressRow {
46+
progress: ClickHouseSummary
47+
}
48+
49+
/** Type guard to use with JSONEachRowWithProgress, checking if the emitted row is a progress row.
50+
* @see https://clickhouse.com/docs/en/interfaces/formats#jsoneachrowwithprogress */
51+
export function isProgressRow(row: unknown): row is ProgressRow {
52+
return (
53+
row !== null &&
54+
typeof row === 'object' &&
55+
'progress' in row &&
56+
Object.keys(row).length === 1
57+
)
58+
}

packages/client-common/src/data_formatter/formatter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const StreamableJSONFormats = [
77
'JSONCompactEachRowWithNamesAndTypes',
88
'JSONCompactStringsEachRowWithNames',
99
'JSONCompactStringsEachRowWithNamesAndTypes',
10+
'JSONEachRowWithProgress',
1011
] as const
1112
export const RecordsJSONFormats = ['JSONObjectEachRow'] as const
1213
export const SingleDocumentJSONFormats = [

packages/client-common/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {
1616
export { type BaseClickHouseClientConfigOptions } from './config'
1717
export type {
1818
Row,
19+
RowOrProgress,
1920
BaseResultSet,
2021
ResultJSONType,
2122
RowJSONType,
@@ -51,7 +52,9 @@ export type {
5152
ResponseHeaders,
5253
WithClickHouseSummary,
5354
WithResponseHeaders,
55+
ProgressRow,
5456
} from './clickhouse_types'
57+
export { isProgressRow } from './clickhouse_types'
5558
export {
5659
type ClickHouseSettings,
5760
type MergeTreeSettings,

packages/client-common/src/result.ts

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { ResponseHeaders, ResponseJSON } from './clickhouse_types'
1+
import type {
2+
ProgressRow,
3+
ResponseHeaders,
4+
ResponseJSON,
5+
} from './clickhouse_types'
26
import type {
37
DataFormat,
48
RawDataFormat,
@@ -8,6 +12,8 @@ import type {
812
StreamableJSONDataFormat,
913
} from './data_formatter'
1014

15+
export type RowOrProgress<T> = { row: T } | ProgressRow
16+
1117
export type ResultStream<Format extends DataFormat | unknown, Stream> =
1218
// JSON*EachRow (except JSONObjectEachRow), CSV, TSV etc.
1319
Format extends StreamableDataFormat
@@ -22,29 +28,35 @@ export type ResultStream<Format extends DataFormat | unknown, Stream> =
2228
Stream
2329

2430
export type ResultJSONType<T, F extends DataFormat | unknown> =
25-
// JSON*EachRow formats except JSONObjectEachRow
26-
F extends StreamableJSONDataFormat
27-
? T[]
28-
: // JSON formats with known layout { data, meta, statistics, ... }
29-
F extends SingleDocumentJSONFormat
30-
? ResponseJSON<T>
31-
: // JSON formats represented as a Record<string, T>
32-
F extends RecordsJSONFormat
33-
? Record<string, T>
34-
: // CSV, TSV etc. - cannot be represented as JSON
35-
F extends RawDataFormat
36-
? never
37-
: // happens only when Format could not be inferred from a literal
38-
T[] | Record<string, T> | ResponseJSON<T>
31+
// Emits either a { row: T } or an object with progress
32+
F extends 'JSONEachRowWithProgress'
33+
? RowOrProgress<T>[]
34+
: // JSON*EachRow formats except JSONObjectEachRow
35+
F extends StreamableJSONDataFormat
36+
? T[]
37+
: // JSON formats with known layout { data, meta, statistics, ... }
38+
F extends SingleDocumentJSONFormat
39+
? ResponseJSON<T>
40+
: // JSON formats represented as a Record<string, T>
41+
F extends RecordsJSONFormat
42+
? Record<string, T>
43+
: // CSV, TSV etc. - cannot be represented as JSON
44+
F extends RawDataFormat
45+
? never
46+
: // happens only when Format could not be inferred from a literal
47+
T[] | Record<string, T> | ResponseJSON<T>
3948

4049
export type RowJSONType<T, F extends DataFormat | unknown> =
41-
// JSON*EachRow formats
42-
F extends StreamableJSONDataFormat
43-
? T
44-
: // CSV, TSV, non-streamable JSON formats - cannot be streamed as JSON
45-
F extends RawDataFormat | SingleDocumentJSONFormat | RecordsJSONFormat
46-
? never
47-
: T // happens only when Format could not be inferred from a literal
50+
// Emits either a { row: T } or an object with progress
51+
F extends 'JSONEachRowWithProgress'
52+
? RowOrProgress<T>
53+
: // JSON*EachRow formats
54+
F extends StreamableJSONDataFormat
55+
? T
56+
: // CSV, TSV, non-streamable JSON formats - cannot be streamed as JSON
57+
F extends RawDataFormat | SingleDocumentJSONFormat | RecordsJSONFormat
58+
? never
59+
: T // happens only when Format could not be inferred from a literal
4860

4961
export interface Row<
5062
JSONType = unknown,

packages/client-node/__tests__/integration/node_query_format_types.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import type { ResultSet } from '../../src'
21
import type {
32
ClickHouseClient as BaseClickHouseClient,
43
DataFormat,
54
} from '@clickhouse/client-common'
65
import { createTableWithFields } from '@test/fixtures/table_with_fields'
76
import { guid } from '@test/utils'
8-
import type { ClickHouseClient } from '../../src'
7+
import type { ClickHouseClient, ResultSet } from '../../src'
98
import { createNodeTestClient } from '../utils/node_client'
109

1110
/* eslint-disable @typescript-eslint/no-unused-expressions */

packages/client-node/__tests__/integration/node_stream_json_formats.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ClickHouseClient } from '@clickhouse/client-common'
1+
import { type ClickHouseClient, isProgressRow } from '@clickhouse/client-common'
22
import { createSimpleTable } from '@test/fixtures/simple_table'
33
import { assertJsonValues, jsonValues } from '@test/fixtures/test_data'
44
import { createTestClient, guid } from '@test/utils'
@@ -231,6 +231,26 @@ describe('[Node.js] stream JSON formats', () => {
231231
})
232232
})
233233

234+
describe('JSONEachRowWithProgress', () => {
235+
it('should work', async () => {
236+
const limit = 2
237+
const expectedProgressRowsCount = 4
238+
const rs = await client.query({
239+
query: `SELECT number FROM system.numbers LIMIT ${limit}`,
240+
format: 'JSONEachRowWithProgress',
241+
clickhouse_settings: {
242+
max_block_size: '1', // reduce the block size, so the progress is reported more frequently
243+
},
244+
})
245+
const rows = await rs.json<{ number: 'string' }>()
246+
expect(rows.length).toEqual(limit + expectedProgressRowsCount)
247+
expect(rows.filter((r) => !isProgressRow(r)) as unknown[]).toEqual([
248+
{ row: { number: '0' } },
249+
{ row: { number: '1' } },
250+
])
251+
})
252+
})
253+
234254
it('does not throw if stream closes prematurely', async () => {
235255
const stream = new Stream.Readable({
236256
objectMode: true,

0 commit comments

Comments
 (0)