Database sinks¶
The scraper writes each record to every sink listed in DATABASE_TARGET (a
comma-separated, ordered list of sink ids). Reads are served by the first configured sink
that supports them. Every configured sink is required: a write failure anywhere marks the
run non-zero.
# one sink
DATABASE_TARGET=postgres
# several (order sets read precedence)
DATABASE_TARGET=postgres,sqlite
# a filesystem database — no server required
DATABASE_TARGET=sqlite
SQLITE_PATH=hkex.db
Support matrix¶
Rows are in the documented popularity order (see
sinks/registry.py). Every sink is a first-class
destination; none is privileged.
| Sink | Model | License | OSI | Extra | Idempotent upsert | Reads | Edges |
|---|---|---|---|---|---|---|---|
postgres |
relational | PostgreSQL License | Yes | postgres |
ON CONFLICT DO UPDATE |
Yes | Yes |
mysql |
relational | GPLv2 (Community) | Yes | mysql |
ON DUPLICATE KEY UPDATE |
Yes | Yes |
sqlite |
relational | Public domain | Yes | — | ON CONFLICT DO UPDATE |
Yes | Yes |
mongodb |
document | SSPL | No | mongodb |
update_one(upsert=True) |
Yes | Yes |
mariadb |
relational | GPLv2 | Yes | mysql |
ON DUPLICATE KEY UPDATE |
Yes | Yes |
neo4j |
graph | GPLv3 (Community) | Yes | neo4j |
MERGE |
Yes | Yes |
clickhouse |
columnar | Apache-2.0 | Yes | clickhouse |
ReplacingMergeTree + read-merge |
Yes | Yes |
duckdb |
relational | MIT | Yes | duckdb |
ON CONFLICT DO UPDATE |
Yes | Yes |
surrealdb |
graph + document | BSL 1.1 | No | — | UPSERT / RELATE |
Yes | Yes |
Source-available engines are labelled as such (see ADR 0003).
Capability differences¶
SinkCapabilities declares where engines differ, so the dispatcher adapts rather than
assuming parity:
clickhousedeclaresnative_upsert=False(no in-place row update; read-merge-reinsert).duckdbhas no secondary indexes and norowcount(the dialect appendsRETURNING 1).postgresdeclaresarrays=Trueandjson=True(text[],jsonb); the other relational sinks store JSON text.mongodb,neo4j, andsurrealdbuse the document/graph shapes described in their guides.
Shared schema¶
Every relational sink mirrors the same tables and keys:
| Table | Primary key | Purpose |
|---|---|---|
exchange_filing |
filing_id |
Filing metadata + document payload. |
scrape_coverage |
(chunk_from, chunk_to, run_id) |
One row per scraped chunk. |
has_filing |
(company_id, filing_id) |
Company → filing edges. |
references_filing |
(filing_id, company_id) |
Filing → referenced-company edges. |
A metadata upsert never writes document_* columns; a document write only updates an
existing row. Re-running is always idempotent.
Per-sink guides¶
postgres— PostgreSQLmysql— MySQL and MariaDBsqlite— SQLitemongodb— MongoDBmariadb— MySQL and MariaDB (same driver and schema asmysql)neo4j— Neo4jclickhouse— ClickHouseduckdb— DuckDBsurrealdb— SurrealDB
Adding a sink¶
- Add a
Dialect(SQL) or a native adapter insrc/hkex_scraper/sinks/. - Register it in
sinks/registry.pywith its license, OSI status, and optional extra. - Add dialect/contract tests and, where a service is required, a CI job.
- Add it to the support matrix above, to the docs nav, and to the wiki page map; if the license is source-available, label it.