Reading and Querying - SmithSoft-Studios/JAZMIN GitHub Wiki
Opening a file reads only its catalog. Everything after that reads as little as it can: the index pages that help, the chunks that can match, and in them only the columns you use.
| JavaScript | .NET | Returns |
|---|---|---|
open(pathOrBytes, { key }) |
JazminReader.Open(path, new() { Key = key }) |
A reader (reads the catalog only) |
reader.find(filter, { select, offset, limit }) |
reader.Find(filter, new JazminQueryOptions { ... }) |
Matching rows, streamed |
reader.rows({ select, offset, limit }) |
reader.Rows(options) |
Every row, streamed |
reader.get(rowId) |
reader.Get(rowId) |
One row by position (0-based) |
reader.count(filter) |
reader.Count(filter) |
Number of matching rows, often without reading rows |
reader.columnArrays(filter, { select }) |
One typed array per column, for charts and totals | |
reader.explain(filter, { analyze }) |
reader.Explain(filter, analyze) |
How the query runs, and what it read |
reader.advise({ columns }) |
Layout advice from the statistics | |
reader.Query<T>(predicate), reader.AsQueryable<T>()
|
Typed objects through LINQ (.NET Serializer and LINQ) | |
reader.findAsync(), openAsync()
|
reader.FindAsync(), JazminReader.OpenAsync()
|
Non-blocking versions (Server Side) |
reader.close() |
reader.Dispose() |
Closes the file |
Reader properties: columns, metadata, rowCount, chunkCount, sortedBy, indexes, encrypted, writtenAt,
tables, files, access (.NET: Columns, Metadata, RowCount, and so on).
import { open, write } from '@smithsoft-studios/jazmin';
const regions = ['ZA', 'NA', 'BW', 'KE'];
write('orders.jzm', Array.from({ length: 50_000 }, (_, i) => ({
id: i + 1,
region: regions[i % 4],
customer: `C-${String((i * 7919) % 5000).padStart(4, '0')}`,
amount: ((i * 104729) % 100_000) / 100,
paid: i % 5 !== 0,
})), {
columns: [
{ name: 'id', type: 'int', nullable: false },
{ name: 'region', type: 'string', index: 'sorted' },
{ name: 'customer', type: 'string', index: 'sorted' },
{ name: 'amount', type: 'float' },
{ name: 'paid', type: 'bool' },
],
sortedBy: ['id'],
});
const reader = open('orders.jzm');
console.log(reader.rowCount, 'rows,', reader.chunkCount, 'chunks');
// One row by position, and the rows of one customer
console.log(reader.get(41));
console.log([...reader.find({ customer: 'C-0042' }, { select: ['id', 'amount'], limit: 3 })]);
// Paging: the third page of 3 big unpaid orders in ZA
const where = { region: 'ZA', paid: false, amount: { gte: 900 } };
console.log([...reader.find(where, { select: ['id', 'amount'], offset: 6, limit: 3 })]);
// Counting and totals
console.log(reader.count(where), 'matching orders');
const { values } = reader.columnArrays({ region: 'NA' }, { select: ['amount'] });
console.log('NA total:', Math.round(values.amount.reduce((a, b) => a + b, 0)));
// How a query runs, and what it read
console.log(reader.explain({ customer: 'C-0042' }));
console.log(reader.explain({ id: { gte: 30_000, lt: 30_010 } }));
reader.close();50000 rows, 13 chunks
{
id: 42,
region: 'NA',
customer: 'C-4679',
amount: 938.89,
paid: true
}
[
{ id: 2519, amount: 76.22 },
{ id: 7519, amount: 526.22 },
{ id: 12519, amount: 976.22 }
]
[
{ id: 1141, amount: 910.6 },
{ id: 1481, amount: 989.2 },
{ id: 1501, amount: 935 }
]
251 matching orders
NA total: 6249875
{ strategy: 'index', candidateRows: 10 }
{ strategy: 'scan', chunks: 13, chunksSkipped: 12 }
-
find()androws()are generators. Rows come one at a time, and only one chunk is held in memory. Loop withfor…ofto stream, or spread with[...]to collect. -
selectdecodes only the columns you list (plus the ones the filter needs). Rows come back with just those. -
get(rowId)reads the chunk holding the row; the reader keeps the last chunk it decoded, so fetching rows in position order is fast. Sort a list of row ids before fetching them. -
count()answers from an index or from chunk statistics where it can, without reading rows. -
columnArrays()gives aFloat64Arrayper number or date column: about a tenth of the memory of row objects.
offset and limit page through results. A chunk that lies wholly before the offset is counted, not read, as long
as every row in it is known to match: with no filter, or when chunk statistics prove it (such as a date range on the
file's first sortedBy column). So the last page of a large file costs about the same as the first.
For deep pages of a filtered query, use keyset paging: remember the last row of a page and ask for the rows after it. It stays fast however deep, and pages don't shift when rows are added.
import { open, write } from '@smithsoft-studios/jazmin';
write('events.jzm', Array.from({ length: 200_000 }, (_, i) => ({ id: i + 1, kind: i % 3 === 0 ? 'error' : 'info' })), {
sortedBy: ['id'],
});
const cost = (filter, options) => {
const reader = open('events.jzm');
const { rows, chunksRead } = reader.explain(filter, { ...options, analyze: true });
reader.close();
return `${rows} rows, ${chunksRead} chunks read`;
};
console.log('first page ', cost(null, { limit: 50 }));
console.log('page at 190,000 ', cost(null, { offset: 190_000, limit: 50 }));
console.log('errors, offset ', cost({ kind: 'error' }, { offset: 60_000, limit: 50 }));
console.log('errors, keyset ', cost({ kind: 'error', id: { gt: 180_000 } }, { limit: 50 }));first page 50 rows, 1 chunks read
page at 190,000 50 rows, 1 chunks read
errors, offset 50 rows, 44 chunks read
errors, keyset 50 rows, 1 chunks read
The filtered offset query had to read every chunk before the offset to count its errors; the keyset query went
straight to the right chunk because the file is sorted by id.
With reverse: true the reader starts at the last chunk and works back, so in a file sorted by date the newest rows
come first, as fast as the oldest. offset and limit count from the end. It works with any filter, an index lookup
and an access key, in find, rows, findAsync, findBatchesAsync, the exports (toJSON(reader, { reverse: true, limit: 20 })) and the browser reader's find and query. No format change: every file can be read this way.
import { from, open, write } from '@smithsoft-studios/jazmin';
const rows = Array.from({ length: 10_000 }, (_, i) => ({ id: i, at: new Date(Date.UTC(2026, 0, 1) + i * 60_000), account: `A${i % 50}` }));
write('journal.jzm', rows, {
columns: [{ name: 'id', type: 'int' }, { name: 'at', type: 'datetime' }, { name: 'account', type: 'string', index: ['sorted'] }],
sortedBy: ['at'],
});
const journal = open('journal.jzm');
console.log('newest 3 of A7:', [...journal.find({ account: 'A7' }, { reverse: true, limit: 3 })].map((t) => t.id));
console.log('newest 2: ', [...journal.rows({ reverse: true, limit: 2 })].map((t) => t.id));
console.log('from(): ', from(journal).where((t) => t.account === 'A7').reverse().first().id);
journal.close();newest 3 of A7: [ 9957, 9907, 9857 ]
newest 2: [ 9999, 9998 ]
from(): 9957
using Jazmin;
using Jazmin.Query;
using (var writer = JazminWriter.Create("journal.jzm",
[new JazminColumn("id", JazminType.Int), new JazminColumn("at", JazminType.DateTime), new JazminColumn("account", JazminType.String) { Indexes = [JazminIndexKind.Sorted] }],
new JazminWriteOptions { SortedBy = ["at"] }))
{
for (var i = 0; i < 10_000; i++) writer.WriteValues((long)i, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMinutes(i), $"A{i % 50}");
}
using var journal = JazminReader.Open("journal.jzm");
var newest = journal.Find(JazminFilter.Eq("account", "A7"), new JazminQueryOptions { Reverse = true, Limit = 3 }).Select(r => r["id"]);
Console.WriteLine($"newest 3 of A7: {string.Join(", ", newest)}");newest 3 of A7: 9957, 9907, 9857
-
Queries:
from(reader)…reverse()in JavaScript and the browser, and LINQ'sReverse()in .NET (reader.AsQueryable<Txn>().Where(t => t.Account == "A7").Reverse().Take(20)), read last first when they come straight after thewherefunctions, or afterorderByon the file's own sort order. Elsewhere they reverse in memory, as LINQ does. - Why it matters: before, "the latest 20" meant counting the matches and then reading the last page. On 1,000,000 rows sorted by time, one account's newest 20 (through an index) took 5.2 ms instead of 46.5 ms.
-
Rows with the same sort value come last first too. To keep those in file order, sort instead
(
orderByDescending), which reads every match. -
Float columns: a filter on a float column can't skip chunks this way, because their statistics leave out
NaN.
If you hold a list of row positions (from your own pointer table, or a join between tables), sort it first. The reader keeps only the last chunk it decoded, so random order can read the same chunk again and again. On 200,000 transactions, fetching one day's 549 rows took 673 ms in time order and 62 ms sorted by position.
const rows = [...rowIds].sort((a, b) => a - b).map((id) => reader.get(id)); // one pass over the filefrom(reader) queries a table LINQ-style, with arrow functions, in Node and the browser (await JazminBrowser.from(reader)…). The reader still uses its indexes, statistics and sort order, and reads only the columns
the functions use:
const big = from(transactions)
.where((t) => t.category === 'Travel' && t.amount < -20000) // read as { category: 'Travel', amount: { lt: -20000 } }
.join(from(clients), (t) => t.client, (c) => c.id, (t, c) => ({ name: c.name, amount: t.amount }))
.orderBy((x) => Number(x.amount))
.toArray();
const city = 'Durban';
from(clients).where((c, $) => c.city === $.city, { city }).count(); // values from outside: the second argument-
How:
wherereads the function's text (JavaScript gives a library no description of a function, as C# does for LINQ) and turns comparisons,&&/||/!,.includes/.startsWith/.toLowerCase().includeson text,[…].includes(t.column)and== nullinto the filter language. -
Always the function's answer: the filter only narrows the rows read; the function still runs on them, unless the
filter says exactly what it says. A function it can't fully read (a helper, a regular expression) reads more rows,
but gives the right ones.
explain()shows the filter, the columns, and what wasn't translated. -
The rest runs in memory, as LINQ to Objects:
select,orderBy/thenBy,skip/take,join,groupJoin,groupBy,count,sum,min,max,average,first,any. - Measured on 1,000,000 transactions: 0.12-0.21 s per query, against 0.6 s for plain JavaScript over every row.
using Jazmin;
using Jazmin.Query;
var regions = new[] { "ZA", "NA", "BW", "KE" };
var columns = new[]
{
new JazminColumn("id", JazminType.Int) { Nullable = false },
new JazminColumn("region", JazminType.String) { Indexes = [JazminIndexKind.Sorted] },
new JazminColumn("amount", JazminType.Float),
};
using (var writer = JazminWriter.Create("orders.jzm", columns, new JazminWriteOptions { SortedBy = ["id"] }))
for (int i = 0; i < 50_000; i++) writer.WriteValues(i + 1L, regions[i % 4], (i * 104729L % 100_000) / 100.0);
using var reader = JazminReader.Open("orders.jzm");
// The filter builder: & is AND, | is OR, ! is NOT
var bigZa = JazminFilter.Eq("region", "ZA") & JazminFilter.Gte("amount", 990.0);
Console.WriteLine($"{reader.Count(bigZa)} big ZA orders");
// The same filter as JSON: the language JavaScript and GraphQL use
foreach (var row in reader.Find("""{ "region": "ZA", "amount": { "gte": 990 } }""", new JazminQueryOptions { Select = ["id", "amount"], Limit = 3 }))
Console.WriteLine($"row {row.RowId}: id {row["id"]}, amount {row["amount"]}");
Console.WriteLine(reader.Get(41)["region"]);
var plan = reader.Explain(JazminFilter.Gte("id", 30_000L) & JazminFilter.Lt("id", 30_010L), analyze: true);
Console.WriteLine($"{plan.Strategy}: {plan.ChunksSkipped} of {plan.Chunks} chunks skipped, {plan.Cost!.Rows} rows, {plan.Cost.ChunksRead} chunk read");130 big ZA orders
row 148: id 149, amount 998.92
row 296: id 297, amount 997.84
row 444: id 445, amount 996.76
NA
scan: 12 of 13 chunks skipped, 10 rows, 1 chunk read
JazminRow is a read-only dictionary: row["amount"], row.RowId, row.TryGetValue(...), and column names are
looked up case-insensitively when there is no exact match. For typed objects, use Query<T> or AsQueryable<T>:
see .NET Serializer and LINQ.
explain(filter) says how a query will run: strategy: 'index' with the candidate row count, or strategy: 'scan'
with the chunks the statistics skip. With { analyze: true } it also runs the query and reports:
| Field | Meaning |
|---|---|
rows |
Rows returned |
bytesRead |
Bytes read: chunks, index pages, statistics and directories |
chunksRead |
Chunks read and decoded |
indexPagesRead |
Index sections read |
columnsDecoded |
Column streams decoded (one per column per chunk) |
ms (.NET Elapsed) |
Time taken |
Use a freshly opened reader for analyze: a reader keeps the indexes it has loaded and its last chunk, so on a
reused reader the numbers show only what that query added. A query that reads many chunks to return a few rows needs
a better layout: sort by the filtered column, add an index, or use smaller chunks. reader.advise() suggests one
from the statistics alone; see Tuning Speed and Memory.
reader.estimate(filter, { offset }) (.NET reader.Estimate(filter, offset)) says what a read would cost without
reading a row. It uses only the chunk directories, the statistics and the indexes, so it is quick, and a program can
warn before a slow query or ask first. Both libraries give the same figures.
| Field | Meaning |
|---|---|
chunks, rows, bytes
|
What find() reads at most (a limit can end it sooner) |
countChunks, countRows, countBytes
|
What count() reads: 0 when indexes or statistics answer it |
import { open, write } from '@smithsoft-studios/jazmin';
const cities = ['Durban', 'Cape Town', 'Pretoria'];
const rows = Array.from({ length: 20_000 }, (_, i) => ({
id: i, at: new Date(Date.UTC(2026, 0, 1) + i * 60_000), account: `A${i % 50}`, city: cities[i % 3],
}));
write('ledger.jzm', rows, {
columns: [
{ name: 'id', type: 'int' }, { name: 'at', type: 'datetime' },
{ name: 'account', type: 'string', index: ['sorted'] }, { name: 'city', type: 'string' },
],
sortedBy: ['at'], chunkRows: 1000,
});
const reader = open('ledger.jzm');
const show = (what, e) => console.log(what, `find: ${e.chunks} chunks, ${e.rows} rows; count: ${e.countChunks} chunks`);
show('no index: ', reader.estimate({ city: 'Durban' }));
show('an index: ', reader.estimate({ account: 'A7' }));
show('the sort order:', reader.estimate({ at: { gte: new Date(Date.UTC(2026, 0, 14)) } }));
reader.close();no index: find: 20 chunks, 20000 rows; count: 20 chunks
an index: find: 20 chunks, 400 rows; count: 0 chunks
the sort order: find: 2 chunks, 2000 rows; count: 1 chunks
The index answers the count of A7 on its own; the date range skips 18 of the 20 chunks, and statistics show that every
row of one of the two matches, so a count reads only the other. explain() is unchanged: it describes the plan, and
with analyze runs it.
// fragment
var estimate = reader.Estimate(JazminFilter.Eq("city", "Durban")); // JazminReadEstimate { Chunks, Rows, Bytes, CountChunks, CountRows, CountBytes }One case costs more: a range an index answers with millions of rows takes about as long to plan as the read itself
would (id >= 20,000,000 in a file of 25,000,000 rows: about 1 s), because the reader lists the index's rows to plan the read.
A long read can say how far it has got, and be stopped, as the browser reader already could:
-
JavaScript:
onProgressis called after each chunk with{ done, total, matches }(chunks read, chunks planned, matching rows so far), andsignal, anAbortSignal, stops the read before its next chunk.count,countAsync,find,findAsync,findBatchesAsync,columnArrays,explain({ analyze: true })and the exports take both. A stopped read throws anAbortError. -
.NET:
JazminQueryOptions.Progress(anIProgress<JazminReadProgress>:Done,Total,Matches) andJazminQueryOptions.CancellationTokenforFind,FindAsyncandExplain(filter, true, options), andCount(filter, progress, token)/CountAsync(filter, progress, token). A stopped read throws anOperationCanceledException. - Without the options, reads take the same time as before. A read that indexes or statistics answer without reading a chunk reports nothing.
import { open, write } from '@smithsoft-studios/jazmin';
const cities = ['Durban', 'Cape Town', 'Pretoria'];
write('ledger.jzm', Array.from({ length: 20_000 }, (_, i) => ({ id: i, city: cities[i % 3] })), { chunkRows: 1000 });
const reader = open('ledger.jzm');
let last;
const matches = reader.count({ city: 'Durban' }, { onProgress: (p) => { last = p; } });
console.log(matches, 'matches; last report:', last);
// countAsync gives the event loop a turn between chunks, so a Stop button (here: after 5 chunks) can stop it
const stop = new AbortController();
try {
await reader.countAsync({ city: 'Durban' }, { signal: stop.signal, onProgress: ({ done }) => { if (done === 5) stop.abort(); } });
} catch (e) {
console.log('stopped:', e.name);
}
reader.close();6667 matches; last report: { done: 20, total: 20, matches: 6667 }
stopped: AbortError
using Jazmin;
using Jazmin.Query;
using (var writer = JazminWriter.Create("ledger.jzm", [new JazminColumn("id", JazminType.Int), new JazminColumn("city", JazminType.String)],
new JazminWriteOptions { ChunkRows = 1000 }))
{
string[] cities = ["Durban", "Cape Town", "Pretoria"];
for (var i = 0; i < 20_000; i++) writer.WriteValues((long)i, cities[i % 3]);
}
using var reader = JazminReader.Open("ledger.jzm");
var last = new JazminReadProgress();
var matches = reader.Count(JazminFilter.Eq("city", "Durban"), new Reporter(p => last = p));
Console.WriteLine($"{matches} matches; last report: {last.Done} of {last.Total} chunks, {last.Matches} matches");
using var stop = new CancellationTokenSource();
try
{
reader.Count(JazminFilter.Eq("city", "Durban"), new Reporter(p => { if (p.Done == 5) stop.Cancel(); }), stop.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("stopped");
}
// Progress<T> reports on another thread; this reports at once, on the reading thread
sealed class Reporter(Action<JazminReadProgress> report) : IProgress<JazminReadProgress>
{
public void Report(JazminReadProgress value) => report(value);
}6667 matches; last report: 20 of 20 chunks, 6667 matches
stopped
count and find hold their thread until they finish, so only onProgress itself can stop them in JavaScript. Use the
async forms when a button or a timeout must be able to stop the read. In .NET, cancel the token from any thread. The
time left can be worked out from the speed so far: elapsed / done * (total - done). Rewrites report progress too: see
Updating, Appending and Compacting.
- Readers are not thread-safe. Open one per thread or worker; opening is cheap (it reads the catalog).
- Open once, query many times. The reader caches the index pages and statistics it has read.
-
Closing:
close()in JavaScript,using/Dispose()in .NET. A reader over aBufferorbyte[]holds no file handle. - Encrypted files read at about the same speed as plain ones.