import type { LuaAggregateCallExpression, LuaBinaryExpression, LuaDynamicField, LuaExpression, LuaExpressionField, LuaFilteredCallExpression, LuaFunctionBody, LuaFunctionCallExpression, LuaParenthesizedExpression, LuaPropField, LuaUnaryExpression, } from "./ast.ts"; import { jsToLuaValue, luaCall, LuaEnv, LuaFunction, luaGet, luaKeys, LuaRuntimeError, LuaStackFrame, LuaTable, luaTruthy, type LuaValue, singleResult, } from "./runtime.ts"; import { isSqlNull, SLIQ_NULL } from "./sliq_null.ts"; import { evalExpression, luaOp } from "./eval.ts"; import { asyncMergeSort } from "./util.ts"; import type { DataStore } from "../data/datastore.ts"; import type { KvPrimitives } from "../data/kv_primitives.ts"; import type { QueryCollationConfig } from "../../plug-api/types/config.ts"; import type { KvKey } from "../../plug-api/types/datastore.ts"; import { executeAggregate, getAggregateSpec } from "./aggregates.ts"; import { Config } from "../config.ts"; // Implicit single group map key (aggregates without `group by`) const IMPLICIT_GROUP_KEY: unique symbol = Symbol("implicit-group"); // Build environment for post-`group by` clauses. Injects `key` and `group` // as top-level variables. Unpacks first group item fields and group-by key // fields as locals so that bare field access works after grouping. function buildGroupItemEnv( objectVariable: string | undefined, groupByNames: string[] | undefined, item: any, parentGlobals: LuaEnv, sf: LuaStackFrame, ): LuaEnv { const itemEnv = new LuaEnv(parentGlobals); itemEnv.setLocal("_", item); if (item instanceof LuaTable) { const keyVal = item.rawGet("key"); const groupVal = item.rawGet("group"); const firstItem = groupVal instanceof LuaTable ? groupVal.rawGet(1) : undefined; if (firstItem) { for (const k of luaKeys(firstItem)) { if (typeof k !== "string") continue; itemEnv.setLocal(k, luaGet(firstItem, k, sf.astCtx ?? null, sf)); } } if (objectVariable) { itemEnv.setLocal(objectVariable, firstItem ?? item); } if (keyVal !== undefined) { itemEnv.setLocal("key", keyVal); } if (groupVal !== undefined) { itemEnv.setLocal("group", groupVal); } // Unpack named fields from multi-key LuaTable keys if (keyVal instanceof LuaTable) { for (const k of luaKeys(keyVal)) { if (typeof k !== "string") continue; itemEnv.setLocal(k, luaGet(keyVal, k, sf.astCtx ?? null, sf)); } } // Bind all `group by` aliases/names to their key values. For // single key bind the name to the scalar `keyVal`. For multi-key // bind each name to the field from the key table. if (groupByNames && groupByNames.length > 0) { if (!(keyVal instanceof LuaTable)) { // Bind all names to scalar for (const gbn of groupByNames) { itemEnv.setLocal(gbn, keyVal); } } else { // Ensure every alias is bound even if `luaKeys` missed it for (const gbn of groupByNames) { const v = keyVal.rawGet(gbn); if (v !== undefined) { itemEnv.setLocal(gbn, v); } } } } } return itemEnv; } /** * Build an environment for evaluating per-item expressions in queries. * * When `objectVariable` is NOT set: item fields are unpacked as locals * and shadow any globals. The item is also bound to `_`. * * When `objectVariable` IS set: only the object variable is bound. * Item fields are NOT unpacked - the user opted into qualified access * (e.g. `p.name`) and bare names must resolve from the parent env. */ function buildItemEnvLocal( objectVariable: string | undefined, item: any, env: LuaEnv, sf: LuaStackFrame, ): LuaEnv { const itemEnv = new LuaEnv(env); if (objectVariable) { itemEnv.setLocal(objectVariable, item); } else { // Unpack item fields as locals so unqualified access works itemEnv.setLocal("_", item); if (item instanceof LuaTable) { for (const key of luaKeys(item)) { itemEnv.setLocal(key, luaGet(item, key, sf.astCtx ?? null, sf)); } } else if (typeof item === "object" && item !== null) { for (const key of luaKeys(item)) { itemEnv.setLocal(key, luaGet(item, key, sf.astCtx ?? null, sf)); } } } return itemEnv; } export { buildItemEnvLocal as buildItemEnv }; export type LuaOrderBy = { expr: LuaExpression; desc: boolean; nulls?: "first" | "last"; using?: string | LuaFunctionBody; }; export type LuaGroupByEntry = { expr: LuaExpression; alias?: string; }; /** * Represents a query for a collection */ export type LuaCollectionQuery = { objectVariable?: string; // The filter expression evaluated with Lua where?: LuaExpression; // The order by expression evaluated with Lua orderBy?: LuaOrderBy[]; // The select expression evaluated with Lua select?: LuaExpression; // The limit of the query limit?: number; // The offset of the query offset?: number; // Whether to return only distinct values distinct?: boolean; // The group by entries evaluated with Lua groupBy?: LuaGroupByEntry[]; // The having expression evaluated with Lua having?: LuaExpression; }; export interface LuaQueryCollection { query( query: LuaCollectionQuery, env: LuaEnv, sf: LuaStackFrame, config?: Config, ): Promise; } /** * Implements a query collection for a regular JavaScript array */ export class ArrayQueryCollection implements LuaQueryCollection { constructor(private readonly array: T[]) {} query( query: LuaCollectionQuery, env: LuaEnv, sf: LuaStackFrame, config?: Config, ): Promise { return applyQuery(this.array, query, env, sf, config); } } // Wrap any object, array, or LuaQueryCollection as a queryable collection export function toCollection(obj: any): LuaQueryCollection { if ( obj instanceof ArrayQueryCollection || obj instanceof DataStoreQueryCollection ) { return obj; } if (Array.isArray(obj)) { return new ArrayQueryCollection(obj); } return new ArrayQueryCollection([obj]); } function containsAggregate(expr: LuaExpression, config?: Config): boolean { switch (expr.type) { case "FilteredCall": { const fc = (expr as LuaFilteredCallExpression).call; if ( fc.prefix.type === "Variable" && getAggregateSpec(fc.prefix.name, config) ) { return true; } return ( containsAggregate(fc, config) || containsAggregate((expr as LuaFilteredCallExpression).filter, config) ); } case "AggregateCall": { const ac = expr as LuaAggregateCallExpression; const fc = ac.call; if ( fc.prefix.type === "Variable" && getAggregateSpec(fc.prefix.name, config) ) { return true; } return containsAggregate(fc, config); } case "FunctionCall": { const fc = expr as LuaFunctionCallExpression; if ( fc.prefix.type === "Variable" && getAggregateSpec(fc.prefix.name, config) ) { return true; } return fc.args.some((a) => containsAggregate(a, config)); } case "Binary": { const bin = expr as LuaBinaryExpression; return ( containsAggregate(bin.left, config) || containsAggregate(bin.right, config) ); } case "Unary": { const un = expr as LuaUnaryExpression; return containsAggregate(un.argument, config); } case "Parenthesized": { const p = expr as LuaParenthesizedExpression; return containsAggregate(p.expression, config); } case "TableConstructor": return expr.fields.some((f) => { switch (f.type) { case "PropField": return containsAggregate((f as LuaPropField).value, config); case "DynamicField": { const df = f as LuaDynamicField; return ( containsAggregate(df.key, config) || containsAggregate(df.value, config) ); } case "ExpressionField": return containsAggregate((f as LuaExpressionField).value, config); default: return false; } }); default: return false; } } // Wrap a value for select result tables so that the column key survives // in the `LuaTable` function selectVal(v: LuaValue): LuaValue { return v === null || v === undefined ? SLIQ_NULL : v; } /** * Evaluate an expression in aggregate-aware mode. * * When a FunctionCall matches a registered aggregate name, the aggregate * protocol is executed instead of normal call semantics. All other * expressions fall through to normal evalExpression. */ export async function evalExpressionWithAggregates( expr: LuaExpression, env: LuaEnv, sf: LuaStackFrame, groupItems: LuaTable, objectVariable: string | undefined, outerEnv: LuaEnv, config: Config, ): Promise { if (!containsAggregate(expr, config)) { return evalExpression(expr, env, sf); } const recurse = (e: LuaExpression) => evalExpressionWithAggregates( e, env, sf, groupItems, objectVariable, outerEnv, config, ); if (expr.type === "FilteredCall") { const filtered = expr as LuaFilteredCallExpression; const fc = filtered.call; if (fc.prefix.type === "Variable") { const name = fc.prefix.name; const spec = getAggregateSpec(name, config); if (spec) { const valueExpr = fc.args.length > 0 ? fc.args[0] : null; const extraArgExprs = fc.args.length > 1 ? fc.args.slice(1) : []; return executeAggregate( spec, groupItems, valueExpr, extraArgExprs, objectVariable, outerEnv, sf, evalExpression, config, filtered.filter, fc.orderBy, ); } } return evalExpression(expr, env, sf); } if (expr.type === "FunctionCall") { const fc = expr as LuaFunctionCallExpression; if (fc.prefix.type === "Variable") { const name = fc.prefix.name; const spec = getAggregateSpec(name, config); if (spec) { const valueExpr = fc.args.length > 0 ? fc.args[0] : null; const extraArgExprs = fc.args.length > 1 ? fc.args.slice(1) : []; return executeAggregate( spec, groupItems, valueExpr, extraArgExprs, objectVariable, outerEnv, sf, evalExpression, config, undefined, fc.orderBy, ); } } } if (expr.type === "TableConstructor") { const table = new LuaTable(); let nextArrayIndex = 1; for (const field of expr.fields) { switch (field.type) { case "PropField": { const pf = field as LuaPropField; const value = await recurse(pf.value); void table.set(pf.key, selectVal(value), sf); break; } case "DynamicField": { const df = field as LuaDynamicField; const key = await evalExpression(df.key, env, sf); const value = await recurse(df.value); void table.set(key, selectVal(value), sf); break; } case "ExpressionField": { const ef = field as LuaExpressionField; const value = await recurse(ef.value); table.rawSetArrayIndex(nextArrayIndex, selectVal(value)); nextArrayIndex++; break; } } } return table; } if (expr.type === "Binary") { const bin = expr as LuaBinaryExpression; if (bin.operator === "and") { const left = singleResult(await recurse(bin.left)); if (!luaTruthy(left)) return left; return singleResult(await recurse(bin.right)); } if (bin.operator === "or") { const left = singleResult(await recurse(bin.left)); if (luaTruthy(left)) return left; return singleResult(await recurse(bin.right)); } const left = singleResult(await recurse(bin.left)); const right = singleResult(await recurse(bin.right)); return luaOp(bin.operator, left, right, undefined, undefined, expr.ctx, sf); } if (expr.type === "Unary") { const un = expr as LuaUnaryExpression; const arg = singleResult(await recurse(un.argument)); switch (un.operator) { case "-": return typeof arg === "number" ? -arg : luaOp("-", 0, arg, undefined, undefined, expr.ctx, sf); case "not": return !luaTruthy(arg); case "#": return evalExpression(expr, env, sf); case "~": if (typeof arg === "number") return ~arg; throw new Error("attempt to perform bitwise operation on a non-number"); default: return evalExpression(expr, env, sf); } } if (expr.type === "Parenthesized") { const paren = expr as LuaParenthesizedExpression; return singleResult(await recurse(paren.expression)); } return evalExpression(expr, env, sf); } /** * Collect the canonical key order from an array of select results. * Finds the first LuaTable that has the maximum number of string keys * and returns its keys in insertion order. This represents the * "complete" column set in the order the user wrote in `select { ... }`. */ function collectCanonicalKeyOrder(results: any[]): string[] | null { let best: string[] | null = null; for (const item of results) { if (item instanceof LuaTable) { const keys = luaKeys(item).filter( (k): k is string => typeof k === "string", ); if (!best || keys.length > best.length) { best = keys; } } } return best; } function normalizeSelectResults(results: any[]): any[] { if (results.length === 0) return results; const canonicalKeys = collectCanonicalKeyOrder(results); if (!canonicalKeys || canonicalKeys.length === 0) return results; for (let i = 0; i < results.length; i++) { const item = results[i]; if (!(item instanceof LuaTable)) continue; let needsRebuild = false; for (const k of canonicalKeys) { const v = item.rawGet(k); if (v === undefined || v === null) { needsRebuild = true; break; } } if (!needsRebuild) continue; const rebuilt = new LuaTable(); for (const k of canonicalKeys) { const v = item.rawGet(k); void rebuilt.rawSet(k, v === undefined || v === null ? SLIQ_NULL : v); } for (const k of luaKeys(item)) { if (typeof k !== "string") { void rebuilt.rawSet(k, item.rawGet(k)); } } results[i] = rebuilt; } return results; } function resolveUsing( using: string | LuaFunctionBody | undefined, env: LuaEnv, _sf: LuaStackFrame, ): LuaValue | null { if (using === undefined) return null; if (typeof using === "string") { return env.get(using) ?? null; } return new LuaFunction(using, env); } // Compare values using a custom comparator with SWO violation detection async function usingCompare( luaCmp: LuaValue, aVal: any, bVal: any, originalA: number, originalB: number, desc: boolean, sf: LuaStackFrame, violated: boolean[], keyIdx: number, ): Promise { const res = luaTruthy( singleResult(await luaCall(luaCmp, [aVal, bVal], sf.astCtx ?? {}, sf)), ); const reverseRes = luaTruthy( singleResult(await luaCall(luaCmp, [bVal, aVal], sf.astCtx ?? {}, sf)), ); // both true means SWO violation if (res && reverseRes) { violated[keyIdx] = true; return originalA < originalB ? -1 : 1; } if (res) return desc ? 1 : -1; if (reverseRes) return desc ? -1 : 1; return 0; } /** * Pre-compute all sort keys for each result item (Schwartzian transform) * and evaluate each `order by` expression exactly once per item */ async function precomputeSortKeys( results: any[], orderBy: LuaOrderBy[], mkEnv: ( ov: string | undefined, item: any, e: LuaEnv, s: LuaStackFrame, ) => LuaEnv, objectVariable: string | undefined, env: LuaEnv, sf: LuaStackFrame, grouped: boolean, selectResults: any[] | undefined, config: Config, ): Promise { const allKeys: any[][] = new Array(results.length); for (let i = 0; i < results.length; i++) { const item = results[i]; const itemEnv = mkEnv(objectVariable, item, env, sf); if (selectResults) { const row = selectResults[i]; if (row) { for (const k of luaKeys(row)) { const v = luaGet(row, k, sf.astCtx ?? null, sf); itemEnv.setLocal(k, isSqlNull(v) ? null : v); } } } const keys: any[] = new Array(orderBy.length); for (let j = 0; j < orderBy.length; j++) { if (grouped) { const groupTable = (item as LuaTable).rawGet("group"); keys[j] = await evalExpressionWithAggregates( orderBy[j].expr, itemEnv, sf, groupTable, objectVariable, env, config, ); } else { keys[j] = await evalExpression(orderBy[j].expr, itemEnv, sf); } } allKeys[i] = keys; } return allKeys; } /** * Compare two items by their pre-computed sort keys without Lua * expressions evaluation. */ async function sortKeyCompare( a: { val: any; idx: number }, b: { val: any; idx: number }, orderBy: LuaOrderBy[], aKeys: any[], bKeys: any[], collation: QueryCollationConfig | undefined, collator: Intl.Collator, resolvedUsing: (LuaValue | null)[], violated: boolean[], sf: LuaStackFrame, ): Promise { for (let idx = 0; idx < orderBy.length; idx++) { const { desc, nulls } = orderBy[idx]; const aVal = aKeys[idx]; const bVal = bKeys[idx]; // Handle nulls positioning const aIsNull = aVal === null || aVal === undefined || isSqlNull(aVal); const bIsNull = bVal === null || bVal === undefined || isSqlNull(bVal); if (aIsNull || bIsNull) { if (aIsNull && bIsNull) continue; // Default: nulls last for asc, nulls first for desc const nullsLast = nulls === "last" || (nulls === undefined && !desc); if (aIsNull) return nullsLast ? 1 : -1; return nullsLast ? -1 : 1; } const usingFn = resolvedUsing[idx]; if (usingFn) { const cmp = await usingCompare( usingFn, aVal, bVal, a.idx, b.idx, desc, sf, violated, idx, ); if (cmp !== 0) return cmp; } else if ( collation?.enabled && typeof aVal === "string" && typeof bVal === "string" ) { const order = collator.compare(aVal, bVal); if (order !== 0) { return desc ? -order : order; } } else if (aVal < bVal) { return desc ? 1 : -1; } else if (aVal > bVal) { return desc ? -1 : 1; } } return 0; } // Build a select-result table from a non-aggregate select expression async function evalSelectExpression( selectExpr: LuaExpression, itemEnv: LuaEnv, sf: LuaStackFrame, ): Promise { const result = await evalExpression(selectExpr, itemEnv, sf); if (!(result instanceof LuaTable)) return result; for (const k of luaKeys(result)) { const v = result.rawGet(k); if (v === null || v === undefined) { void result.rawSet(k, SLIQ_NULL); } } return result; } export async function applyQuery( results: any[], query: LuaCollectionQuery, env: LuaEnv, sf: LuaStackFrame, config: Config = new Config(), ): Promise { results = results.slice(); if (query.where) { const filteredResults = []; for (const value of results) { const itemEnv = buildItemEnvLocal(query.objectVariable, value, env, sf); if (await evalExpression(query.where, itemEnv, sf)) { filteredResults.push(value); } } results = filteredResults; } // Implicit single group if ( !query.groupBy && ((query.select && containsAggregate(query.select, config)) || (query.having && containsAggregate(query.having, config))) ) { query = { ...query, groupBy: [] }; } const grouped = !!query.groupBy; // Collect group-by key names for unpacking into the post-group environment. let groupByNames: string[] | undefined; if (query.groupBy) { // Extract expressions and names from `group by` entries const groupByEntries = query.groupBy; // Derive canonical name (explicit alias first, or from expression) groupByNames = groupByEntries .map((entry) => { if (entry.alias) return entry.alias; if (entry.expr.type === "Variable") return entry.expr.name; if (entry.expr.type === "PropertyAccess") return entry.expr.property; return undefined as unknown as string; }) .filter(Boolean); const groups = new Map(); for (const item of results) { const itemEnv = buildItemEnvLocal(query.objectVariable, item, env, sf); const keyParts: any[] = []; const keyRecord: Record = {}; for (let ei = 0; ei < groupByEntries.length; ei++) { const entry = groupByEntries[ei]; const v = await evalExpression(entry.expr, itemEnv, sf); keyParts.push(v); // Use alias if provided, or from expression const name = entry.alias ?? (entry.expr.type === "Variable" ? entry.expr.name : undefined) ?? (entry.expr.type === "PropertyAccess" ? entry.expr.property : undefined); if (name) { keyRecord[name] = v; } } // Implicit single group uses a symbol key const compositeKey: string | symbol = keyParts.length === 0 ? IMPLICIT_GROUP_KEY : keyParts.length === 1 ? generateKey(keyParts[0]) : JSON.stringify(keyParts.map(generateKey)); let entry = groups.get(compositeKey); if (!entry) { let keyVal: any; if (keyParts.length === 0) { // Implicit single group — key is `nil` keyVal = null; } else if (keyParts.length === 1) { keyVal = keyParts[0]; } else { const kt = new LuaTable(); // Always populate array indices from keyParts for (let i = 0; i < keyParts.length; i++) { kt.rawSetArrayIndex(i + 1, keyParts[i]); } // Additionally set named fields for Variable/PropertyAccess exprs for (const name in keyRecord) { void kt.rawSet(name, keyRecord[name]); } keyVal = kt; } entry = { key: keyVal, items: [], }; groups.set(compositeKey, entry); } entry.items.push(item); } results = []; for (const { key, items } of groups.values()) { const groupTable = new LuaTable(); for (let i = 0; i < items.length; i++) { const item = items[i]; groupTable.rawSetArrayIndex( i + 1, item instanceof LuaTable || typeof item !== "object" || item === null ? item : jsToLuaValue(item), ); } const row = new LuaTable(); void row.rawSet("key", key); void row.rawSet("group", groupTable); results.push(row); } } if (query.having) { const filteredResults = []; for (const value of results) { let condResult; if (grouped) { const itemEnv = buildGroupItemEnv( query.objectVariable, groupByNames, value, env, sf, ); const groupTable = (value as LuaTable).rawGet("group"); condResult = await evalExpressionWithAggregates( query.having, itemEnv, sf, groupTable, query.objectVariable, env, config, ); } else { const itemEnv = buildItemEnvLocal(query.objectVariable, value, env, sf); condResult = await evalExpression(query.having, itemEnv, sf); } if (condResult) { filteredResults.push(value); } } results = filteredResults; } const mkEnv = grouped ? (ov: string | undefined, item: any, e: LuaEnv, s: LuaStackFrame) => buildGroupItemEnv(ov, groupByNames, item, e, s) : buildItemEnvLocal; let selectResults: any[] | undefined; // Pre-compute select for grouped + ordered queries if (grouped && query.select && query.orderBy) { const selectExpr = query.select; selectResults = []; for (const item of results) { const itemEnv = mkEnv(query.objectVariable, item, env, sf); const groupTable = (item as LuaTable).rawGet("group"); const selected = await evalExpressionWithAggregates( selectExpr, itemEnv, sf, groupTable, query.objectVariable, env, config, ); selectResults.push(selected); } selectResults = normalizeSelectResults(selectResults); } if (query.orderBy) { const collation = config.get("queryCollation", {}); const collator = Intl.Collator(collation?.locale, collation?.options); const resolvedUsing: (LuaValue | null)[] = []; const violated: boolean[] = []; for (const ob of query.orderBy) { resolvedUsing.push(resolveUsing(ob.using, env, sf)); violated.push(false); } // Decorate: pre-compute all sort keys once (Schwartzian transform) const sortKeys = await precomputeSortKeys( results, query.orderBy, mkEnv, query.objectVariable, env, sf, grouped, selectResults, config, ); // Tag each result with its original index for stable sorting const tagged = results.map((val, idx) => ({ val, idx })); // Sort: compare cached keys only, no Lua eval in comparator await asyncMergeSort(tagged, (a, b) => sortKeyCompare( a, b, query.orderBy!, sortKeys[a.idx], sortKeys[b.idx], collation, collator, resolvedUsing, violated, sf, ), ); // Check for SWO violations in comparators for (let i = 0; i < violated.length; i++) { if (violated[i]) { throw new LuaRuntimeError( `order by #${i + 1}: 'using' comparator violates strict weak ordering`, sf, ); } } if (selectResults) { const reorderedResults: any[] = new Array(tagged.length); const reorderedSelect: any[] = new Array(tagged.length); for (let i = 0; i < tagged.length; i++) { reorderedResults[i] = tagged[i].val; reorderedSelect[i] = selectResults[tagged[i].idx]; } results = reorderedResults; selectResults = reorderedSelect; } else { results = tagged.map((t) => t.val); } } if (query.select) { const selectExpr = query.select; if (selectResults) { results = selectResults; } else { const newResult = []; for (const item of results) { const itemEnv = mkEnv(query.objectVariable, item, env, sf); if (grouped) { const groupTable = (item as LuaTable).rawGet("group"); newResult.push( await evalExpressionWithAggregates( selectExpr, itemEnv, sf, groupTable, query.objectVariable, env, config, ), ); } else { newResult.push(await evalSelectExpression(query.select, itemEnv, sf)); } } results = newResult; } results = normalizeSelectResults(results); } if (query.distinct) { const seen = new Set(); const distinctResult = []; for (const item of results) { const key = generateKey(item); if (!seen.has(key)) { seen.add(key); distinctResult.push(item); } } results = distinctResult; } if (query.limit !== undefined && query.offset !== undefined) { results = results.slice(query.offset, query.offset + query.limit); } else if (query.limit !== undefined) { results = results.slice(0, query.limit); } else if (query.offset !== undefined) { results = results.slice(query.offset); } return results; } export async function queryLua( kv: KvPrimitives, prefix: KvKey, query: LuaCollectionQuery, env: LuaEnv, sf: LuaStackFrame = LuaStackFrame.lostFrame, enricher?: (key: KvKey, item: any) => any, config?: Config, ): Promise { const results: T[] = []; for await (let { key, value } of kv.query({ prefix })) { if (enricher) { value = enricher(key, value); // Enrichers may return undefined to filter a row out if (value === undefined) continue; } results.push(value); } return applyQuery(results, query, env, sf, config); } function generateKey(value: any) { if (isSqlNull(value)) { return "__SQL_NULL__"; } if (value instanceof LuaTable) { return JSON.stringify(luaTableToJSWithNulls(value)); } return typeof value === "object" && value !== null ? JSON.stringify(value) : value; } function luaTableToJSWithNulls( table: LuaTable, sf = LuaStackFrame.lostFrame, ): any { if (table.length > 0) { const arr: any[] = []; for (let i = 1; i <= table.length; i++) { const v = table.rawGet(i); arr.push( isSqlNull(v) ? "__SQL_NULL__" : v instanceof LuaTable ? luaTableToJSWithNulls(v, sf) : v, ); } return arr; } const obj: Record = {}; for (const key of luaKeys(table)) { const v = table.rawGet(key); obj[key] = isSqlNull(v) ? "__SQL_NULL__" : v instanceof LuaTable ? luaTableToJSWithNulls(v, sf) : v; } return obj; } export class DataStoreQueryCollection implements LuaQueryCollection { constructor( private readonly dataStore: DataStore, readonly prefix: string[], ) {} query( query: LuaCollectionQuery, env: LuaEnv, sf: LuaStackFrame, config?: Config, ): Promise { return queryLua( this.dataStore.kv, this.prefix, query, env, sf, undefined, config, ); } }