DuckDB Node Neo Client 試し撃ち

## TL;DR
コードの全量は[GitHub](https://github.com/mineco13/duckdb-node-neo
)を参照ください。
下記コマンドで試せます。
```bash
git clone https://github.com/mineco13/duckdb-node-neo
cd duckdb-node-neo
docker compose down&&docker compose up -d --build
open http://localhost:3000
```

## はじめに
duckdbにNode.js clientがあるらしく試してみようと思ったのですが、従来のClientはDeprecatedとなっていたので新しいものを使いました。

https://duckdb.org/2024/12/18/duckdb-node-neo-client.html

古いクライアントも軽く試してみましたがcallback styleしか対応してなかったので書き味は少し悪かったです。
新しいクライアントはPromiseスタイルの書き方に対応するなど機能が追加されてますが、一部未機能の機能があったりと開発途中のようです。

https://duckdb.org/docs/api/node_neo/overview.html#features

今回はS3に見立てたMinIO上にあるcsvファイルをRemix上でテーブル形式にして表示してみます。
## 前提条件
下記インストール済みの想定です
- Docker
- Node.js


## 準備

### 表示用のcsvデータを用意
適当なcsvファイルを用意します。

```csvs data/sample.csv
name,github_star,url
Material-UI (MUI),90500,https://mui.com/
Chakra UI,34700,https://chakra-ui.com/
Ant Design,87000,https://ant.design/
React Bootstrap,22000,https://react-bootstrap.github.io/
Semantic UI React,13100,https://react.semantic-ui.com/
Blueprint,20000,https://blueprintjs.com/docs/
Grommet,8100,https://v2.grommet.io/
Evergreen,12000,https://evergreen.segment.com/
Rebass,7900,https://rebassjs.org/
Fluent UI,15000,https://developer.microsoft.com/en-us/fluentui
```
※人気のReact Component libraryをGitHub star数とともにAIに出力してもらいました。


### MinIOコンテナ(S3互換)を用意
先ほど作った`data/sample.csv`を自動で読み込むminioコンテナを、下記のように`compose.yaml`に定義します


```yaml compose.yaml
services:
  minio:
    image: minio/minio:latest
    ports:
      - 9000:9000
      - 9001:9001
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: minio123
    command: server /export --console-address ":9001"
  createbuckets:
    image: minio/mc
    depends_on:
      - minio
    entrypoint: >
      /bin/sh -c "
      until (/usr/bin/mc config host add myminio http://minio:9000 minio minio123) do echo '...waiting...' && sleep 1; done;
      /usr/bin/mc mb myminio/mybucket;
      /usr/bin/mc policy download myminio/mybucket;
      /usr/bin/mc cp /data/sample.csv myminio/mybucket/sample.csv;
      exit 0;
      "
    volumes:
      - ./data:/data
```
下記でMinIOにブラウザでアクセスして、`compose.yaml`内の`MINIO_ACCESS_KEY`と`MINIO_SECRET_KEY`を入力してログインすることでバケットとその中身を確認できます
```bash
open http://localhost:9000
```
### Remixテンプレート作成
画面表示用のテンプレートとしてRemixを使っていきます。(Next.jsみたいなものです)
```bash
npx create-remix@latest remix
```
`localhost:5173`にアクセスしてそれっぽい画面が表示されれば完了です
```bash
cd remix
npm run dev
## ACCESS http://localhost:5173
```

## 開発
```bash
npm i @duckdb/node-api
```
duckdbライブラリを用いて先ほど定義したMinIOからデータを読み出して、整形して返します。
`columnNames()`や`await result.getRows()`など、結果に容易にアクセスできるネイティブのメソッドが用意されているのは嬉しいですね。

```typescript remix/app/duckdb.ts
import { DuckDBInstance } from "@duckdb/node-api";

export const fetchCSV = async () => {
  const instance = await DuckDBInstance.create();
  const con = await instance.connect();

  await con.run(
    `
  INSTALL httpfs;
  LOAD httpfs;
  `
  );
  await con.run(
    `
  CREATE SECRET secret1 (
    TYPE S3,
    KEY_ID '${process.env.S3_ACCESS_KEY_ID}',
    SECRET '${process.env.S3_SECRET_ACCESS_KEY}',
    REGION 'us-east-1',
    ENDPOINT '${process.env.S3_HOST}:${process.env.S3_PORT}',
    USE_SSL false,
    URL_STYLE 'path'
  );
  `
  );

  const result = await con.run(
    "SELECT * FROM read_csv_auto('s3://mybucket/sample.csv');"
  );

  return { header: result.columnNames(), rows: await result.getRows() };
};

```

Remixからは`loader`(`getServerSideProps`的な)経由で呼び出してテーブル形式で出力します。
`fetchCSV`の結果をjsonにそのまま詰めて返したい所ですが、`TypeError: Do not know how to serialize a BigInt`を吐いて止まるので、bigintをstringに変換するロジックを挟んでいます。

```typescript remix/app/routes/_index.ts
import { useLoaderData } from "@remix-run/react";
import { fetchCSV } from "~/duckdb";

...
export const loader = async () =>
  json(
    JSON.parse(
      JSON.stringify(await fetchCSV(), (key, value) =>
        typeof value === "bigint" ? value.toString() : value
      )
    )
  );
export default function Index() {
  const data = useLoaderData<typeof loader>();

  return (
...
          </div>

          <table>
            <caption>Data from Object Storage</caption>
            <tr>
              {data.header.map((header) => (
                <th scope="col">{header}</th>
              ))}
            </tr>
            {data.rows.map((row, index) => (
              <tr key={index}>
                {row.map((d) => (
                  <td scope="col">{d}</td>
                ))}
              </tr>
            ))}
          </table>

        </header>
...

```

下記スクリプトで起動してテーブルが出力されることを確認します。

```bash
S3_HOST=localhost S3_PORT="9000" S3_ACCESS_KEY_ID=minio S3_SECRET_ACCESS_KEY=minio123 npm run dev
```

これでひとまず完了です。

## コンテナ化

蛇足ですが、下記のような`remix/Dockerfile`を作成して
```dockerfile remix/Dockerfile
FROM node:20-slim

WORKDIR /remix

COPY package*json .
RUN npm install
COPY . .

RUN npm run build

CMD ["npm", "run", "start"]

```

compose.yamlに書き加えたらコンテナ化まで完了です。

```yaml compose.yaml
services:
  remix:
    build:
      context: remix
    ports: [3000:3000]
    environment:
      S3_HOST: minio
      S3_PORT: "9000"
      S3_ACCESS_KEY_ID: minio
      S3_SECRET_ACCESS_KEY: minio123
    depends_on:
      - createbuckets
...
```

起動コマンドです。
```bash
docker compose down&&docker compose up -d --build
open http://localhost:3000
```

## 感想
様々な形式のデータにSQLインターフェースでアクセスできるのは楽しいです。
積極的に使っていこうと思います。