* [LIQ] Add new aggregate functions, aliases, and queryable aggregate registry * Extend with 13 new built-in aggregates: `product`, `string_agg`, `yaml_agg`, `json_agg`, `bit_and`, `bit_or`, `bit_xor`, `bool_and`, `bool_or`, `stddev_pop`, `stddev_samp`, `var_pop` and `var_samp`. * Introduce `aggregate.alias` API allowing users to define custom aliases for any aggregate. Standard aliases (`every`, `std`, `stddev` and `variance`) are now defined via this API rather than hardcoded. * Add `index.aggregates` queryable collection so users can discover all available aggregates directly from LIQ queries. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix config pass through query path so custom aggregates work Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Preserve `LuaTable`/`LuaFunction` values in aggregate config storage `config.set` uses `LuaNativeJSFunction` which calls `luaValueToJS` on all arguments. This converted the aggregate `LuaTable` to a plain JS object and wrapped `LuaFunction` callbacks in JS functions that also converted their returned values via `luaValueToJS`. The result was that state returned by initialize (a `LuaTable`) got converted to a plain JS object before being passed to `iterate`. Therefor Lua operations like `table.insert` on that were failing because they expected a `LuaTable` and not a plain JS array. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix formatting Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Improve aggregate functions descriptions, fix `sum` divergence Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Align `product` with `sum` Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: extract `alias` from `LuaTable` via `rawGet` in `aggregates()` registry Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Rename `alias` in `aggregates()` to `target` for clarity Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Add a null guard at the top of `jsToLuaValue` This preserves `null`/`undefined` as-is (both map to Lua nil) and prevents them from falling through to the `typeof` "object" branch. For this PR it means that null `target` in our `aggregates` entries will correctly show as empty/`nil` in query results rather than `{}`. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Documentation reflects recent changes Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: make `sum`/`product` return null on empty input; stop `LIQ_NULL` leaks * `sum(`) and` product(`) now return null when no rows match (matching Postgres semantics) instead of returning 0 and 1 respectively. * Query result columns that hold null are internally preserved using a `LIQ_NULL` sentinel so that column keys survive in `LuaTable` storage. This sentinel was leaking into Lua code as "userdata" through three read paths: * `luaIndexValue`: `rawGet` returned the sentinel directly to Lua when accessing table fields, * `rawget` (stdlib): the builtin `rawget` function exposed the sentinel without converting it back to `nil`, * `createAugmentedEnv`: string interpolation unpacked table values via `rawGet` into local variables, making the sentinel visible in template expressions like `${var}`. All three now convert `LIQ_NULL` to `nil` at the read boundary, keeping the sentinel internal to table storage where it belongs. * Update affected test expectations accordingly. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Remove duplicated LIQ_NULL hazard, add guard for all builtin aggregate `iterate`s Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: `array_agg` preserves NULL positions Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Add symbol guard to `json_agg` `JSON.stringify(Symbol(...))` in an array produces null by accident. That is a JS implementation detail we **MUST NOT** rely on. Explicit null push makes intent clear and avoids surprises if the `Symbol` representation ever changes. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Add symbol guard to `yaml_agg` (ditto) `js-yaml` has no knowledge of the `LIQ_NULL` symbol. Passing null makes it emit YAML null (or `~`), which is the correct YAML representation of a missing value and matches standard `json_agg`/`yaml_agg` NULL-inclusion semantics. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Add intra-aggregate ordering null guards Without this, `LIQ_NULL` sort keys would fall through to `valA < valB` which is always false for `Symbol`s which is breaking the `nulls first`/`nulls last` contract... Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Ditto, but for `order by` null comparisons Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Guard `luaTypeName`, `luaTypeOf` and `luaToString` against `LIQ_NULL` sentinel Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Guard presentation layer against `LIQ_NULL` sentinel leaking as visible text Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix: Evaluate extra args per-item in `executeAggregate`; add new aggregates Extra arguments (2nd, 3rd, etc.) to aggregate functions were evaluated against the outer query environment where the object variable is not bound. This caused multi-argument aggregates like `covar_samp(data.y, data.x)` to fail with nil reference errors. This commit addresses this by evaluating extra args per-item inside the iterate loop using the item environment so all arguments resolve correctly. We also add few common aggregates: - `covar_pop`, `covar_samp`, `corr`: population/sample covariance and correlation coefficient using online co-moment algorithm. - `quantile(value, q, method)`: general quantile with interpolation methods: lower, higher, nearest, midpoint and default linear. - `percentile_cont(value, q)`: continuous percentile (linear) - `percentile_disc(value, q)`: discrete percentile (lower) Note: `percentile_cont` and `percentile_disc` share the `quantile` implementation through `ctx.name` at initialize time. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Update docs Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Make the ordering for quantile aggregates explicit Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Update docs Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Improve docs Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
import type { SysCallMapping } from "../system.ts";
|
|
import type {
|
|
LuaCollectionQuery,
|
|
LuaQueryCollection,
|
|
} from "../../space_lua/query_collection.ts";
|
|
|
|
import {
|
|
type ObjectIndex,
|
|
ObjectValidationError,
|
|
} from "../../data/object_index.ts";
|
|
import type { ObjectValue } from "@silverbulletmd/silverbullet/type/index";
|
|
import type { Client } from "../../client.ts";
|
|
import type { LuaTable } from "../../space_lua/runtime.ts";
|
|
|
|
export function indexSyscalls(
|
|
objectIndex: ObjectIndex,
|
|
client: Client,
|
|
): SysCallMapping {
|
|
return {
|
|
"index.tag": (_ctx, tagName: string): LuaQueryCollection => {
|
|
return objectIndex.tag(tagName);
|
|
},
|
|
"index.aggregates": (_ctx): LuaQueryCollection => {
|
|
return objectIndex.aggregates();
|
|
},
|
|
"index.ensureFullIndex": (_ctx) => {
|
|
return objectIndex.ensureFullIndex(client.space);
|
|
},
|
|
"index.reindexSpace": () => {
|
|
return objectIndex.reindexSpace(client.space);
|
|
},
|
|
"index.indexObjects": (
|
|
_ctx,
|
|
page: string,
|
|
objects: ObjectValue[],
|
|
): Promise<void> => {
|
|
return objectIndex.indexObjects(page, objects);
|
|
},
|
|
"index.validateObjects": async (
|
|
_ctx,
|
|
page: string,
|
|
objects: ObjectValue[],
|
|
): Promise<{ error: string; object: ObjectValue } | null> => {
|
|
try {
|
|
await objectIndex.validateObjects(page, objects);
|
|
return null;
|
|
} catch (e: any) {
|
|
if (e instanceof ObjectValidationError) {
|
|
return {
|
|
error: e.message,
|
|
object: e.object,
|
|
};
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
},
|
|
"index.getObjectByRef": (
|
|
_ctx,
|
|
page: string,
|
|
tag: string,
|
|
ref: string,
|
|
): Promise<ObjectValue | undefined> => {
|
|
return objectIndex.getObjectByRef(ref, page, tag);
|
|
},
|
|
"index.queryLuaObjects": (
|
|
_ctx,
|
|
tag: string,
|
|
query: LuaCollectionQuery,
|
|
scopedVariables?: Record<string, any>,
|
|
): Promise<ObjectValue[]> => {
|
|
return objectIndex.queryLuaObjects(
|
|
client.clientSystem.spaceLuaEnv.env,
|
|
tag,
|
|
query,
|
|
scopedVariables,
|
|
);
|
|
},
|
|
|
|
"index.deleteObject": (
|
|
_ctx,
|
|
page: string,
|
|
tag: string,
|
|
ref: string,
|
|
): Promise<void> => {
|
|
return objectIndex.deleteObject(page, tag, ref);
|
|
},
|
|
"lua:index.defineTag": (_ctx, tagDef: LuaTable) => {
|
|
// Using 'lua:' prefix to _not_ convert tagDef to a JS version (but keep original LuaTable)
|
|
if (!tagDef.has("name")) {
|
|
throw new Error("A tag name is required");
|
|
}
|
|
const currentTag = client.config.get(["tags", tagDef.get("name")], null);
|
|
if (!currentTag) {
|
|
client.config.set(["tags", tagDef.get("name")], {
|
|
name: tagDef.get("name"),
|
|
});
|
|
}
|
|
client.config.set(
|
|
["tags", tagDef.get("name"), "metatable"],
|
|
tagDef.get("metatable"),
|
|
);
|
|
},
|
|
};
|
|
}
|