NimでDuckDBのClient APIを勝手に作ってみた
皆さん、DuckDBって、ご存じでしょうか?
DuckDBはOLAP(オンライン分散処理)分析に特化したデータベースだそうです。
2024年6月に、DuckDBはバージョン1.0.0が正式に公開され、軽量・高速であり安定したデーターベースとして注目されています。
私も3日前に、たまたまYoutubeの動画を視聴して知りました。(笑)
今回はNimとは関係なく、高速なDuckDBについて記載していきます。
DuckDBについては、下記の記事が詳細に記載されています。
はじめは、Sqlite3と同様かな?って思ってましたが、Sqlite3に較べて数倍高速で
csvファイルを登録しなくとも、その場でSQL文が書けてしまう優れ物でもあり、
且つ、驚くべき事に、AWS S3やDataFrameへの連携も可能と言う、とんでもない程、優れています。
また、JavaやRustなどの多数の言語のAPIが揃っており、APIがあるなら、Nim言語でも書いてみようかなっと思ってDuckDBの公式HPでサポート言語を見た訳ですよ!
そうなんですよ。Nim言語はサポート外だった訳ですよ!。そらね、超マイナーな言語かもしれませんが、Pythonより動作が速く、Rustより分かりやすい言語がサポート外とかふざけてますよね。
色々調べた結果、有志の方がNimでDuckDBのラッパーAPIを作られていました。
ただ、最新のVersion1.0.0じゃなく、0.10.0のバージョンのサポートだったし、C言語の構造体がわからないのか、多数の訳のわからないツールがインポートされるので、これは使えないと思い、それなら、ソースを見て勝手に改変して作ってみようと思った次第です。
(良い子のみんなはマネしちゃダメですよ!)
※ちなみに、非同期処理とか何も考えずに1日で作っちゃってますので、ご容赦願います。
環境
私のPC環境はWindowsで動作させています。
- OS: Windows11
- Nim 2.0.8
- DuckDB 1.0.0
DuckDBのインストールとツールの設定
1. DuckDB.exeのダウンロード
DuckDB公式のダウンロードHPに移動して、各OSのダウンロードモジュールをダウンロードします。
※Windows環境であれば、duckdb_cli-windows-amd64.zip
をダウンロードします。
ZIPファイルを解凍すると、duckdb.exe
の実行モジュールのみしか入っていません。
それをC:\Windows配下にコピーします。
2. DuckDB.exeの確認
DOSプロンプトを開き、duckdb
と入力します。問題なくDuckDBの画面が表示されれば、問題なくインストール完了した事になります。
ちなみに、終了はCtrl+Cで通常のプロンプト画面に戻ります。
3. DuckDBのC言語Client APIをダウンロード
DuckDB公式のダウンロードHPで、Environment:C/C++を押して、各自のOS毎のC言語Client APIがダウンロードできます。
※Windows環境であれば、libduckdb-windows-amd64.zip
をダウンロードします。
4. DuckDB用プロジェクトの作成
VSCodeを開き、Nim用のプロジェクトを作成します。
プロジェクト直下に、duckdb
フォルダを作成し、先ほどのlibduckdb-windows-amd64.zip
を解凍して入れます。
解凍したモジュール「duckdb.dll
、duckdb.h
、duckdb.hpp
、duckdb.lib
」の4つのファイルを、duckdb
フォルダに移動します。
更に、duckdb
フォルダに、duckdb.nim
ファイルを新規に作成します。
duckdb.nim
ファイルの中身を、以下に記載します。
※GitHubへソースを上げれれば良かったのですが、超ド素人なので、GitHubへの登録さえ出来ません。(笑)
また、duckdb/duckdb.nim
の1行目は、自分個人用としてWindowsのDLLファイルだけ指定しています。もしお使いのOSがMacやLinuxなら、それようのライブラリを指定してください。
const Lib = "duckdb/duckdb.dll" # windows特化
const DuckDBSuccess = 0
const DuckDBError = 1
type
Db = distinct pointer ## Generic database pointer.
Con = distinct pointer ## Generic connection pointer.
DuckDBRow* = seq[string]
DuckDBOperationError* = object of CatchableError
# DuckDB側のC言語構造体
type
DuckDBConn* = ref object
database: Db
connection: Con
DuckDBColum* = object
deprecated_data: pointer
deprecated_nullmask: pointer
deprecated_type: cint # duckdb_type
deprecated_name: cstring
internal_data: pointer
DuckDBResult* = object
deprecated_column_count: int
deprecated_row_count: int
deprecated_rows_changed: int
deprecated_columns: ptr DuckDBColum
deprecated_error_message: cstring
internal_data: pointer
DuckDBPreparedStatement* = pointer
# DuckDBのC言語API 必要と思う物のみを抽出
{.push importc, cdecl, dynlib: Lib.}
proc duckdb_open(path: cstring, database: ptr Db): int
proc duckdb_close(database: ptr Db)
proc duckdb_connect(database: Db, connection: ptr Con): int
proc duckdb_disconnect(connection: ptr Con)
proc duckdb_query(connection: Con, query: cstring, result: ptr DuckDBResult): int
proc duckdb_value_varchar(result: ptr DuckDBResult, col: int, row: int): cstring
proc duckdb_free(v: pointer): void
proc duckdb_column_count(result: DuckDBResult): int
proc duckdb_row_count(result: DuckDBResult): int
proc duckdb_prepare(connection: Con, query: cstring, statement: ptr DuckDBPreparedStatement): int
proc duckdb_bind_varchar(statement: DuckDBPreparedStatement, param_idx: int, val: cstring): int
proc duckdb_execute_prepared(statement: DuckDBPreparedStatement, result: ptr DuckDBResult): int
proc duckdb_result_error(result: ptr DuckDBResult): cstring
proc duckdb_prepare_error(statement: DuckDBPreparedStatement): cstring
proc duckdb_destroy_result(result: ptr DuckDBResult)
proc duckdb_destroy_prepare(statement: ptr DuckDBPreparedStatement)
{.pop.}
# エラーチェック
proc checkStatus(status: int) =
if status != DuckDBSuccess :
raise newException(DuckDBOperationError, "DuckDB operation did not complete sucessfully.")
proc checkStatus(status: int, resultParam: DuckDBResult) =
if status != DuckDBSuccess :
let msg = duckdb_result_error(resultParam.addr)
raise newException(DuckDBOperationError, "DuckDB operation did not complete sucessfully. Reason:\n" & $msg)
proc checkStatus(status: int, statement: DuckDBPreparedStatement) =
if status != DuckDBSuccess :
let msg = duckdb_prepare_error(statement)
raise newException(DuckDBOperationError, "DuckDB operation did not complete sucessfully. Reason:\n" & $msg)
# 接続処理
proc connect*(path: string): DuckDBConn =
result = new DuckDBConn
duckdb_open(path.cstring, result.database.addr).checkStatus()
duckdb_connect(result.database, result.connection.addr).checkStatus()
# 接続処理(in-memory)
proc connect*(): DuckDBConn =
result = connect(":memory:")
# クローズ処理
proc close*(conn: DuckDBConn) =
duckdb_close(conn.database.addr)
# 切断処理
proc disconnect*(conn: DuckDBConn) =
duckdb_disconnect(conn.connection.addr)
iterator getRows(resultParam: DuckDBResult): DuckDBRow =
var clmCnt = duckdb_column_count(resultParam)
var rowCnt = duckdb_row_count(resultParam)
var duckDBRow = newSeq[string](clmCnt)
var v: cstring
for row in 0 ..< rowCnt:
for clm in 0 ..< clmCnt:
v = duckdb_value_varchar(resultParam.addr, clm, row)
duckDBRow[clm] = (
if v.isNil():
"NULL"
else:
$v
)
duckdb_free(v)
yield duckDBRow
# 行毎にSelect処理
iterator rows*(conn: DuckDBConn, sql: string, args: varargs[string,`$`]): DuckDBRow =
var resultParam: DuckDBResult
var statement: DuckDBPreparedStatement
defer:
if args.len() != 0:
duckdb_destroy_prepare(statement.addr)
duckdb_destroy_result(resultParam.addr)
if args.len() == 0:
duckdb_query(conn.connection, sql.cstring, resultParam.addr).checkStatus(resultParam)
else:
duckdb_prepare(conn.connection, sql.cstring, statement.addr).checkStatus(statement)
for i, arg in args:
duckdb_bind_varchar(statement, i+1, arg.cstring).checkStatus()
duckdb_execute_prepared(statement, resultParam.addr).checkStatus(resultParam)
for duckDBRow in getRows(resultParam):
yield duckDBRow
# 実行処理
proc exec*(conn: DuckDBConn, sql: string) =
var resultParam: DuckDBResult
duckdb_query(conn.connection, sql.cstring, resultParam.addr).checkStatus(resultParam)
duckdb_destroy_result(resultParam.addr)
次に、呼び出し側のメイン部分のソースを作成します。
プロジェクトフォルダ直下に、sample.nim
ファイルを作成し、下記のように記載します。
import duckdb/duckdb
var con: DuckDBConn
try:
con = connect("mydb.db")
con.exec("CREATE TABLE IF NOT EXISTS integers(i INTEGER, j INTEGER);")
con.exec("INSERT INTO integers VALUES (3, 4), (5, 6), (7, NULL);")
for item in con.rows("SELECT * FROM integers WHERE i = ? or j = ?", 3, "6"):
echo item
except:
echo getCurrentExceptionMsg()
finally:
con.disconnect()
con.close()
メインソースをコンパイル実行します。
メインソースは、integers
テーブルが存在しないなら作成し、データを3レコード登録し、i項目が3とj項目が6のレコードを抽出するだけのプログラムです。
結果はシーケンステーブルとして、表示されて出力されるはずです。
また、プロジェクト直下にmydb.db
ファイルが作成されているはずです。
mydb.db
ファイルがデーターベースとなります。
$ nim c .\sample.nim
$ .\sample.exe
@["3", "4"]
@["5", "6"]
次に、コマンドプロンプト側でduckdbを操作します。
duckdb mydb.db
と入力して、以下のような表示なっているか確認します。
$ duckdb mydb.db
D .tables
integers
D select * from integers;
┌───────┬───────┐
│ i │ j │
│ int32 │ int32 │
├───────┼───────┤
│ 3 │ 4 │
│ 5 │ 6 │
│ 7 │ │
└───────┴───────┘
D
おわりに
DuckDBのラッパーを作ったのには、理由がありまして、有志の方が作成されたラッパーだと抽出したデータのメモリエリアを解放していないため、メモリリークの原因になる事がわかって作ってみた次第です。
それに、Nim言語だけAPIがないのも解せない話です。
出来たら、Nim用のDataFrameであるDatamancerへの抽出・登録なども作ろうかと思いましたが、面倒でした。
注意:
高速に登録するAppenderや、高速に読み込みを行うDataChunksやVectorは今回はサポートしていません。
(「NimでDuckDB用のインポート・エキスポートAPIを追加してみた」にて、高速に登録するAPIは追加しました。)
また、DuckDBのC APIのサンプルを見返すと、SQL処理結果をクリーンアップする部分も抜けているみたいです。どうするか検討しておきます。
(修正済み、1万レコードの読み書きを行っても、メモリ増加は見られなかったです。)
Discussion