まだ開発中なんですがCSVファイル群をSQL文で操作できるCライブラリを作りました。
使用風景
CsvTomatoError error = {0};
CsvTomato *db;
CsvTomatoStmt *stmt;
db = csvtmt_open("test_db", &error);
if (error.error) {
csvtmt_error_show(&error);
return;
}
// execute関数の実行
csvtmt_exec(
db,
"CREATE TABLE IF NOT EXISTS users ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" age INTEGER"
");",
&error
);
csvtmt_exec(
db,
"INSERT INTO users (name, age) VALUES (\"Alice\", 20);",
&error
);
// プレースホルダの使用
// stmtのメモリを確保
csvtmt_prepare(
db,
"INSERT INTO users (name, age) VALUES (?, ?);",
&stmt,
&error
);
// プレースホルダに値を紐づける
csvtmt_bind_text(
stmt,
1,
"Bob",
-1,
CSVTMT_TRANSTENT,
&error
);
csvtmt_bind_int(
stmt,
2,
30,
&error
);
// 実行
csvtmt_step(stmt, &error);
// stmtのメモリを解放
csvtmt_finalize(stmt);
csvtmt_close(db);
開発動機
CSVファイルをSQL文で操作出来たら面白いのでは? というありがちなアイデアを思いつきました。
実際に作ってみてどんなもんか確認してみることにしました。
ライブラリの設計
インターフェースはSQLiteを踏襲するようにしました。
いまのところ実装している文は
- CREATE TABLE文
- INSERT文
- SELECT文
- UPDATE文
- DELETE文
のみです。
またSQLiteに比べると機能は貧弱で、実装していない文法が多いです。
エラーハンドリング
エラーハンドリングはCsvTomatoError
という構造体で行います。
関数内でエラーが起こるとメンバのerror
がtrue
になります。
db = csvtmt_open("test_db", &error);
if (error.error) { // エラー発生!
// エラーメッセージを出力
csvtmt_error_show(&error);
return;
}
データベースのオープン
csvtmt_open()
の第1引数に指定する文字列はデータベース名です。
このライブラリではデータベースはフォルダになります。
CSVファイルはこの場合、実行時フォルダのtest_db
フォルダ以下に作成されます。
db = csvtmt_open("test_db", &error);
if (error.error) {
csvtmt_error_show(&error);
return;
}
CREATE TABLE
csvtmt_exec(
db,
"CREATE TABLE IF NOT EXISTS users ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" age INTEGER"
");",
&error
);
テーブル(CSVファイル)を作成します。
IF NOT EXISTS
が付いている場合はすでにテーブルが存在する場合は何もしません。
IF NOT EXISTS
が付いてなくてテーブルが既に存在する場合はエラーになります。
INSERT
csvtmt_exec(
db,
"INSERT INTO users (name, age) VALUES (\"Alice\", 20);",
&error
);
テーブル(CSVファイル)の末尾にレコードを追記します。
上記ではtest_db/users.csv
ファイルにレコードを追記しています。
SELECT
db = csvtmt_open(db_dir, &error);
csvtmt_prepare(db, "SELECT id, age FROM users;", &stmt, &error);
while (csvtmt_step(stmt, &error) == CSVTMT_ROW) {
int id = csvtmt_column_int(stmt, 0);
const char *name = csvtmt_column_text(stmt, 1);
printf("%d %s\n", id, name);
}
csvtmt_finalize(stmt);
csvtmt_close(db);
SELECT文の仕様もSQLiteと同じにしました。
csvtmt_prepare()
でクエリを準備し、csvtmt_step()
で順次実行します。
csvtmt_step()
の返り値がCSVTMT_ROW
の場合はstmt
に行データが入っているので、csvtmt_column_int()
などでカラムを取り出します。
UPDATE
csvtmt_exec(
db,
"UPDATE users SET age = 30 WHERE name = \"Alice\;",
&error
);
レコードを更新します。
WHERE
で該当するとレコードのカラムを変更できます。
UPDATE
は内部的にレコードを論理削除してから追記する、と言う仕様になっています。
DELETE
csvtmt_exec(
db,
"DELETE FROM users WHERE age = 30;",
&error
);
レコードを削除します。
DELETE
はレコードを論理削除します。
ここまで作ってみて思ったこと
CSVファイルへのCRUDの設計をどうしたもんか考えましたが、前述のような仕様になりました。
UPDATE, DELETEはCSVファイル全体の置き換えが必要になるため、コストが高くなります。
そこで論理削除とmmapを使ってコストを下げられないかなーと実装してみました。
テキストファイルは追記はコストが低いんですが、そのほかの編集がコストが高くなる性質があるので、今回の実装をしてみました。
まだ性能は計測してません。
CSVファイルは表計算ソフトで編集できるので、データベースを表計算ソフトで閲覧&編集できるというアドバンテージがあります(多分)。
もっとも、大量データを扱うのには向いていないので、ごくごく小規模なアプリのデータベースに使うという前提で妄想してました。
更新履歴
2025-09-26: SELECT文を実装しました。
おわりに
まだバグとか変なところはあると思いますが、実用性が出てきたら自分のアプリにも使ってみたいです。
テストはちゃんと書かないといけませんね。