README.md

June 5, 2026 · View on GitHub

Lastra

CI JitPack JDK 11+ License

Columnar time series file format optimized for numeric data.
Ideal for financial tick data, IoT sensors, and infrastructure metrics.


Combines ALP, Gorilla, and Pongo compression for doubles, delta-varint for timestamps, and ZSTD/gzip for binary data — with per-column codec selection in a single .lastra file.

Features

  • Per-column codecs: ALP/Gorilla/Pongo for numeric data, delta-varint for timestamps, ZSTD/gzip for binary
  • Two sections: regular time series (series) + sparse timestamped events (events)
  • Column metadata: optional key-value metadata per column (e.g., indicator parameters, sensor config)
  • Selective column access: footer offsets enable reading specific columns without decompressing others
  • Little-endian throughout: JS/TS readers can use Float64Array zero-copy on decoded data
  • Zero Hadoop/Parquet dependency: pure Java + alp-java + zstd-jni, Gorilla and Pongo codecs have zero external deps
  • Java 11+

File Format

HEADER (22 bytes, LE):
  "LAST" magic | version (1) | flags | seriesRowCount | seriesColCount
  eventsRowCount | eventsColCount

COLUMN DESCRIPTORS (series, then events):
  codec | dataType | flags | name
  optional: metadata (JSON, gzip-compressed)

SERIES DATA:
  Without row groups: per column: [4 bytes length] [compressed data]
  With row groups:    per RG: per column: [4 bytes length] [compressed data]

EVENTS DATA:        per column: [4 bytes length] [compressed data]

FOOTER:
  Without row groups: [column offsets] + [column CRC32s] + "LAS!" magic
  With row groups:    [rgCount] + [per-RG stats] + [per-RG CRC32s]
                      + [event offsets] + [event CRC32s] + "LAS!" magic

Events section

All event columns share a single eventsRowCount. When columns have different logical lengths, use the highest count and pad shorter columns with zero/empty values. Use a type column (VARLEN) to identify event categories and filter on read.

Header flags

FlagBitDescription
FLAG_HAS_EVENTS0File contains an events section
FLAG_HAS_FOOTER1Footer with column offsets is present
FLAG_HAS_CHECKSUMS2Per-column CRC32 checksums in footer
FLAG_HAS_ROW_GROUPS3Multiple row groups with per-group statistics

Row groups

When FLAG_HAS_ROW_GROUPS is set, series data is partitioned into row groups. Each row group contains all series columns for a subset of rows, with codecs reset per group (enabling independent decoding).

The footer stores per-RG metadata: byte offset, row count, tsMin, tsMax. Readers can skip row groups whose timestamp range doesn't overlap the query window — enabling efficient HTTP range requests against remote files.

Row group size is configurable via setRowGroupSize() (default: 4096 rows). Files with fewer rows than the group size are written as a single implicit row group without the flag.

Integrity: per-column CRC32

When FLAG_HAS_CHECKSUMS is set, the footer contains one CRC32 (IEEE 802.3) per column per row group, computed over the compressed data bytes. The reader verifies each column on access — a corrupted column throws an exception identifying which column failed, while intact columns remain readable.

Codecs

CodecIDDataTypeUse case
DELTA_VARINT1LONGTimestamps (~1 byte/value for regular intervals)
ALP2DOUBLEDecimal doubles: prices, temperatures, measurements (~3-4 bits/value for 2dp)
GORILLA6DOUBLEXOR compression (Facebook VLDB 2015). Best for volatile metrics: CPU%, latency, network
PONGO7DOUBLEDecimal-aware erasure + Gorilla XOR. Best for decimal-native data: prices, sensor readings (~18 bits/value on 2dp)
VARLEN3BINARYShort strings, event types, labels
VARLEN_ZSTD4BINARYJSON payloads, bulk binary data
VARLEN_GZIP5BINARYMetadata, small text (browser DecompressionStream compatible)
RAW0LONG/DOUBLEUncompressed fallback

Usage

Write OHLCV ticker data

try (LastraWriter w = new LastraWriter(outputStream)) {
    w.addSeriesColumn("ts", DataType.LONG, Codec.DELTA_VARINT);
    w.addSeriesColumn("open", DataType.DOUBLE, Codec.ALP);
    w.addSeriesColumn("high", DataType.DOUBLE, Codec.ALP);
    w.addSeriesColumn("low", DataType.DOUBLE, Codec.ALP);
    w.addSeriesColumn("close", DataType.DOUBLE, Codec.ALP);
    w.addSeriesColumn("volume", DataType.DOUBLE, Codec.ALP);
    w.writeSeries(rowCount, ts, open, high, low, close, volume);
}

Write IoT sensor data with alerts

