Lua query fixes

pull/1212/head
Zef Hemel 2025-01-16 15:33:18 +01:00
parent 396c9fc60a
commit 55f5a465c8
13 changed files with 269 additions and 73 deletions

View File

@ -583,7 +583,12 @@ export async function evalStatement(
.map((lval) => evalLValue(lval, env, sf))); .map((lval) => evalLValue(lval, env, sf)));
for (let i = 0; i < lvalues.length; i++) { for (let i = 0; i < lvalues.length; i++) {
luaSet(lvalues[i].env, lvalues[i].key, values[i], sf.withCtx(s.ctx)); await luaSet(
lvalues[i].env,
lvalues[i].key,
values[i],
sf.withCtx(s.ctx),
);
} }
break; break;

View File

@ -142,7 +142,9 @@ assert_equal(t.foo, "Key not found: foo")
t = setmetatable( t = setmetatable(
{}, { {}, {
__newindex = function(table, key, value) __newindex = function(table, key, value)
print("Raw set", key, value)
rawset(table, key, "Value: " .. value) rawset(table, key, "Value: " .. value)
print("Raw set done")
end end
} }
) )
@ -150,8 +152,8 @@ t = setmetatable(
t.name = "John" t.name = "John"
-- rawset ignores the metamethod -- rawset ignores the metamethod
rawset(t, "age", 100) rawset(t, "age", 100)
assert(t.name == "Value: John") assert_equal(t.name, "Value: John")
assert(t.age == 100) assert_equal(t.age, 100)
-- Test some of the operator metamethods -- Test some of the operator metamethods
t = setmetatable( t = setmetatable(

View File

@ -1,5 +1,8 @@
import { parseExpressionString } from "$common/space_lua/parse.ts"; import { parseExpressionString } from "$common/space_lua/parse.ts";
import { ArrayQueryCollection } from "./query_collection.ts"; import {
ArrayQueryCollection,
findAllQueryVariables,
} from "./query_collection.ts";
import { import {
LuaEnv, LuaEnv,
LuaNativeJSFunction, LuaNativeJSFunction,
@ -137,3 +140,13 @@ Deno.test("ArrayQueryCollection", async () => {
assertEquals(result9[2], "Jane Doe"); assertEquals(result9[2], "Jane Doe");
assertEquals(result9[3], "Bob Johnson"); assertEquals(result9[3], "Bob Johnson");
}); });
Deno.test("findAllQueryVariables", () => {
const query = {
where: parseExpressionString("p.x >= 2 and b.x >= 2"),
select: parseExpressionString("p.x + b.x"),
orderBy: [{ expr: parseExpressionString("q.x"), desc: false }],
};
const variables = findAllQueryVariables(query);
assertEquals(variables, ["p", "b", "q"]);
});

View File

@ -51,6 +51,73 @@ export type LuaCollectionQuery = {
offset?: number; offset?: number;
}; };
export function findAllQueryVariables(query: LuaCollectionQuery): string[] {
const variables = new Set<string>();
// Helper to traverse an expression and collect variables
function findVariables(expr: LuaExpression) {
if (!expr) return;
switch (expr.type) {
case "Variable":
variables.add(expr.name);
break;
case "Binary":
findVariables(expr.left);
findVariables(expr.right);
break;
case "Unary":
findVariables(expr.argument);
break;
case "TableAccess":
findVariables(expr.object);
findVariables(expr.key);
break;
case "FunctionCall":
findVariables(expr.prefix);
expr.args.forEach(findVariables);
break;
case "TableConstructor":
expr.fields.forEach((field) => {
switch (field.type) {
case "DynamicField":
findVariables(field.key);
findVariables(field.value);
break;
case "PropField":
findVariables(field.value);
break;
case "ExpressionField":
findVariables(field.value);
break;
}
});
break;
case "PropertyAccess":
findVariables(expr.object);
break;
case "Parenthesized":
findVariables(expr.expression);
break;
}
}
// Check all parts of the query that can contain expressions
if (query.where) {
findVariables(query.where);
}
if (query.orderBy) {
query.orderBy.forEach((ob) => findVariables(ob.expr));
}
if (query.select) {
findVariables(query.select);
}
return Array.from(variables);
}
export interface LuaQueryCollection { export interface LuaQueryCollection {
query( query(
query: LuaCollectionQuery, query: LuaCollectionQuery,

View File

@ -48,4 +48,6 @@ Deno.test("Test Lua Rutime", async () => {
assertEquals(await luaToString(new Promise((resolve) => resolve(1))), "1"); assertEquals(await luaToString(new Promise((resolve) => resolve(1))), "1");
assertEquals(await luaToString({ a: 1 }), "{a = 1}"); assertEquals(await luaToString({ a: 1 }), "{a = 1}");
assertEquals(await luaToString([{ a: 1 }]), "{{a = 1}}"); assertEquals(await luaToString([{ a: 1 }]), "{{a = 1}}");
// Ensure simple cases are not returning promises
assertEquals(luaToString(10), "10");
}); });

View File

@ -328,7 +328,13 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
return false; return false;
} }
rawSet(key: LuaValue, value: LuaValue) { rawSet(key: LuaValue, value: LuaValue): void | Promise<void> {
if (key instanceof Promise) {
return key.then((key) => this.rawSet(key, value));
}
if (value instanceof Promise) {
return value.then(() => this.rawSet(key, value));
}
if (typeof key === "string") { if (typeof key === "string") {
this.stringKeys[key] = value; this.stringKeys[key] = value;
} else if (Number.isInteger(key) && key >= 1) { } else if (Number.isInteger(key) && key >= 1) {
@ -360,7 +366,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
} }
// Just set the value // Just set the value
this.rawSet(key, value); return this.rawSet(key, value);
} }
rawGet(key: LuaValue): LuaValue | null { rawGet(key: LuaValue): LuaValue | null {
@ -480,7 +486,12 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue }; export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
export function luaSet(obj: any, key: any, value: any, sf: LuaStackFrame) { export async function luaSet(
obj: any,
key: any,
value: any,
sf: LuaStackFrame,
): Promise<void> {
if (!obj) { if (!obj) {
throw new LuaRuntimeError( throw new LuaRuntimeError(
`Not a settable object: nil`, `Not a settable object: nil`,
@ -489,7 +500,7 @@ export function luaSet(obj: any, key: any, value: any, sf: LuaStackFrame) {
} }
if (obj instanceof LuaTable || obj instanceof LuaEnv) { if (obj instanceof LuaTable || obj instanceof LuaEnv) {
obj.set(key, value, sf); await obj.set(key, value, sf);
} else { } else {
obj[key] = value; obj[key] = value;
} }
@ -678,18 +689,19 @@ export function luaTruthy(value: any): boolean {
return true; return true;
} }
export async function luaToString(value: any): Promise<string> { export function luaToString(value: any): string | Promise<string> {
if (value === null || value === undefined) { if (value === null || value === undefined) {
return "nil"; return "nil";
} }
if (value instanceof Promise) { if (value instanceof Promise) {
return luaToString(await value); return value.then(luaToString);
} }
if (value.toStringAsync) { if (value.toStringAsync) {
return value.toStringAsync(); return value.toStringAsync();
} }
// Handle plain JavaScript objects in a Lua-like format // Handle plain JavaScript objects in a Lua-like format
if (typeof value === "object") { if (typeof value === "object") {
return (async () => {
let result = "{"; let result = "{";
let first = true; let first = true;
@ -726,6 +738,7 @@ export async function luaToString(value: any): Promise<string> {
} }
result += "}"; result += "}";
return result; return result;
})();
} }
return String(value); return String(value);
} }

View File

@ -11,6 +11,7 @@ import {
luaToString, luaToString,
luaTypeOf, luaTypeOf,
type LuaValue, type LuaValue,
luaValueToJS,
} from "$common/space_lua/runtime.ts"; } from "$common/space_lua/runtime.ts";
import { stringApi } from "$common/space_lua/stdlib/string.ts"; import { stringApi } from "$common/space_lua/stdlib/string.ts";
import { tableApi } from "$common/space_lua/stdlib/table.ts"; import { tableApi } from "$common/space_lua/stdlib/table.ts";
@ -20,11 +21,13 @@ import {
interpolateLuaString, interpolateLuaString,
spaceLuaApi, spaceLuaApi,
} from "$common/space_lua/stdlib/space_lua.ts"; } from "$common/space_lua/stdlib/space_lua.ts";
import type { import {
LuaCollectionQuery, findAllQueryVariables,
LuaQueryCollection, type LuaCollectionQuery,
type LuaQueryCollection,
} from "$common/space_lua/query_collection.ts"; } from "$common/space_lua/query_collection.ts";
import { templateApi } from "$common/space_lua/stdlib/template.ts"; import { templateApi } from "$common/space_lua/stdlib/template.ts";
import { json } from "@codemirror/legacy-modes/X-ZUBjb2RlbWlycm9yL2xhbmd1YWdl/mode/javascript.d.ts";
const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => { const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => {
console.log("[Lua]", ...(await Promise.all(args.map(luaToString)))); console.log("[Lua]", ...(await Promise.all(args.map(luaToString))));
@ -135,8 +138,7 @@ const setmetatableFunction = new LuaBuiltinFunction(
const rawsetFunction = new LuaBuiltinFunction( const rawsetFunction = new LuaBuiltinFunction(
(_sf, table: LuaTable, key: LuaValue, value: LuaValue) => { (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => {
table.rawSet(key, value); return table.rawSet(key, value);
return table;
}, },
); );
@ -152,7 +154,28 @@ const tagFunction = new LuaBuiltinFunction(
throw new LuaRuntimeError("Global not found", sf); throw new LuaRuntimeError("Global not found", sf);
} }
return { return {
query: async (query: LuaCollectionQuery): Promise<any[]> => { query: async (query: LuaCollectionQuery, env: LuaEnv): Promise<any[]> => {
const localVars = findAllQueryVariables(query).filter((v) =>
!global.has(v) && v !== "_"
);
const scopedVariables: Record<string, any> = {};
for (const v of localVars) {
try {
const jsonValue = await luaValueToJS(env.get(v));
// Ensure this is JSON serializable
JSON.stringify(jsonValue);
scopedVariables[v] = jsonValue;
} catch (e: any) {
console.error(
"Failed to JSON serialize variable",
v,
);
throw new LuaRuntimeError(
`Failed to JSON serialize variable ${v} in query`,
sf,
);
}
}
return (await global.get("datastore").get("query_lua").call( return (await global.get("datastore").get("query_lua").call(
sf, sf,
[ [
@ -160,6 +183,7 @@ const tagFunction = new LuaBuiltinFunction(
tagName, tagName,
], ],
query, query,
scopedVariables,
)).toJSArray(); )).toJSArray();
}, },
}; };

View File

@ -6,6 +6,43 @@ import {
luaToString, luaToString,
} from "$common/space_lua/runtime.ts"; } from "$common/space_lua/runtime.ts";
function createLuaMatcher(pattern: string, global = false) {
const jsPattern = pattern
.replace(/%(.)/g, (_, char) => {
switch (char) {
case ".":
return "[.]";
case "%":
return "%";
case "d":
return "\\d";
case "D":
return "\\D";
case "s":
return "\\s";
case "S":
return "\\S";
case "w":
return "\\w";
case "a":
return "[A-Za-z]";
case "l":
return "[a-z]";
case "u":
return "[A-Z]";
case "p":
return "[\\p{P}]";
default:
return char;
}
});
const regex = new RegExp(jsPattern, global ? "g" : undefined);
return (s: string) => {
return regex.exec(s);
};
}
export const stringApi = new LuaTable({ export const stringApi = new LuaTable({
byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => { byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => {
i = i ?? 1; i = i ?? 1;
@ -34,27 +71,9 @@ export const stringApi = new LuaTable({
}, },
), ),
gmatch: new LuaBuiltinFunction((_sf, s: string, pattern: string) => { gmatch: new LuaBuiltinFunction((_sf, s: string, pattern: string) => {
const jsPattern = pattern const matcher = createLuaMatcher(pattern, true);
.replace(/%(.)/g, (_, char) => {
switch (char) {
case ".":
return "[.]";
case "%":
return "%";
case "d":
return "\\d";
case "s":
return "\\s";
case "w":
return "\\w";
default:
return char;
}
});
const regex = new RegExp(jsPattern, "g");
return () => { return () => {
const result = regex.exec(s); const result = matcher(s);
if (!result) { if (!result) {
return; return;
} }
@ -155,11 +174,12 @@ export const stringApi = new LuaTable({
match: new LuaBuiltinFunction( match: new LuaBuiltinFunction(
(_sf, s: string, pattern: string, init?: number) => { (_sf, s: string, pattern: string, init?: number) => {
init = init ?? 1; init = init ?? 1;
const result = s.slice(init - 1).match(pattern); const result = createLuaMatcher(pattern)(s.slice(init - 1));
if (!result) { if (!result) {
return new LuaMultiRes([]); return new LuaMultiRes([]);
} }
return new LuaMultiRes(result.slice(1)); const captures = result.slice(1);
return new LuaMultiRes(captures.length > 0 ? captures : [result[0]]);
}, },
), ),
rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => { rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => {

View File

@ -106,4 +106,38 @@ assert_equal(string.startswith("hello world", "world"), false)
assert_equal(string.endswith("hello world", "world"), true) assert_equal(string.endswith("hello world", "world"), true)
assert_equal(string.endswith("hello world", "hello"), false) assert_equal(string.endswith("hello world", "hello"), false)
-- Extended string.match tests
-- Basic pattern matching
assert_equal(string.match("hello", "h"), "h")
assert_equal(string.match("hello", "hello"), "hello")
-- Test with no matches
assert_equal(string.match("hello", "x"), nil)
-- Test with captures
local m1, m2 = string.match("hello", "(h)(ello)")
assert_equal(m1, "h")
assert_equal(m2, "ello")
-- Test with init position
local init_match = string.match("hello world", "(world)", 7)
assert_equal(init_match, "world")
-- Test init position with no match
assert_equal(string.match("hello world", "hello", 7), nil)
-- Test pattern characters
assert_equal(string.match("123", "%d+"), "123")
assert_equal(string.match("abc123", "%a+"), "abc")
assert_equal(string.match(" abc", "%s+"), " ")
-- Test multiple captures
local day, month, year = string.match("2024-03-14", "(%d+)-(%d+)-(%d+)")
assert_equal(day, "2024")
assert_equal(month, "03")
assert_equal(year, "14")
-- Test optional captures
local word = string.match("The quick brown fox", "%s*(%w+)%s*")
assert_equal(word, "The")

View File

@ -81,6 +81,9 @@ export const tableApi = new LuaTable({
*/ */
includes: new LuaBuiltinFunction( includes: new LuaBuiltinFunction(
(sf, tbl: LuaTable | Record<string, any>, value: LuaValue) => { (sf, tbl: LuaTable | Record<string, any>, value: LuaValue) => {
if (!tbl) {
return false;
}
if (tbl instanceof LuaTable) { if (tbl instanceof LuaTable) {
// Iterate over the table // Iterate over the table
for (const key of tbl.keys()) { for (const key of tbl.keys()) {

View File

@ -6,7 +6,12 @@ import type { CommonSystem } from "$common/common_system.ts";
import type { KV, KvKey, KvQuery } from "../../../plug-api/types.ts"; import type { KV, KvKey, KvQuery } from "../../../plug-api/types.ts";
import type { DataStore } from "../../data/datastore.ts"; import type { DataStore } from "../../data/datastore.ts";
import type { SysCallMapping } from "../system.ts"; import type { SysCallMapping } from "../system.ts";
import { LuaStackFrame, luaValueToJS } from "$common/space_lua/runtime.ts"; import {
jsToLuaValue,
LuaEnv,
LuaStackFrame,
luaValueToJS,
} from "$common/space_lua/runtime.ts";
/** /**
* Exposes the datastore API to plugs, but scoping everything to a prefix based on the plug's name * Exposes the datastore API to plugs, but scoping everything to a prefix based on the plug's name
@ -41,11 +46,16 @@ export function dataStoreReadSyscalls(
_ctx, _ctx,
prefix: string[], prefix: string[],
query: LuaCollectionQuery, query: LuaCollectionQuery,
scopeVariables: Record<string, any> = {},
): Promise<KV[]> => { ): Promise<KV[]> => {
const dsQueryCollection = new DataStoreQueryCollection(ds, prefix); const dsQueryCollection = new DataStoreQueryCollection(ds, prefix);
const env = new LuaEnv(commonSystem.spaceLuaEnv.env);
for (const [key, value] of Object.entries(scopeVariables)) {
env.set(key, jsToLuaValue(value));
}
return (await dsQueryCollection.query( return (await dsQueryCollection.query(
query, query,
commonSystem.spaceLuaEnv.env, env,
LuaStackFrame.lostFrame, LuaStackFrame.lostFrame,
)).map((item) => luaValueToJS(item)); )).map((item) => luaValueToJS(item));
}, },

View File

@ -74,8 +74,9 @@ export function query(
export function queryLua( export function queryLua(
prefix: string[], prefix: string[],
query: LuaCollectionQuery, query: LuaCollectionQuery,
scopeVariables: Record<string, any>,
): Promise<KV[]> { ): Promise<KV[]> {
return syscall("datastore.queryLua", prefix, query); return syscall("datastore.queryLua", prefix, query, scopeVariables);
} }
/** /**

View File

@ -64,7 +64,7 @@ export class ServerSystem extends CommonSystem {
} }
// Always needs to be invoked right after construction // Always needs to be invoked right after construction
async init(awaitIndex = false) { async init(awaitIndex = false, performIndex = true) {
this.system = new System( this.system = new System(
"server", "server",
{ {
@ -219,11 +219,13 @@ export class ServerSystem extends CommonSystem {
space.updatePageList().catch(console.error); space.updatePageList().catch(console.error);
if (performIndex) {
// Ensure a valid index // Ensure a valid index
const indexPromise = ensureSpaceIndex(this.ds, this.system); const indexPromise = ensureSpaceIndex(this.ds, this.system);
if (awaitIndex) { if (awaitIndex) {
await indexPromise; await indexPromise;
} }
}
await this.eventHook.dispatchEvent("system:ready"); await this.eventHook.dispatchEvent("system:ready");
} }