fix(clickhouse-io): use official @clickhouse/client instead of unmaintained clickhouse package (#2422)

* fix(clickhouse-io): use official @clickhouse/client instead of unmaintained clickhouse package

The example imported the third-party `clickhouse` (TimonKK) package and used
its API (new ClickHouse, .query().toPromise(), .insert().stream()). Migrate to
the official @clickhouse/client: createClient() and structured
clickhouse.insert({ table, values, format }). The structured values array also
removes the previous SQL string-interpolation anti-pattern.

Applies to the source skill and the ja-JP, zh-CN, zh-TW, ko-KR translated
copies (translated code comments preserved).

Refs: https://clickhouse.com/docs/integrations/javascript

* fix(clickhouse-io): migrate remaining insert calls to @clickhouse/client

Address review feedback: the earlier commit missed two spots that still used
the legacy clickhouse API.

- CDC example: clickhouse.insert('market_updates', [...]) ->
  clickhouse.insert({ table, values, format: 'JSONEachRow' })
- Single-row insertTrade: map the row to the column shape (same as the bulk
  path) instead of passing the raw trade object.

Applies to the source skill and the ja-JP, zh-CN, zh-TW, ko-KR copies.
This commit is contained in:
28winz-bot 2026-07-04 10:36:46 +07:00 committed by GitHub
parent 7a46a7b8fc
commit ee2663d5b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 230 additions and 202 deletions

View file

@ -151,39 +151,43 @@ ORDER BY market_id, date;
### 一括挿入(推奨) ### 一括挿入(推奨)
```typescript ```typescript
import { ClickHouse } from 'clickhouse' import { createClient } from '@clickhouse/client'
const clickhouse = new ClickHouse({ const clickhouse = createClient({
url: process.env.CLICKHOUSE_URL, url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123',
port: 8123, username: process.env.CLICKHOUSE_USER,
basicAuth: { password: process.env.CLICKHOUSE_PASSWORD
username: process.env.CLICKHOUSE_USER,
password: process.env.CLICKHOUSE_PASSWORD
}
}) })
// PASS: バッチ挿入(効率的) // PASS: バッチ挿入(効率的)
async function bulkInsertTrades(trades: Trade[]) { async function bulkInsertTrades(trades: Trade[]) {
const values = trades.map(trade => `( await clickhouse.insert({
'${trade.id}', table: 'trades',
'${trade.market_id}', values: trades.map(trade => ({
'${trade.user_id}', id: trade.id,
${trade.amount}, market_id: trade.market_id,
'${trade.timestamp.toISOString()}' user_id: trade.user_id,
)`).join(',') amount: trade.amount,
timestamp: trade.timestamp.toISOString()
await clickhouse.query(` })),
INSERT INTO trades (id, market_id, user_id, amount, timestamp) format: 'JSONEachRow'
VALUES ${values} })
`).toPromise()
} }
// FAIL: 個別挿入(低速) // FAIL: 個別挿入(低速)
async function insertTrade(trade: Trade) { async function insertTrade(trade: Trade) {
// ループ内でこれをしないでください! // ループ内でこれをしないでください!
await clickhouse.query(` await clickhouse.insert({
INSERT INTO trades VALUES ('${trade.id}', ...) table: 'trades',
`).toPromise() values: [{
id: trade.id,
market_id: trade.market_id,
user_id: trade.user_id,
amount: trade.amount,
timestamp: trade.timestamp.toISOString()
}],
format: 'JSONEachRow'
})
} }
``` ```
@ -191,17 +195,14 @@ async function insertTrade(trade: Trade) {
```typescript ```typescript
// 継続的なデータ取り込み用 // 継続的なデータ取り込み用
import { createWriteStream } from 'fs' import { Readable } from 'node:stream'
import { pipeline } from 'stream/promises'
async function streamInserts() { async function streamInserts(dataSource: AsyncIterable<Record<string, unknown>>) {
const stream = clickhouse.insert('trades').stream() await clickhouse.insert({
table: 'trades',
for await (const batch of dataSource) { values: Readable.from(dataSource, { objectMode: true }),
stream.write(batch) format: 'JSONEachRow'
} })
await stream.end()
} }
``` ```
@ -386,14 +387,18 @@ pgClient.query('LISTEN market_updates')
pgClient.on('notification', async (msg) => { pgClient.on('notification', async (msg) => {
const update = JSON.parse(msg.payload) const update = JSON.parse(msg.payload)
await clickhouse.insert('market_updates', [ await clickhouse.insert({
{ table: 'market_updates',
market_id: update.id, values: [
event_type: update.operation, // INSERT, UPDATE, DELETE {
timestamp: new Date(), market_id: update.id,
data: JSON.stringify(update.new_data) event_type: update.operation, // INSERT, UPDATE, DELETE
} timestamp: new Date(),
]) data: JSON.stringify(update.new_data)
}
],
format: 'JSONEachRow'
})
}) })
``` ```

