CSV Reader Engine (DuckDB Engine)
Overview
Audience results in this system are stored as CSV files on MinIO/S3, not as database rows, so that very large member lists remain workable. Whenever that data has to be read back — paginating the member list, counting members, or pulling LINE user IDs for a broadcast — this engine does the reading.
The original NestJS service used in-memory DuckDB with httpfs to read CSVs straight from S3. The Go implementation is a rewrite in pure Go with no CGO: it streams the file from object storage, parses the CSV itself (handling quoted fields, embedded newlines, and BOM), and holds the result as an in-memory table cached for 30 seconds per path — while keeping the public surface and output identical to the DuckDB original.
One test endpoint exists, GET /api/duckdb in the duckdbctl module, for debugging whether a given
CSV path can be read.
Business Flow
- The Audience Management and Audience Filter modules build an audience and write the result as a CSV into object storage.
- When cms-web calls
GET /api/audiences/:id/member, the service callsLoadCsvFromMinio(path)if the file is not already cached, thenGetPaginatedLineUserIds. - The cache lives for 30 seconds, so subsequent page requests within that window skip the download.
GetTotalLineUserCountanswers the total member count.- When the data is no longer needed, the engine calls
DestroyTempTable,CleanupExpiredTables, orCleanupAllTables. - Rich menus bound to an audience use the same path to resolve their LINE user list, via
processRichMenuAudienceinrichmenu/create_update.go. GET /api/duckdb?path=...&page=&limit=is the debug endpoint, wired straight into theTestfunction.
Behaviours deliberately kept identical to the DuckDB original
- Empty CSV values are treated as NULL, so rows with a blank
"Line UserID"column are filtered out by anIS NOT NULLcondition. GetPaginatedLineUserIdsandTestread the first column of each row, following the TypeScriptObject.values(r)[0]behaviour, rather than the column literally named"Line UserID"._ensureTableExists, the path used by the test endpoint, does not apply the null filter.- All returned values are strings (DuckDB would have inferred numeric types) — an accepted, documented deviation.
Key Files & Functions
| File | Role |
|---|---|
internal/duckdb/duckdb.go | The engine itself, reached via Deps.CSVEngine |
internal/modules/duckdbctl/controller.go | RegisterRoutes registers authed.GET("/duckdb", h.test) |
internal/modules/duckdbctl/dto.go | Query params: path is required, page and limit optional |
| Method | Route | Handler | Guard |
|---|---|---|---|
| GET | /api/duckdb | h.test | The authed group (global JWT); no policy check |
Engine public API: LoadCsvFromMinio, GetPaginatedLineUserIds, GetTotalLineUserCount,
DestroyTempTable, ListTables, CleanupTableCache, CleanupExpiredTables, CleanupAllTables,
GetCacheStats, Test
Connections to Other Services
- Permission — requires a token that passes the global
JwtAuthguard (which mandateslineOaId), but no policy check is applied. - Storage — reads files from MinIO/S3 through
internal/storage. - Consumers — Audience Management, Audience Filter, and Rich Menu.
- Caveat — the cache is in-memory and per-process, so each replica maintains its own copy.