try (LastraWriter w = new LastraWriter(outputStream)) {
    // Series: sensor readings with metadata
    w.addSeriesColumn("ts", DataType.LONG, Codec.DELTA_VARINT);
    w.addSeriesColumn("temperature", DataType.DOUBLE, Codec.PONGO,
        Map.of("unit", "celsius", "sensor", "dht22"));
    w.addSeriesColumn("humidity", DataType.DOUBLE, Codec.ALP,
        Map.of("unit", "%", "sensor", "dht22"));
    w.addSeriesColumn("pressure", DataType.DOUBLE, Codec.GORILLA);

    // Events: alerts with their own timestamps
    w.addEventColumn("ts", DataType.LONG, Codec.DELTA_VARINT);
    w.addEventColumn("type", DataType.BINARY, Codec.VARLEN);
    w.addEventColumn("data", DataType.BINARY, Codec.VARLEN_ZSTD);

    w.writeSeries(sampleCount, ts, temp, humidity, pressure);
    w.writeEvents(alertCount, alertTs, alertTypes, alertData);
}

Write financial strategy results

try (LastraWriter w = new LastraWriter(outputStream)) {
    w.addSeriesColumn("ts", DataType.LONG, Codec.DELTA_VARINT);
    w.addSeriesColumn("close", DataType.DOUBLE, Codec.ALP);
    w.addSeriesColumn("ema1", DataType.DOUBLE, Codec.ALP,
        Map.of("indicator", "ema", "periods", "10"));
    w.addSeriesColumn("rsi1", DataType.DOUBLE, Codec.ALP,
        Map.of("indicator", "rsi", "periods", "14"));

    w.addEventColumn("ts", DataType.LONG, Codec.DELTA_VARINT);
    w.addEventColumn("type", DataType.BINARY, Codec.VARLEN);
    w.addEventColumn("data", DataType.BINARY, Codec.VARLEN_ZSTD);

    w.writeSeries(tickCount, ts, close, ema, rsi);
    w.writeEvents(signalCount, signalTs, signalTypes, signalData);
}

Write with row groups (for range queries)

try (LastraWriter w = new LastraWriter(outputStream)) {
    w.setRowGroupSize(3600);  // 1 hour of 1s ticks per row group
    w.addSeriesColumn("ts", DataType.LONG, Codec.DELTA_VARINT);
    w.addSeriesColumn("close", DataType.DOUBLE, Codec.ALP);
    // writeSeries auto-partitions into row groups
    w.writeSeries(86400, dayTimestamps, dayCloses);  // 24 RGs for a full day
}

Read (selective columns)

LastraReader r = LastraReader.from(inputStream);

// Read only what you need — other columns are not decompressed
long[] ts = r.readSeriesLong("ts");
double[] close = r.readSeriesDouble("close");

// Column metadata
Map<String, String> meta = r.getSeriesColumn("ema1").metadata();
// {"indicator": "ema", "periods": "10"}

// Events (independent timestamps)
long[] signalTs = r.readEventLong("ts");
byte[][] signalData = r.readEventBinary("data");

Read with temporal filtering (row groups)

LastraReader r = LastraReader.from(inputStream);

// Skip row groups outside the query window
long queryFrom = Instant.parse("2026-04-07T14:00:00Z").toEpochMilli();
long queryTo   = Instant.parse("2026-04-07T16:00:00Z").toEpochMilli();

for (int i = 0; i < r.rowGroupCount(); i++) {
    RowGroupStats stats = r.rowGroupStats(i);
    if (stats.tsMax() < queryFrom || stats.tsMin() > queryTo) continue;

    // Only decode row groups that overlap the query range
    long[] ts = r.readRowGroupLong(i, "ts");
    double[] close = r.readRowGroupDouble(i, "close");
}

Compression Ratios

OHLCV ticker data (1000 rows, 2 decimal places):

FormatSizeRatio
Raw48 KB1x
Lastra~3.5 KB~13x
Apache Parquet (SNAPPY)~8 KB~6x

Lastra vs Apache Parquet

Codec comparison

AspectApache ParquetLastra
Timestamps (int64)DELTA_BINARY_PACKED (~1-2 bytes/value)DELTA_VARINT (~1 byte/value)
Doubles (numeric)PLAIN + block compression (SNAPPY/ZSTD)ALP (~3-4 bits/value), Pongo (~18 bits/value), Gorilla (XOR)
Strings / binaryDICTIONARY + RLE, DELTA_BYTE_ARRAYVARLEN, VARLEN_ZSTD, VARLEN_GZIP
Block compressionSNAPPY, GZIP, ZSTD, LZ4, BROTLINo (compression integrated per codec)
Per-column codecSame codec per file or row groupDifferent codec per column
Optimized forGeneral purpose, big dataNumeric time series (financial, IoT, infra)

Real-world benchmark (BTC/USDT, 3,591 rows, 11 columns)

FormatSizeRatio vs Parquet
Apache Parquet (SNAPPY)118 KB1x
Lastra (ALP default)82 KB1.4x smaller
Lastra (mixed codecs via lastra-convert --best)73 KB1.6x smaller

Why Lastra compresses better for numeric time series

Apache Parquet stores doubles as raw 8 bytes (PLAIN encoding) then applies generic block compression (SNAPPY/ZSTD). Lastra applies semantic compression per column:

  • ALP: understands that 65007.28 has 2 decimal places → 3-4 bits/value
  • Pongo: detects decimal patterns and erases mantissa noise before XOR → ~18 bits/value
  • Gorilla: XOR between consecutive similar values → good for volatile data