View file

@ -161,36 +161,43 @@ ORDER BY market_id, date;
### 배치 삽입 (권장) ### 배치 삽입 (권장)
```typescript ```typescript
import { ClickHouse } from 'clickhouse' import { createClient } from '@clickhouse/client'
const clickhouse = new ClickHouse({ const clickhouse = createClient({
url: process.env.CLICKHOUSE_URL, url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123',
port: 8123, username: process.env.CLICKHOUSE_USER,
basicAuth: { password: process.env.CLICKHOUSE_PASSWORD
username: process.env.CLICKHOUSE_USER,
password: process.env.CLICKHOUSE_PASSWORD
}
}) })
// PASS: 배치 삽입 (효율적) // PASS: 배치 삽입 (효율적)
async function bulkInsertTrades(trades: Trade[]) { async function bulkInsertTrades(trades: Trade[]) {
const rows = trades.map(trade => ({ await clickhouse.insert({
id: trade.id, table: 'trades',
market_id: trade.market_id, values: trades.map(trade => ({
user_id: trade.user_id, id: trade.id,
amount: trade.amount, market_id: trade.market_id,
timestamp: trade.timestamp.toISOString() user_id: trade.user_id,
})) amount: trade.amount,
timestamp: trade.timestamp.toISOString()
await clickhouse.insert('trades', rows) })),
format: 'JSONEachRow'
})
} }
// FAIL: 개별 삽입 (느림) // FAIL: 개별 삽입 (느림)
async function insertTrade(trade: Trade) { async function insertTrade(trade: Trade) {
// 루프 안에서 이렇게 하지 마세요! // 루프 안에서 이렇게 하지 마세요!
await clickhouse.query(` await clickhouse.insert({
INSERT INTO trades VALUES ('${trade.id}', ...) table: 'trades',
`).toPromise() values: [{
id: trade.id,
market_id: trade.market_id,
user_id: trade.user_id,
amount: trade.amount,
timestamp: trade.timestamp.toISOString()
}],
format: 'JSONEachRow'
})
} }
``` ```
@ -198,17 +205,14 @@ async function insertTrade(trade: Trade) {
```typescript ```typescript
// 연속적인 데이터 수집용 // 연속적인 데이터 수집용
import { createWriteStream } from 'fs' import { Readable } from 'node:stream'
import { pipeline } from 'stream/promises'
async function streamInserts() { async function streamInserts(dataSource: AsyncIterable<Record<string, unknown>>) {
const stream = clickhouse.insert('trades').stream() await clickhouse.insert({
table: 'trades',
for await (const batch of dataSource) { values: Readable.from(dataSource, { objectMode: true }),
stream.write(batch) format: 'JSONEachRow'
} })
await stream.end()
} }
``` ```
@ -404,14 +408,18 @@ pgClient.query('LISTEN market_updates')
pgClient.on('notification', async (msg) => { pgClient.on('notification', async (msg) => {
const update = JSON.parse(msg.payload) const update = JSON.parse(msg.payload)
await clickhouse.insert('market_updates', [ await clickhouse.insert({
{ table: 'market_updates',
market_id: update.id, values: [
event_type: update.operation, // INSERT, UPDATE, DELETE {
timestamp: new Date(), market_id: update.id,
data: JSON.stringify(update.new_data) event_type: update.operation, // INSERT, UPDATE, DELETE
} timestamp: new Date(),
]) data: JSON.stringify(update.new_data)
}
],
format: 'JSONEachRow'
})
}) })
``` ```

View file

