fix: convert named SQL params to positional for Authority server compatibility

Authority server expects params: SqlValue[] (positional array) but
ST-BME was sending params as a named map with :placeholders, causing
'invalid type: map, expected a sequence' deserialization errors.

Added convertNamedParamsToPositional() in AuthoritySqlHttpClient that
transforms :name placeholders to ? and extracts values in order before
sending to the server. This fixes the SQL probe failure that kept the
graph stuck in LOADING state.
This commit is contained in:
Youzini-afk
2026-04-28 23:54:12 +08:00
parent 801d580a4c
commit edaaca4bbd
2 changed files with 82 additions and 9 deletions

View File

@@ -342,6 +342,22 @@ function normalizeUpsertCountDelta(delta = {}) {
}; };
} }
export function convertNamedParamsToPositional(sql, params = {}) {
if (Array.isArray(params)) return { sql, params };
if (!params || typeof params !== "object") return { sql, params: [] };
const names = [];
const positionalSql = sql.replace(/(?<!:):(\w+)/g, (_, name) => {
names.push(name);
return "?";
});
if (!names.length) return { sql: positionalSql, params: [] };
const positionalParams = names.map((name) => {
if (!Object.prototype.hasOwnProperty.call(params, name)) return null;
return params[name];
});
return { sql: positionalSql, params: positionalParams };
}
export class AuthoritySqlHttpClient { export class AuthoritySqlHttpClient {
constructor(options = {}) { constructor(options = {}) {
this.http = new AuthorityHttpClient({ this.http = new AuthorityHttpClient({
@@ -352,18 +368,20 @@ export class AuthoritySqlHttpClient {
} }
async query(sql, params = {}) { async query(sql, params = {}) {
const positional = convertNamedParamsToPositional(String(sql || ""), params);
return await this._request(AUTHORITY_SQL_QUERY_ENDPOINT, { return await this._request(AUTHORITY_SQL_QUERY_ENDPOINT, {
database: this.database, database: this.database,
statement: String(sql || ""), statement: positional.sql,
params, params: positional.params,
}); });
} }
async execute(sql, params = {}) { async execute(sql, params = {}) {
const positional = convertNamedParamsToPositional(String(sql || ""), params);
return await this._request(AUTHORITY_SQL_EXEC_ENDPOINT, { return await this._request(AUTHORITY_SQL_EXEC_ENDPOINT, {
database: this.database, database: this.database,
statement: String(sql || ""), statement: positional.sql,
params, params: positional.params,
}); });
} }
@@ -372,10 +390,13 @@ export class AuthoritySqlHttpClient {
database: this.database, database: this.database,
statements: toArray(statements) statements: toArray(statements)
.filter((statement) => statement?.sql) .filter((statement) => statement?.sql)
.map((statement) => ({ .map((statement) => {
statement: String(statement.sql || ""), const positional = convertNamedParamsToPositional(String(statement.sql || ""), statement.params || {});
params: statement.params || {}, return {
})), statement: positional.sql,
params: positional.params,
};
}),
}); });
} }

View File

@@ -5,6 +5,7 @@ import {
AUTHORITY_GRAPH_STORE_MODE, AUTHORITY_GRAPH_STORE_MODE,
AuthorityGraphStore, AuthorityGraphStore,
AuthoritySqlHttpClient, AuthoritySqlHttpClient,
convertNamedParamsToPositional,
} from "../sync/authority-graph-store.js"; } from "../sync/authority-graph-store.js";
import { import {
BME_DB_SCHEMA_VERSION, BME_DB_SCHEMA_VERSION,
@@ -335,10 +336,61 @@ async function testHttpSqlClientBoundary() {
assert.deepEqual(JSON.parse(requests[1].init.body), { assert.deepEqual(JSON.parse(requests[1].init.body), {
database: "default", database: "default",
statement: "SELECT 1", statement: "SELECT 1",
params: { chatId: "chat" }, params: [],
}); });
} }
async function testConvertNamedParamsToPositional() {
// Named params with :placeholders get converted to positional ? with array
const r1 = convertNamedParamsToPositional(
"SELECT * FROM t WHERE chat_id = :chatId AND meta_key = :key",
{ chatId: "abc", key: "rev" },
);
assert.equal(r1.sql, "SELECT * FROM t WHERE chat_id = ? AND meta_key = ?");
assert.deepEqual(r1.params, ["abc", "rev"]);
// Duplicate named params produce multiple positional entries
const r2 = convertNamedParamsToPositional(
"INSERT INTO t (a, b) VALUES (:chatId, :chatId)",
{ chatId: "dup" },
);
assert.equal(r2.sql, "INSERT INTO t (a, b) VALUES (?, ?)");
assert.deepEqual(r2.params, ["dup", "dup"]);
// No placeholders → empty array
const r3 = convertNamedParamsToPositional("SELECT 1", { chatId: "x" });
assert.equal(r3.sql, "SELECT 1");
assert.deepEqual(r3.params, []);
// Already-array params pass through unchanged
const r4 = convertNamedParamsToPositional("SELECT ?", [42]);
assert.equal(r4.sql, "SELECT ?");
assert.deepEqual(r4.params, [42]);
// Empty/null params → empty array
const r5 = convertNamedParamsToPositional("SELECT 1", null);
assert.deepEqual(r5.params, []);
const r6 = convertNamedParamsToPositional("SELECT 1", undefined);
assert.deepEqual(r6.params, []);
// Missing param name → null in array
const r7 = convertNamedParamsToPositional(
"WHERE x = :x AND y = :y",
{ x: 1 },
);
assert.equal(r7.sql, "WHERE x = ? AND y = ?");
assert.deepEqual(r7.params, [1, null]);
// ::typecast is not treated as a named param
const r8 = convertNamedParamsToPositional(
"SELECT x::text FROM t WHERE id = :id",
{ id: 5 },
);
assert.equal(r8.sql, "SELECT x::text FROM t WHERE id = ?");
assert.deepEqual(r8.params, [5]);
}
await testConvertNamedParamsToPositional();
await testOpenSeedsAuthorityMeta(); await testOpenSeedsAuthorityMeta();
await testImportCommitAndExportSnapshot(); await testImportCommitAndExportSnapshot();
await testPruneAndClear(); await testPruneAndClear();