Docs · Working with tables
Missing values
How Vectro marks a missing value (NaN for numbers and dates, an empty string for text) and how filters, aggregates, joins, sorts and SQL treat it.
Vectro has no separate null marker. A missing number or date is NaN; missing text is ''. Aggregates skip missing values the way SQL skips NULL, so a blank price doesn’t drag an average to zero.
import { Engine } from 'vectrojs';
const engine = new Engine();
const orders = engine.table({
region: ['EU', 'US', '', 'APAC', 'US', 'EU'],
price: [120, NaN, 45, NaN, 99, 15],
});
console.log(orders.agg({
rows: 'count()',
priced: 'count(price)',
total: 'sum(price)',
average: 'mean(price)', // 279 / 4, not 279 / 6
cheapest: 'min(price)',
}));
await engine.close();
Where missing values come from
| Source | Missing number or date | Missing text |
|---|---|---|
| CSV | an empty field | an empty field |
| JSON lines | null, or the key absent |
null, or the key absent |
| Arrow | a null | a null |
engine.table(columns) |
NaN, null or undefined |
'' |
In engine.table and engine.tableFromRows, null and undefined in a number column load as missing, like NaN. A missing date in an array of Date objects can be null too.
When rows come back from toRows(), missing numbers are NaN, missing text is '' and missing dates are null.
Aggregates
| Aggregate | With missing values |
|---|---|
count() |
counts every row |
count(x) |
counts the rows where x is present |
count(x > 5) |
counts the rows where the condition holds (a missing x doesn’t) |
sum(x) |
adds the values present |
mean(x) |
divides by the number of values present |
min(x), max(x) |
ignore missing values; NaN if none are present |
countDistinct(x), approxDistinct(x) |
a missing number doesn’t count as a value |
These rules are about missing numbers and dates. On a text column, count(region) and countDistinct(region) count '' like any other label; filter it out first with filter("region != ''") if it shouldn’t count.
One difference from SQL: in agg and groupBy().agg, the sum of no values is 0. SQL’s SUM of no values is NULL, and sql() gives NaN there.
In expressions
isMissing(x) is true where x is missing, and coalesce(x, y, ...) gives the first value that is present. Both work in any expression: filters, derived columns, aggregates.
import { Engine } from 'vectrojs';
const engine = new Engine();
const t = engine.table({ price: [120, NaN, 45, NaN, 99], qty: [2, 1, 4, 1, 3] });
console.log(t.filter('isMissing(price)').count());
console.log(t.sum('coalesce(price, 0) * qty'));
// Comparisons with NaN are false, so negations let missing rows through:
console.log(t.filter('price > 50').count()); // 2
console.log(t.filter('!(price > 50)').count()); // 3: 45 and the two missing
console.log(t.filter('price != 50').count()); // 5
console.log(t.filter('!(price > 50) && !isMissing(price)').count()); // 1
await engine.close();
Expressions follow JavaScript’s rule for NaN: every comparison with it is false. So price > 50 drops missing prices, but !(price > 50) and price != 50 keep them. Add !isMissing(price) when you mean “a price that isn’t over 50”.
For text, compare with the empty string: filter("region == ''").
Group by, joins and sort
- Group by: a missing number key (
NaN) is a group of its own, and comes after the other numbers. A missing text key''is a group of its own too. - Joins: a key with a missing part never matches, as
NULLdoesn’t in SQL. A left join fills the right columns of unmatched rows withNaNor''. See joins. - Sort and top: missing values come last, whether you sort up or down. SQL’s
NULLS FIRSTputs them at the start. See sort and top-k. - Compressed columns store a missing value as a reserved code, so packing never loses one. See compressed columns.
In SQL
sql() uses the same markers, read as NULL:
x IS NULLandx IS NOT NULLwork on numbers, dates and text (where''is null).COALESCE,IFNULL,NVLandNULLIFwork on numbers and text.COUNT(x),SUM,AVG,MIN,MAX, percentiles and window aggregates skip missing numbers.ORDER BY x NULLS FIRST | NULLS LASTplaces them; the default is last.CASE WHEN price > 100 ...sends a missing price toELSE, as in SQL.x IS DISTINCT FROM ytreats two missing values as equal.
import { Engine, sql } from 'vectrojs';
const engine = new Engine();
const t = engine.table({
region: ['EU', 'US', '', 'APAC', 'US', 'EU'],
price: [120, NaN, 45, NaN, 99, 15],
});
console.log(sql(`
SELECT COUNT(*) AS n, COUNT(price) AS priced, AVG(price) AS avg_price,
SUM(CASE WHEN price IS NULL THEN 1 ELSE 0 END) AS unpriced
FROM t`, { t }).toRows());
console.log(sql(`
SELECT COALESCE(region, 'unknown') AS region, COALESCE(price, 0) AS price
FROM t ORDER BY price DESC`, { t }).toRows());
await engine.close();
Three-valued logic in SQL
SQL follows the standard: a comparison with a missing value is unknown, and NOT unknown is still unknown, so the row is dropped. These all leave out rows whose price is missing, as PostgreSQL and DuckDB do:
NOT (price > 50)price <> 50price NOT IN (45, 15)price NOT BETWEEN 40 AND 50
The same holds for missing text: region <> 'EU', region NOT IN (...), region LIKE '%' and region NOT LIKE 'E%' all skip rows where region is '', and COUNT(region) and COUNT(DISTINCT region) don’t count it.
Table expressions follow JavaScript
The expressions you pass to filter, derive and friends compare the way JavaScript does: price != 50 and !(price > 50) are true where price is NaN. Add && !isMissing(price) when a missing value should not pass:
import { Engine } from 'vectrojs';
const engine = new Engine();
const t = engine.table({ price: [10, NaN, 60, 50] });
console.log(t.filter('price != 50').count()); // 3: NaN != 50 is true
console.log(t.filter('price != 50 && !isMissing(price)').count()); // 2
await engine.close();
Counting text works as in SQL: count(region), countDistinct(region) and approxDistinct(region) skip ''.