@ -162,39 +162,43 @@ ORDER BY market_id, date;
### 批量插入 (推荐) ### 批量插入 (推荐)
```typescript ```typescript
import { ClickHouse } from 'clickhouse' import { createClient } from '@clickhouse/client'
const clickhouse = new ClickHouse({ const clickhouse = createClient({
url: process.env.CLICKHOUSE_URL, url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123',
port: 8123, username: process.env.CLICKHOUSE_USER,
basicAuth: { password: process.env.CLICKHOUSE_PASSWORD
username: process.env.CLICKHOUSE_USER,
password: process.env.CLICKHOUSE_PASSWORD
}
}) })
// PASS: Batch insert (efficient) // PASS: Batch insert (efficient)
async function bulkInsertTrades(trades: Trade[]) { async function bulkInsertTrades(trades: Trade[]) {
const values = trades.map(trade => `( await clickhouse.insert({
'${trade.id}', table: 'trades',
'${trade.market_id}', values: trades.map(trade => ({
'${trade.user_id}', id: trade.id,
${trade.amount}, market_id: trade.market_id,
'${trade.timestamp.toISOString()}' user_id: trade.user_id,
)`).join(',') amount: trade.amount,
timestamp: trade.timestamp.toISOString()
await clickhouse.query(` })),
INSERT INTO trades (id, market_id, user_id, amount, timestamp) format: 'JSONEachRow'
VALUES ${values} })
`).toPromise()
} }
// FAIL: Individual inserts (slow) // FAIL: Individual inserts (slow)
async function insertTrade(trade: Trade) { async function insertTrade(trade: Trade) {
// Don't do this in a loop! // Don't do this in a loop!
await clickhouse.query(` await clickhouse.insert({
INSERT INTO trades VALUES ('${trade.id}', ...) table: 'trades',
`).toPromise() values: [{
id: trade.id,
market_id: trade.market_id,
user_id: trade.user_id,
amount: trade.amount,
timestamp: trade.timestamp.toISOString()
}],
format: 'JSONEachRow'
})
} }
``` ```
@ -202,17 +206,14 @@ async function insertTrade(trade: Trade) {
```typescript ```typescript
// For continuous data ingestion // For continuous data ingestion
import { createWriteStream } from 'fs' import { Readable } from 'node:stream'
import { pipeline } from 'stream/promises'
async function streamInserts() { async function streamInserts(dataSource: AsyncIterable<Record<string, unknown>>) {
const stream = clickhouse.insert('trades').stream() await clickhouse.insert({
table: 'trades',
for await (const batch of dataSource) { values: Readable.from(dataSource, { objectMode: true }),
stream.write(batch) format: 'JSONEachRow'
} })
await stream.end()
} }
``` ```
@ -397,14 +398,18 @@ pgClient.query('LISTEN market_updates')
pgClient.on('notification', async (msg) => { pgClient.on('notification', async (msg) => {
const update = JSON.parse(msg.payload) const update = JSON.parse(msg.payload)
await clickhouse.insert('market_updates', [ await clickhouse.insert({
{ table: 'market_updates',
market_id: update.id, values: [
event_type: update.operation, // INSERT, UPDATE, DELETE {
timestamp: new Date(), market_id: update.id,
data: JSON.stringify(update.new_data) event_type: update.operation, // INSERT, UPDATE, DELETE
} timestamp: new Date(),
]) data: JSON.stringify(update.new_data)
}
],
format: 'JSONEachRow'
})
}) })
``` ```

View file

@ -151,39 +151,43 @@ ORDER BY market_id, date;
### 批量插入(推薦) ### 批量插入(推薦)
```typescript ```typescript
import { ClickHouse } from 'clickhouse' import { createClient } from '@clickhouse/client'
const clickhouse = new ClickHouse({ const clickhouse = createClient({
url: process.env.CLICKHOUSE_URL, url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123',
port: 8123, username: process.env.CLICKHOUSE_USER,
basicAuth: { password: process.env.CLICKHOUSE_PASSWORD
username: process.env.CLICKHOUSE_USER,
password: process.env.CLICKHOUSE_PASSWORD
}
}) })
// PASS: 批量插入(高效) // PASS: 批量插入(高效)
async function bulkInsertTrades(trades: Trade[]) { async function bulkInsertTrades(trades: Trade[]) {
const values = trades.map(trade => `( await clickhouse.insert({
'${trade.id}', table: 'trades',
'${trade.market_id}', values: trades.map(trade => ({
'${trade.user_id}', id: trade.id,
${trade.amount}, market_id: trade.market_id,
'${trade.timestamp.toISOString()}' user_id: trade.user_id,
)`).join(',') amount: trade.amount,
timestamp: trade.timestamp.toISOString()
await clickhouse.query(` })),
INSERT INTO trades (id, market_id, user_id, amount, timestamp) format: 'JSONEachRow'
VALUES ${values} })
`).toPromise()
} }
// FAIL: 個別插入(慢) // FAIL: 個別插入(慢)
async function insertTrade(trade: Trade) { async function insertTrade(trade: Trade) {
// 不要在迴圈中這樣做! // 不要在迴圈中這樣做!
await clickhouse.query(` await clickhouse.insert({
INSERT INTO trades VALUES ('${trade.id}', ...) table: 'trades',
`).toPromise() values: [{
id: trade.id,
market_id: trade.market_id,
user_id: trade.user_id,
amount: trade.amount,
timestamp: trade.timestamp.toISOString()
}],
format: 'JSONEachRow'
})
} }
``` ```
@ -191,17 +195,14 @@ async function insertTrade(trade: Trade) {
```typescript ```typescript
// 用於持續資料攝取 // 用於持續資料攝取
import { createWriteStream } from 'fs' import { Readable } from 'node:stream'
import { pipeline } from 'stream/promises'
async function streamInserts() { async function streamInserts(dataSource: AsyncIterable<Record<string, unknown>>) {
const stream = clickhouse.insert('trades').stream() await clickhouse.insert({
table: 'trades',
for await (const batch of dataSource) { values: Readable.from(dataSource, { objectMode: true }),
stream.write(batch) format: 'JSONEachRow'
} })
await stream.end()
} }
``` ```
@ -386,14 +387,18 @@ pgClient.query('LISTEN market_updates')
pgClient.on('notification', async (msg) => { pgClient.on('notification', async (msg) => {
const update = JSON.parse(msg.payload) const update = JSON.parse(msg.payload)
await clickhouse.insert('market_updates', [ await clickhouse.insert({
{ table: 'market_updates',
market_id: update.id, values: [
event_type: update.operation, // INSERT, UPDATE, DELETE {
timestamp: new Date(), market_id: update.id,
data: JSON.stringify(update.new_data) event_type: update.operation, // INSERT, UPDATE, DELETE
} timestamp: new Date(),
]) data: JSON.stringify(update.new_data)
}
],
format: 'JSONEachRow'
})
}) })
``` ```

