peachq v0.81 released¶
PeachQ 0.81 is out. Two features carry this release: enumerations are now a real datatype rather than something the engine resolves away, and there is a CSV reader that will tell you what it found instead of guessing.
Enumerations are a real datatype¶
`sym$ produces an enumerated vector, type 20h, which keeps its domain
rather than collapsing back to symbols.
q)sym:`ibm`msft`goog
q)e:`sym$`ibm`msft`ibm
q)e
`sym$`ibm`msft`ibm
q)type e
20h
q)value e
`ibm`msft`ibm
The point is what does not happen. A comparison against an enum column stays in position space — the integer positions the enum already holds — instead of materialising the column back into symbols to compare them. On a wide column with a small domain, that is the difference between comparing a few integers and rebuilding every symbol in the table.
q)t:([]s:`sym$`ibm`msft`ibm;px:10 20 30)
q)select from t where s=`ibm
| s | px |
| symbol | long |
|--------|------|
| ibm | 10 |
| ibm | 30 |
An enum column in kx splayed on-disk data also reads back enum-native. It
arrives as 20h with its domain intact, rather than being resolved to symbols
at load and losing the thing that made it cheap.
Foreign keys, built from the same parts¶
Foreign keys and link columns are no longer a separate mechanism from enums —
they are the same structure pointed at a keyed table instead of a symbol list.
meta names the domain and fkeys reports the mapping.
q)k:([id:1 2 3]nm:`x`y`z)
q)u:([]f:`k$1 2;v:10 20)
q)fkeys u
f| k
Reading CSV without guessing¶
.csv.read loads a file into a table and hands back a summary of what it did.
q)\l pq
q).csv.read[`:trades.csv;`trades;()!();()!()]
rows | 3
rejected| 0
chunks | 1
ignored | `symbol$()
types | `sym`px`qty`dt!"*fjd"
Read the types line: px became a float, qty a long, dt a date — and
sym stayed *, a string. That is deliberate. The sniffer reads structure,
not intent: it recognises the forms csv 0: writes, and it will not promote a
text column to symbols because the values happen to look like symbols. Guessing
that wrong is expensive to undo once a table is loaded.
q)trades
| sym | px | qty | dt |
| | float | long | date |
|--------|-------|------|------------|
| "ibm" | 10.5 | 100 | 2026.01.02 |
| "msft" | 20.25 | 200 | 2026.01.03 |
| "goog" | 30 | 300 | 2026.01.04 |
When you know better, say so. The third argument is a schema, one type char per column, and it overrides the sniff:
q).csv.read[`:trades.csv;`t2;`sym`px`qty`dt!"sfjd";()!()]
rows | 3
rejected| 0
chunks | 1
ignored | `symbol$()
types | `sym`px`qty`dt!"sfjd"
q)t2
| sym | px | qty | dt |
| symbol | float | long | date |
|--------|-------|------|------------|
| ibm | 10.5 | 100 | 2026.01.02 |
| msft | 20.25 | 200 | 2026.01.03 |
| goog | 30 | 300 | 2026.01.04 |
Files are read incrementally — bytes to rows with a carry, batch by batch —
rather than slurped whole, and the target argument can be a callback instead
of a table name if you want to handle each batch yourself.
The reader also takes a timezone position rather than papering over one. kdb
parses no timezone, so a cell carrying an offset stays text at defaults, bytes
intact. An offset is applied only when you ask for it with timestampformat's
%z, and then the stored timestamp is UTC. There is no silent shift.
Bad rows go somewhere, not nowhere¶
By default a row that cannot be read is an error, loudly:
q).csv.read[`:bad.csv;`t4;`sym`px`qty!"sfj";()!()]
'csv
That is the right default — a malformed load should not look like a clean one.
But when you are importing real data you usually want the other 99% of it, and
you want to know exactly what you lost. store_rejects continues the load and
records every rejected row:
q).csv.read[`:bad.csv;`t5;`sym`px`qty!"sfj";(enlist`store_rejects)!enlist 1b]
rows | 2
rejected| 1
chunks | 1
ignored | `symbol$()
types | `sym`px`qty!"sfj"
q)reject_errors
| line | column | error | csvLine |
| long | symbol | symbol | |
|------|--------|--------|-----------------------|
| 3 | px | cast | "msft,notanumber,200" |
Line number, column, error class, and the raw text of the line as it appeared. The options govern whether the load survives a bad row, never whether it is counted — a rejected row is always recorded and always in the summary, so a tolerant load cannot quietly become a lossy one.
Comparison that survives floating point¶
=, the ordering operators, ~, within, differ and Converge's stop test
all share one relative tolerance, E=2^-43.
q)1f=1-2 xexp -45
1b
q)1f=1-0.1
0b
Close enough is equal; actually different is not. Integer-backed types stay
exact, in stays exact, and only zero is tolerantly equal to zero. The
practical effect shows up in convergence, where an iteration that shrinks by a
hair used to grind toward denormal:
q){x*1-1e-14} over 1f
1f
Also in this release¶
inv and lsq are native — matrix inverse by LU with partial pivoting, least
squares through a Cholesky factorisation, with the elimination order pinned so
results reproduce.
q)inv (2 0f;0 4f)
0.5 0
0 0.25
Keyed tables carry their key rule into the modern grid, and emitted text is ASCII with a gate that keeps it that way.
Downloads are on the releases page. If you have a q library you would like PeachQ to run next, try it and tell us where it breaks.