Writing Files - SmithSoft-Studios/JAZMIN GitHub Wiki
Writing Files
There are three ways to write a JAZMIN file. All three produce the same format.
| Way | JavaScript | .NET | Use when |
|---|---|---|---|
| One call | write(path, rows, options), JAZMIN.stringify(rows) |
JazminConvert.SerializeObject(list), new JazminSerializer(settings).Serialize(path, list) |
The rows are already in memory, or come from an iterator |
| Streaming writer | new JazminWriter(path, options) + writeRow() + finish() |
JazminWriter.Create(path, columns, options) + WriteValues() / WriteRow() + Dispose() |
Millions of rows, generated or read from somewhere else |
| Async | writeAsync(), writer.writeRowsAsync() |
writer.WriteRowsAsync(), WriteValuesAsync() |
Rows come from a database cursor or network stream (see Server Side) |
Pass null as the path (JavaScript) or a Stream (.NET) to get the bytes instead of a file. When writing to a path
fails part-way, the half-written file is deleted.
Schemas: inferred or declared
Without columns, the schema is inferred from the rows:
- whole numbers become
int, other numbersfloat; - strings become
string(withdetectDates: true, ISO-8601 strings becomedatetime); Datebecomesdatetime,Bufferbecomesbinary, objects and arrays becomejson;- a column is nullable when any row leaves it out or holds
null.
Declare the columns for anything you keep: you choose exact types (decimal for money), nullability, indexes,
descriptions and attributes, and every row is checked against them.
import { inferSchema } from '@smithsoft-studios/jazmin';
const rows = [
{ id: 1, name: 'Ann', score: 9.5, joined: '2024-01-02T00:00:00Z', tags: ['a'] },
{ id: 2, name: 'Bob', score: 7, joined: '2024-03-04T00:00:00Z' },
];
for (const c of inferSchema(rows, { detectDates: true })) console.log(`${c.name}: ${c.type}${c.nullable ? ' (nullable)' : ''}`);
id: int
name: string
score: float
joined: datetime
tags: json (nullable)
Every column has:
| Property | Default | Meaning |
|---|---|---|
name |
required | Any non-empty text; names are matched exactly in JavaScript, case-insensitively by the .NET serializer |
type |
required | bool, int, float, decimal, string, datetime, binary or json (Core Concepts) |
nullable |
true |
false makes a null or missing value an error |
index |
none | 'sorted', 'trigram' or both: ['sorted', 'trigram'] |
description |
none | Travels with the file; shown by the viewer and the CLI |
attributes |
none | Free-form details, such as { unit: 'kPa' } or { currency: 'ZAR' } |
Rows are validated
A wrong type, a misspelt column or a null in a non-nullable column throws, naming the row and column. Nothing
half-written is left behind.
import { JazminValidationError, write } from '@smithsoft-studios/jazmin';
const columns = [{ name: 'id', type: 'int', nullable: false }, { name: 'name', type: 'string' }];
const attempts = [
[{ id: 1, name: 'Ann' }, { id: 'two', name: 'Bob' }],
[{ id: 1, nme: 'Ann' }],
[{ id: null, name: 'Ann' }],
];
for (const rows of attempts) {
try {
write(null, rows, { columns });
} catch (e) {
if (e instanceof JazminValidationError) console.log(e.message);
}
}
Column 'id': expected an integer, got string
Row 0: unknown column 'nme'
Row 0: column 'id' is not nullable
Options
| Option (JS / .NET) | Default | Effect |
|---|---|---|
columns / the columns argument |
inferred | The schema |
sortedBy / SortedBy |
none | Rows must arrive in this column order. Lookups and ranges on the first column skip straight to the right chunks, with no index; updates merge in place. A key can also ignore case or compare numbers by value (1.7.0, { column, nocase, natural }; .NET Sort): see Text orders |
indexes / [JazminIndex] or Indexes |
none | Shorthand: { country: 'sorted', name: ['trigram'] } |
codec / Codec |
deflate |
brotli: smaller, slower to write. none: for data that is already compressed |
level / CompressionLevel |
balanced | Deflate 0–9, Brotli 0–11 |
chunkRows / ChunkRows |
4,096 | Rows per chunk. Smaller chunks suit many single-row lookups; larger ones suit whole-file reads |
chunkBytes / ChunkBytes |
1 MiB | A chunk is closed early once its raw data reaches this size (wide rows) |
compactIndexes / CompactIndexes |
off | Much smaller sorted indexes on numbers and dates. Readers need 1.2 or later (New in 1.2) |
metadata / Metadata |
none | Your own key/values: { source: 'crm', exportedAt: '...' } |
key or password / Key or Password |
none | Encrypt the file (Encryption and Keys) |
kdfIterations / KdfIterations |
600,000 | PBKDF2 iterations for passwords |
access / Access |
none | One file, many keys (Access Control) |
tables / Tables |
one table | Several tables in one file (Several Tables) |
files, package / Files, Package |
none | Embedded files and documents (Embedded Files and Documents) |
locks / Locks |
none | Shared files: passwords or 2FA for stored files and masked columns (1.7.0; locked files, Masked Columns) |
onProgress, signal / Progress, CancellationToken |
none | Progress and Stop (1.7.0; for rewrites) |
priority / Priority |
balanced |
memory or speed first (Tuning Speed and Memory) |
maxDegreeOfParallelism / MaxDegreeOfParallelism |
by priority | Threads compressing chunks; 1 for the lowest memory |
What the options do to the size
The same 200,000 orders, written seven ways:
import { write } from '@smithsoft-studios/jazmin';
const regions = ['ZA', 'NA', 'BW', 'KE'];
const rows = Array.from({ length: 200_000 }, (_, i) => ({
id: i + 1,
at: new Date(Date.UTC(2026, 0, 1) + i * 60_000),
region: regions[i % 4],
customer: `C-${(i * 7919) % 10_000}`,
amount: ((i * 104729) % 100_000) / 100,
}));
const columns = [
{ name: 'id', type: 'int', nullable: false },
{ name: 'at', type: 'datetime', nullable: false },
{ name: 'region', type: 'string' },
{ name: 'customer', type: 'string' },
{ name: 'amount', type: 'float' },
];
const indexed = columns.map((c) => (['at', 'region', 'customer'].includes(c.name) ? { ...c, index: 'sorted' } : c));
const json = Buffer.byteLength(JSON.stringify(rows));
const size = (label, options) => {
const bytes = write(null, rows, { columns, ...options }).length;
console.log(label.padEnd(36), `${Math.round(bytes / 1024).toLocaleString('en-US').padStart(6)} KB`, `${(json / bytes).toFixed(0).padStart(3)}x smaller than JSON`);
};
console.log('JSON'.padEnd(36), `${Math.round(json / 1024).toLocaleString('en-US').padStart(6)} KB`);
size('deflate (default)', {});
size('brotli', { codec: 'brotli' });
size('none', { codec: 'none' });
size('deflate, sortedBy id', { sortedBy: ['id'] });
size('deflate, 3 sorted indexes', { columns: indexed });
size('deflate, 3 indexes, compactIndexes', { columns: indexed, compactIndexes: true });
size('brotli, 3 indexes, compactIndexes', { columns: indexed, compactIndexes: true, codec: 'brotli' });
JSON 18,575 KB
deflate (default) 1,155 KB 16x smaller than JSON
brotli 796 KB 23x smaller than JSON
none 3,079 KB 6x smaller than JSON
deflate, sortedBy id 1,155 KB 16x smaller than JSON
deflate, 3 sorted indexes 2,267 KB 8x smaller than JSON
deflate, 3 indexes, compactIndexes 1,175 KB 16x smaller than JSON
brotli, 3 indexes, compactIndexes 808 KB 23x smaller than JSON
sortedBycosts nothing (these rows were already in id order) and makes an index onidunnecessary.- Indexes can be the largest part of a file: here three sorted indexes nearly doubled it. With
compactIndexes, the same indexes take about 20 KB instead of 1.1 MB, because times and ids that follow each other are stored as small differences. Index only the columns you search on, and usecompactIndexesonce every reader is on 1.2. - Brotli saves another 30% for archives; it is slower to write, and the browser reader can't read it.
- Even uncompressed, the file is several times smaller than JSON: names are stored once and values in binary.
The streaming writer
import { JazminWriter, open } from '@smithsoft-studios/jazmin';
const writer = new JazminWriter('sensors.jzm', {
columns: [
{ name: 'sensor', type: 'string', nullable: false },
{ name: 'at', type: 'datetime', nullable: false },
{ name: 'pressure', type: 'float', description: 'Line pressure', attributes: { unit: 'kPa' } },
],
sortedBy: ['sensor', 'at'], // rows must arrive in this order
metadata: { site: 'Plant 4', exportedBy: 'nightly-job' },
});
for (const sensor of ['S-01', 'S-02', 'S-03']) {
for (let m = 0; m < 1440; m++) writer.writeRow({ sensor, at: new Date(Date.UTC(2026, 4, 1, 0, m)), pressure: 300 + (m % 60) });
}
writer.finish(); // required: writes the indexes, catalog and trailer
const reader = open('sensors.jzm');
console.log(reader.rowCount, 'rows, sorted by', reader.sortedBy, 'metadata', reader.metadata);
console.log(reader.columns[2]);
console.log(reader.count({ sensor: 'S-02', at: { gte: new Date('2026-05-01T12:00Z') } }), 'S-02 readings after noon');
4320 rows, sorted by [ 'sensor', 'at' ] metadata { site: 'Plant 4', exportedBy: 'nightly-job' }
{
name: 'pressure',
type: 'float',
nullable: true,
description: 'Line pressure',
attributes: { unit: 'kPa' }
}
720 S-02 readings after noon
finish()is required. Until it runs, the file has no catalog. Callabort()to stop and delete it.- Memory is one chunk plus the index lists, whatever the row count.
- Out-of-order rows in a
sortedByfile throw aJazminValidationErrornaming the row.
In .NET, Dispose() finishes the file (unless a row failed validation), and WriteValues takes the values in column
order, with no dictionary per row:
using Jazmin;
using System.Text.Json.Nodes;
var columns = new[]
{
new JazminColumn("sensor", JazminType.String) { Nullable = false },
new JazminColumn("at", JazminType.DateTime) { Nullable = false },
new JazminColumn("pressure", JazminType.Float) { Description = "Line pressure", Attributes = new JsonObject { ["unit"] = "kPa" } },
};
var options = new JazminWriteOptions
{
SortedBy = ["sensor", "at"],
Metadata = new JsonObject { ["site"] = "Plant 4" },
};
using (var writer = JazminWriter.Create("sensors.jzm", columns, options))
{
foreach (var sensor in new[] { "S-01", "S-02", "S-03" })
for (int m = 0; m < 1440; m++)
writer.WriteValues(sensor, new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc).AddMinutes(m), 300.0 + m % 60);
}
using var reader = JazminReader.Open("sensors.jzm");
Console.WriteLine($"{reader.RowCount} rows, sorted by {string.Join(", ", reader.SortedBy!)}, site {reader.Metadata["site"]}");
Console.WriteLine($"{reader.Columns[2]} ({reader.Columns[2].Description}, {reader.Columns[2].Attributes!.ToJsonString()})");
4320 rows, sorted by sensor, at, site Plant 4
pressure: float? (Line pressure, {"unit":"kPa"})
Writing from your own classes (.NET)
The serializer builds the schema from your class: see .NET Serializer and LINQ for the
attributes ([JazminIndex], [JazminProperty], [JazminIgnore]) and settings.
using Jazmin;
using Jazmin.Serialization;
var settings = new JazminSerializerSettings
{
Codec = JazminCodec.Brotli,
Indexes = new() { ["Region"] = [JazminIndexKind.Sorted] }, // indexes without attributes
};
var orders = Enumerable.Range(1, 1000).Select(i => new Order { Id = i, Region = i % 2 == 0 ? "ZA" : "NA", Amount = i * 1.5m });
new JazminSerializer(settings).Serialize("orders.jzm", orders); // streams: the sequence is not collected first
using var reader = JazminReader.Open("orders.jzm");
Console.WriteLine(string.Join(", ", reader.Columns));
Console.WriteLine(string.Join(", ", reader.Indexes.Select(i => $"{i.Column} {i.Kind}")));
public class Order
{
public int Id { get; set; }
public string Region { get; set; } = "";
public decimal Amount { get; set; }
}
Id: int, Region: string?, Amount: decimal
Region Sorted
Good to know
- Write once, sorted. If you know how the file will be queried, sort the rows by that column and say so with
sortedBy. It is the cheapest index there is. See Tuning Speed and Memory. - Decimals: pass them as strings in JavaScript (
'1520.75'), or asdecimalin .NET. A JavaScript number written to a decimal column is accepted, but a number like0.1 + 0.2is already inexact before it arrives. - Dates are stored in UTC with millisecond precision. In .NET, a
DateTimeof kindLocalis converted to UTC;Unspecifiedis taken as UTC. - Big integers: JavaScript
bigintvalues beyond ±2^53 are stored exactly, and come back asbigint. - Lists and objects are stored as JSON text by default. From 1.4, nested columns store them as
columns of their own fields: smaller and faster to read, and filtered with
any,allandmatch. - Changing a file later doesn't need a full rewrite by hand: see Updating, Appending and Compacting.