View file

@ -162,39 +162,43 @@ ORDER BY market_id, date;
### Bulk Insert (Recommended) ### Bulk Insert (Recommended)
```typescript ```typescript
import { ClickHouse } from 'clickhouse' import { createClient } from '@clickhouse/client'
const clickhouse = new ClickHouse({ const clickhouse = createClient({
url: process.env.CLICKHOUSE_URL, url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123',
port: 8123, username: process.env.CLICKHOUSE_USER,
basicAuth: { password: process.env.CLICKHOUSE_PASSWORD
username: process.env.CLICKHOUSE_USER,
password: process.env.CLICKHOUSE_PASSWORD
}
}) })
// PASS: Batch insert (efficient) // PASS: Batch insert (efficient)
async function bulkInsertTrades(trades: Trade[]) { async function bulkInsertTrades(trades: Trade[]) {
const values = trades.map(trade => `( await clickhouse.insert({
'${trade.id}', table: 'trades',
'${trade.market_id}', values: trades.map(trade => ({
'${trade.user_id}', id: trade.id,
${trade.amount}, market_id: trade.market_id,
'${trade.timestamp.toISOString()}' user_id: trade.user_id,
)`).join(',') amount: trade.amount,
timestamp: trade.timestamp.toISOString()
await clickhouse.query(` })),
INSERT INTO trades (id, market_id, user_id, amount, timestamp) format: 'JSONEachRow'
VALUES ${values} })
`).toPromise()
} }
// FAIL: Individual inserts (slow) // FAIL: Individual inserts (slow)
async function insertTrade(trade: Trade) { async function insertTrade(trade: Trade) {
// Don't do this in a loop! // Don't do this in a loop!
await clickhouse.query(` await clickhouse.insert({
INSERT INTO trades VALUES ('${trade.id}', ...) table: 'trades',
`).toPromise() values: [{
id: trade.id,
market_id: trade.market_id,
user_id: trade.user_id,
amount: trade.amount,
timestamp: trade.timestamp.toISOString()
}],
format: 'JSONEachRow'
})
} }
``` ```
@ -202,17 +206,14 @@ async function insertTrade(trade: Trade) {
```typescript ```typescript
// For continuous data ingestion // For continuous data ingestion
import { createWriteStream } from 'fs' import { Readable } from 'node:stream'
import { pipeline } from 'stream/promises'
async function streamInserts() { async function streamInserts(dataSource: AsyncIterable<Record<string, unknown>>) {
const stream = clickhouse.insert('trades').stream() await clickhouse.insert({
table: 'trades',
for await (const batch of dataSource) { values: Readable.from(dataSource, { objectMode: true }),
stream.write(batch) format: 'JSONEachRow'
} })
await stream.end()
} }
``` ```
@ -397,14 +398,18 @@ pgClient.query('LISTEN market_updates')
pgClient.on('notification', async (msg) => { pgClient.on('notification', async (msg) => {
const update = JSON.parse(msg.payload) const update = JSON.parse(msg.payload)
await clickhouse.insert('market_updates', [ await clickhouse.insert({
{ table: 'market_updates',
market_id: update.id, values: [
event_type: update.operation, // INSERT, UPDATE, DELETE {
timestamp: new Date(), market_id: update.id,
data: JSON.stringify(update.new_data) event_type: update.operation, // INSERT, UPDATE, DELETE
} timestamp: new Date(),
]) data: JSON.stringify(update.new_data)
}
],
format: 'JSONEachRow'
})
}) })
``` ```