Skip to content

Add Parquet KVIN upload - #65

Open
wmehling wants to merge 3 commits into
mainfrom
59-add-parquet-kvin-upload
Open

wmehling wants to merge 3 commits into
mainfrom
59-add-parquet-kvin-upload

Conversation

@wmehling

@wmehling wmehling commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Closes #59

What this adds

POST /linkedfactory/**/values with Content-Type: application/vnd.apache.parquet stores one KVIN tuple per Parquet row. It goes through the same store.put(...) path as CSV, including the KvinTupleGroupingIterator.

Column Parquet type Notes
item, property BINARY (string) Relative URIs resolve against the request path, like CSV
time INT64 Epoch ms. TIMESTAMP(MICROS/NANOS) is converted to ms
value BOOLEAN, INT32, INT64, FLOAT, DOUBLE, BINARY BINARY values become strings
context (optional) BINARY Must be absolute. Default: the request's model context
seqNr (optional) INT32 or INT64 Default 0

Other columns are ignored. This is the format the SPARQL Parquet exporter writes. Files written by pandas/pyarrow (timestamp units, int64 integers) work too.

Performance

Local JMH run, 30,000 tuples per invocation, 2 forks:

Benchmark Score
postCsv ~2.39 M tuples/s
postParquet ~2.62 M tuples/s (±40 k)

Scaling results (up to 500k rows / 3M tuples per request) are in docs/benchmarks/kvin-ingestion.md.

How to reproduce / review

Automated:

mvn -pl bundles/io.github.linkedfactory.core install -DskipTests
mvn -pl bundles/io.github.linkedfactory.service test -Dtest=KvinServiceTest

The test start with parquetPost… and cover:

  • the supported value types
  • defaults
  • relative URIs
  • multiple row groups, and the temp-file path
  • timestamp units and int64 seqNr
  • rejecting mixed types before anything is stored
  • hidden error details
  • malformed schemas
  • the exporter round trip

Manually against a running pod (adjust the host and port):

python3 -c "import pyarrow as pa, pyarrow.parquet as pq; pq.write_table(pa.table({'item':['machine1'],'property':['temperature'],'time':pa.array([1710000000000],pa.int64()),'value':[21.5]}), 'ok.parquet')"
curl -i -X POST -H 'Content-Type: application/vnd.apache.parquet' --data-binary @ok.parquet http://localhost:8080/linkedfactory/demo/values
  1. Expect 200. A GET of …/values?item=http://localhost:8080/linkedfactory/demo/machine1&property=http://localhost:8080/linkedfactory/demo/temperature should return 21.5.
  2. Write time with pa.timestamp('ns') instead. The stored time must still be the same millisecond value.
  3. POST random bytes. Expect 400 Invalid Parquet payload, with no /tmp/… path in the body.
  4. Upload a file over 32 MB. Then check the count, and that no kvin-upload-*.parquet file is left in the temp dir.

Benchmark:

mvn -pl bundles/io.github.linkedfactory.service -Pjmh '-Djmh.includes=KvinIngestionBenchmark.post(Csv|Parquet)$' test-compile exec:exec

POST .../values with Content-Type application/vnd.apache.parquet ingests
one tuple per row from the columns item, property, time and value, plus
the optional columns context and seqNr, through the existing store.put
path. This is the format written by the SPARQL Parquet exporter.

- Relative item/property URIs resolve against the request path, like CSV.
- TIMESTAMP(MILLIS/MICROS/NANOS) times are converted to milliseconds;
  int64 seqNr is accepted (for example from pandas/pyarrow).
- Unsupported schemas, decimal or large unsigned values, and null
  required values (from footer statistics) are rejected before any
  tuple is stored.
- Uploads up to 32 MB are parsed in memory; larger ones are spooled to
  a temporary file. Rows are streamed per row group, so memory stays
  bounded.
- Hadoop configuration is avoided, because it parses its XML defaults
  for every new reader.
- Internal error details such as file paths are not returned to clients.

Refs #59
- KvinIngestionBenchmark.postParquet measures the in-process Parquet
  route. The payload is created outside the measured section.
- KvinIngestionScalingBenchmark compares postCsv and postParquet for
  growing requests (-Djmh.rows=5000,100000,...) that span multiple
  Parquet row groups.
- Documentation updated with current results.

Refs #59
@wmehling wmehling linked an issue Oct 17, 2026 that may be closed by this pull request
@wmehling
wmehling requested a review from kenwenzel October 17, 2026 16:40
@kenwenzel

Copy link
Copy Markdown
Member

@wmehling Thank you for the PR. The implementation uses a quite different format than CSV which uses a column per item/property combination like <some:item1>@<some:property1>.

If datatypes change for subsequent items/properties then the value column needs to use variant or some kind of shredding (like one value column for each datatype like value_boolean, value_int etc.).
Alternatively, multiple files need to be uploaded if datatypes change.

What do you think?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Parquet KVIN upload

2 participants