Where Parquet wins

Apache Parquet has a much larger ecosystem (Spark, DuckDB, Arrow, Pandas) and advanced features: predicate pushdown, bloom filters, column statistics, nested types, and modular encryption.

Lastra vs Apache ORC

Codec comparison

AspectApache ORCLastra
Timestamps (int64)RLE + delta encoding (~1-2 bytes/value)DELTA_VARINT (~1 byte/value)
Doubles (numeric)PLAIN (raw 8 bytes) + block compression (ZLIB/ZSTD)ALP (~3-4 bits/value), Pongo (~18 bits/value), Gorilla (XOR)
Strings / binaryDICTIONARY + RLE, DIRECTVARLEN, VARLEN_ZSTD, VARLEN_GZIP
Block compressionZLIB, ZSTD, Snappy, LZ4No (compression integrated per codec)
Per-column codecSame codec for all columnsDifferent codec per column
Row groupsStripes (250 MB), row groups (10K rows)Configurable (default 4096 rows)
Predicate pushdownMin/max per stripe + bloom filterstsMin/tsMax per row group
ACID transactionsYes (Hive)No
SchemaProtobuf in footer (nested types, unions)Fixed 22-byte header (flat types)
DependenciesHadoop, Protobuf, hive-storage-apialp-java + zstd-jni
Optimized forData warehousing, ACID, big dataNumeric time series (financial, IoT, infra)

Why Lastra compresses doubles better than ORC

Apache ORC encodes doubles as raw 8 bytes (IEEE 754 PLAIN) then applies generic block compression (ZLIB/ZSTD) over the entire stripe. This is the same limitation as Parquet — generic compressors don't understand decimal structure.

Lastra applies semantic compression that understands the data:

  • ALP knows 65007.28 has 2 decimal places → encodes as integer 6500728 → 3-4 bits/value
  • Pongo erases mantissa noise before XOR → ~18 bits/value
  • Result: ~2x better compression than ORC for numeric time series

Where ORC wins

Apache ORC has ACID transaction support (Hive), bloom filters for equality predicates, excellent integer encoding (RLE v2), a mature ecosystem (Hive, Spark, Presto/Trino, Flink, Iceberg), and support for complex nested types via Protobuf schemas.

Lastra vs ClickHouse Native Format

ClickHouse is the closest comparison because it also supports per-column codec selection for doubles — unlike Parquet and ORC which use PLAIN encoding.

Codec comparison

AspectClickHouse nativeLastra
TimestampsDoubleDelta + LZ4 (~1-2 bytes/value)DELTA_VARINT (~1 byte/value)
Doubles (numeric)Gorilla (XOR), FPC (predictor-based) + ZSTDALP (~3-4 bits/value), Pongo (~18 bits/value), Gorilla (XOR)
Per-column codecYes (CODEC(Gorilla, ZSTD) per column in DDL)Yes (per column in file header)
Block compressionLZ4, LZ4HC, ZSTD (layered on top of codecs)No (compression integrated per codec)
Codec pipelineCodec first → block compression secondSingle-pass per codec
Row groupsGranules (8192 rows default, configurable)Configurable (default 4096 rows)
ALP supportNot yet (under discussion)Yes (via alp-java)
Format typeDatabase engine storage (.bin + .mrk2 + primary.idx)Standalone file (.lastra)
DependenciesFull ClickHouse serveralp-java + zstd-jni
Use caseOLAP database with SQL queriesPortable file format for archival and exchange

The double compression gap

ClickHouse offers Gorilla and FPC for Float64 columns — both are XOR/predictor-based codecs that work well for volatile metrics but don't understand decimal structure.

For financial prices like 65007.28:

  • ClickHouse Gorilla: XOR between consecutive values → ~32-40 bits/value (depends on volatility)
  • ClickHouse FPC: Predictor + XOR → similar range, faster decompression
  • Lastra ALP: Decimal-aware → 65007.28$ \times 100 = $6500728 → integer encoding → 3-4 bits/value
  • Lastra Pongo: Decimal erasure + XOR → ~18 bits/value

ALP is published research (ACM SIGMOD 2024) showing 50% better compression and 44x faster scans than Gorilla. ClickHouse has it as an open feature request but hasn't integrated it yet.

Where ClickHouse wins

ClickHouse is a full OLAP database — SQL queries, distributed joins, materialized views, real-time ingestion, replication, and a massive ecosystem. Lastra is a file format. If you need a query engine, use ClickHouse (and potentially store exports as .lastra files for archival or cross-system exchange).

Dependency (JitPack)

Maven

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

<dependency>
    <groupId>com.qtsurfer</groupId>
    <artifactId>lastra-java</artifactId>
    <version>x.x.x</version>
</dependency>

Gradle

repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.qtsurfer:lastra-java:x.x.x'
}

License

Copyright 2026 Wualabs LTD. Apache License 2.0 — see LICENSE.