@palbase/backend 22.1.0 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/palbase-backend.cjs +439 -18
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +4 -4
- package/dist/{chunk-SQC5EIWY.js → chunk-FSGSB42K.js} +11 -6
- package/dist/chunk-FSGSB42K.js.map +1 -0
- package/dist/{chunk-I3ON7MYF.js → chunk-HAF67F2H.js} +73 -16
- package/dist/chunk-HAF67F2H.js.map +1 -0
- package/dist/{chunk-QMVK4X3V.js → chunk-OMRTHM4X.js} +98 -98
- package/dist/chunk-OMRTHM4X.js.map +1 -0
- package/dist/{chunk-74XDEF5J.js → chunk-REZU6UKT.js} +432 -17
- package/dist/chunk-REZU6UKT.js.map +1 -0
- package/dist/{chunk-YL4C5NRY.js → chunk-Y5HXVUMP.js} +2 -2
- package/dist/{chunk-N32VDWKH.js → chunk-ZC6Q2BRD.js} +4 -59
- package/dist/chunk-ZC6Q2BRD.js.map +1 -0
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +3 -21
- package/dist/db/env.d.ts +3 -21
- package/dist/db/index.cjs +72 -15
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +3 -2
- package/dist/db/index.d.ts +3 -2
- package/dist/db/index.js +1 -1
- package/dist/{endpoint-BVT6jcVW.d.cts → endpoint-BavvbW4P.d.ts} +77 -7
- package/dist/{endpoint-BVT6jcVW.d.ts → endpoint-i8TTCohk.d.cts} +77 -7
- package/dist/engine/index.cjs +439 -18
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +6 -5
- package/dist/engine/index.d.ts +6 -5
- package/dist/engine/index.js +3 -3
- package/dist/{index-vwHoS0l2.d.ts → index-B3jmmItD.d.ts} +111 -5
- package/dist/{index-CCZqzych.d.ts → index-B7YBEG5w.d.ts} +96 -71
- package/dist/{index-BqCiHao8.d.cts → index-Bmvx1EvJ.d.cts} +111 -5
- package/dist/{index-BS1gW4nV.d.cts → index-E7OscPJT.d.cts} +96 -71
- package/dist/index.cjs +285 -1099
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -1134
- package/dist/index.d.ts +89 -1134
- package/dist/index.js +107 -902
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +3 -57
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +3 -2
- package/dist/openapi/index.d.ts +3 -2
- package/dist/openapi/index.js +5 -22
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-Bsuf-orT.d.ts → registry-C3H2uPeZ.d.cts} +13 -169
- package/dist/{registry-BWttGlaT.d.cts → registry-DY3d9l1k.d.ts} +13 -169
- package/dist/{purchases/keys.cjs → stack.cjs} +4 -4
- package/dist/stack.cjs.map +1 -0
- package/dist/stack.d.cts +76 -0
- package/dist/stack.d.ts +76 -0
- package/dist/stack.js +1 -0
- package/docs/README.md +7 -6
- package/docs/llms-full.txt +7 -260
- package/docs/llms.txt +0 -2
- package/package.json +6 -6
- package/template/package.json +1 -1
- package/dist/chunk-74XDEF5J.js.map +0 -1
- package/dist/chunk-I3ON7MYF.js.map +0 -1
- package/dist/chunk-N32VDWKH.js.map +0 -1
- package/dist/chunk-QMVK4X3V.js.map +0 -1
- package/dist/chunk-SQC5EIWY.js.map +0 -1
- package/dist/purchases/keys.cjs.map +0 -1
- package/dist/purchases/keys.d.cts +0 -42
- package/dist/purchases/keys.d.ts +0 -42
- package/dist/purchases/keys.js +0 -1
- package/docs/config.md +0 -147
- package/docs/resources.md +0 -97
- package/template/config/secrets.ts +0 -24
- /package/dist/{chunk-YL4C5NRY.js.map → chunk-Y5HXVUMP.js.map} +0 -0
- /package/dist/{purchases/keys.js.map → stack.js.map} +0 -0
package/dist/engine/index.cjs
CHANGED
|
@@ -460,7 +460,10 @@ function makeTablesAccessor(ops) {
|
|
|
460
460
|
delete: (id) => ops().delete(name, id),
|
|
461
461
|
findById: (id) => ops().findById(name, id),
|
|
462
462
|
findMany: (query) => ops().findMany(name, query),
|
|
463
|
-
search: (params) => ops().search(name, params)
|
|
463
|
+
search: (params) => ops().search(name, params),
|
|
464
|
+
similar: (id, params) => ops().similar(name, id, params),
|
|
465
|
+
recommend: (params) => ops().recommend(name, params),
|
|
466
|
+
supersede: (id, row) => ops().supersede(name, id, row)
|
|
464
467
|
};
|
|
465
468
|
}
|
|
466
469
|
}
|
|
@@ -469,6 +472,7 @@ function makeTablesAccessor(ops) {
|
|
|
469
472
|
}
|
|
470
473
|
var rawDatabase = makeServiceProxy("Database");
|
|
471
474
|
function makeTypedSurface(raw) {
|
|
475
|
+
const reco = raw;
|
|
472
476
|
const ops = {
|
|
473
477
|
query: (sql, params) => raw.query(sql, params),
|
|
474
478
|
insert: (table, data) => raw.insert(table, data),
|
|
@@ -476,10 +480,13 @@ function makeTypedSurface(raw) {
|
|
|
476
480
|
delete: (table, id) => raw.delete(table, id),
|
|
477
481
|
findById: (table, id) => raw.findById(table, id),
|
|
478
482
|
findMany: (table, query) => raw.findMany(table, query),
|
|
479
|
-
search: (table, params) => raw.search(table, params)
|
|
483
|
+
search: (table, params) => raw.search(table, params),
|
|
484
|
+
similar: (table, id, params) => reco.similar(table, id, params),
|
|
485
|
+
recommend: (table, params) => reco.recommend(table, params),
|
|
486
|
+
supersede: (table, id, row) => raw.supersede(table, id, row)
|
|
480
487
|
};
|
|
481
488
|
return Object.assign(ops, {
|
|
482
|
-
tables: makeTablesAccessor(() =>
|
|
489
|
+
tables: makeTablesAccessor(() => reco),
|
|
483
490
|
transaction(fn) {
|
|
484
491
|
const builder = new TxPlanBuilder();
|
|
485
492
|
return runTxPlan(raw, makeTxTablesAccessor(builder), builder, fn);
|
|
@@ -542,7 +549,6 @@ var Cache = makeServiceProxy("Cache");
|
|
|
542
549
|
var Secrets = makeServiceProxy("Secrets");
|
|
543
550
|
var Log = makeServiceProxy("Log");
|
|
544
551
|
var Notifications = makeServiceProxy("Notifications");
|
|
545
|
-
var Purchases = makeServiceProxy("Purchases");
|
|
546
552
|
var rawFlags = makeServiceProxy("Flags");
|
|
547
553
|
var Flags = Object.assign(
|
|
548
554
|
{
|
|
@@ -1066,7 +1072,42 @@ function searchConfigFor(table) {
|
|
|
1066
1072
|
);
|
|
1067
1073
|
}
|
|
1068
1074
|
const search = t.search;
|
|
1069
|
-
const
|
|
1075
|
+
const synonyms = search?.synonyms !== void 0 && Object.keys(search.synonyms).length > 0 ? { synonyms: search.synonyms } : {};
|
|
1076
|
+
const validity = search?.validity === true ? { validity: true } : {};
|
|
1077
|
+
if (search?.from !== void 0 && search.model !== void 0) {
|
|
1078
|
+
const m = search.model;
|
|
1079
|
+
const embed = {
|
|
1080
|
+
model: m.model,
|
|
1081
|
+
apiKeyName: m.apiKeyName ?? "OPENAI_API_KEY",
|
|
1082
|
+
...m.baseURL !== void 0 ? { baseURL: m.baseURL } : {},
|
|
1083
|
+
...m.dimensions !== void 0 ? { dimensions: m.dimensions } : {}
|
|
1084
|
+
};
|
|
1085
|
+
const metric = search.metric ?? "cosine";
|
|
1086
|
+
const ftsOn = search.text !== false;
|
|
1087
|
+
const ftsColsNew = ftsOn ? Array.isArray(search.text) && search.text.length > 0 ? search.text : search.from : [];
|
|
1088
|
+
if (vectorCols.length > 0) {
|
|
1089
|
+
return {
|
|
1090
|
+
pk,
|
|
1091
|
+
cols,
|
|
1092
|
+
colSet: new Set(cols),
|
|
1093
|
+
ftsCols: ftsColsNew,
|
|
1094
|
+
legs: [{ column: vectorCols[0], metric, embed }],
|
|
1095
|
+
...synonyms,
|
|
1096
|
+
...validity
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
pk,
|
|
1101
|
+
cols,
|
|
1102
|
+
colSet: new Set(cols),
|
|
1103
|
+
ftsCols: ftsColsNew,
|
|
1104
|
+
legs: [],
|
|
1105
|
+
chunk: { table: `${table}__palbase_chunks`, metric, embed, fts: ftsOn },
|
|
1106
|
+
...synonyms,
|
|
1107
|
+
...validity
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const ftsCols = (Array.isArray(search?.text) ? search.text : void 0) ?? [];
|
|
1070
1111
|
const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [search.vector];
|
|
1071
1112
|
let legs;
|
|
1072
1113
|
if (rawLegs.length > 0) {
|
|
@@ -1092,7 +1133,7 @@ function searchConfigFor(table) {
|
|
|
1092
1133
|
legs = vectorCols.map((column) => ({ column, metric: "cosine" }));
|
|
1093
1134
|
}
|
|
1094
1135
|
if (ftsCols.length === 0 && legs.length === 0) return null;
|
|
1095
|
-
return { pk, cols, colSet: new Set(cols), ftsCols, legs };
|
|
1136
|
+
return { pk, cols, colSet: new Set(cols), ftsCols, legs, ...synonyms, ...validity };
|
|
1096
1137
|
}
|
|
1097
1138
|
function pickLeg(table, legs, using) {
|
|
1098
1139
|
if (legs.length === 0) return null;
|
|
@@ -1108,7 +1149,74 @@ function pickLeg(table, legs, using) {
|
|
|
1108
1149
|
if (legs.length === 1) return legs[0];
|
|
1109
1150
|
throw new Error(`search(${table}): birden \xE7ok vekt\xF6r kolu var \u2014 'using' ile se\xE7in (FR-013) \u2014 salt metin ar\u0131yorsan mode:"text" kullan`);
|
|
1110
1151
|
}
|
|
1152
|
+
var HALF_LIFE_UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
1153
|
+
function parseHalfLife(s) {
|
|
1154
|
+
const m = /^(\d+)(s|m|h|d)$/.exec(s);
|
|
1155
|
+
const sec = m === null ? 0 : Number(m[1]) * HALF_LIFE_UNIT_SECONDS[m[2]];
|
|
1156
|
+
if (!Number.isFinite(sec) || sec <= 0) {
|
|
1157
|
+
throw new Error(`recency.halfLife "${s}" \xE7\xF6z\xFCmlenemedi \u2014 beklenen: pozitif <say\u0131>+(s|m|h|d), \xF6rn. "30d" (FR-004)`);
|
|
1158
|
+
}
|
|
1159
|
+
return sec;
|
|
1160
|
+
}
|
|
1111
1161
|
var WHERE_OPS = { gt: ">", gte: ">=", lt: "<", lte: "<=", neq: "<>" };
|
|
1162
|
+
function ftsExprOf(ftsCols) {
|
|
1163
|
+
return [...ftsCols].sort().map((c) => `coalesce(t.${quoteIdent(c)},'')`).join(" || ' ' || ");
|
|
1164
|
+
}
|
|
1165
|
+
function expandSynonyms(query, map) {
|
|
1166
|
+
const lower = {};
|
|
1167
|
+
for (const [word, alts] of Object.entries(map)) lower[word.toLowerCase()] = alts;
|
|
1168
|
+
return query.split(/("[^"]*")/).map((seg) => {
|
|
1169
|
+
if (seg.startsWith('"')) return seg;
|
|
1170
|
+
return seg.split(/(\s+)/).map((tok) => {
|
|
1171
|
+
if (tok === "" || /^\s+$/.test(tok)) return tok;
|
|
1172
|
+
const alts = lower[tok.toLowerCase()];
|
|
1173
|
+
return alts !== void 0 && alts.length > 0 ? `(${tok} OR ${alts.join(" OR ")})` : tok;
|
|
1174
|
+
}).join("");
|
|
1175
|
+
}).join("");
|
|
1176
|
+
}
|
|
1177
|
+
var searchLogger = null;
|
|
1178
|
+
function logNoResults(table, query, mode, n) {
|
|
1179
|
+
if (n === 0) {
|
|
1180
|
+
searchLogger?.("palbase.search.no_results", {
|
|
1181
|
+
table,
|
|
1182
|
+
query: (query ?? "").slice(0, 200),
|
|
1183
|
+
mode: mode ?? "hybrid"
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function fetchFacets(live, table, colSet, facets, where, extraWhere = () => "") {
|
|
1188
|
+
const bind = [];
|
|
1189
|
+
const add = (v) => {
|
|
1190
|
+
bind.push(v);
|
|
1191
|
+
return `$${bind.length}`;
|
|
1192
|
+
};
|
|
1193
|
+
const whereSql = compileWhere(table, colSet, where, add) + extraWhere(add);
|
|
1194
|
+
const parts = facets.map(
|
|
1195
|
+
(col) => `(SELECT '${col.replace(/'/g, "''")}' AS f, t.${quoteIdent(col)}::text AS v, count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql} GROUP BY 2 ORDER BY 3 DESC LIMIT 20)`
|
|
1196
|
+
);
|
|
1197
|
+
const rows = await live.unsafe(parts.join(" UNION ALL "), bind);
|
|
1198
|
+
const out = {};
|
|
1199
|
+
for (const col of facets) out[col] = [];
|
|
1200
|
+
for (const r of rows) {
|
|
1201
|
+
if (typeof r.f === "string" && out[r.f] !== void 0) {
|
|
1202
|
+
out[r.f].push({ value: r.v ?? null, count: Number(r.n) });
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return out;
|
|
1206
|
+
}
|
|
1207
|
+
function validitySql(v, add) {
|
|
1208
|
+
if (v === "all") return "";
|
|
1209
|
+
if (v !== void 0) {
|
|
1210
|
+
const p = add(v.asOf);
|
|
1211
|
+
return ` AND t."valid_from" <= ${p} AND (t."valid_to" IS NULL OR t."valid_to" > ${p})`;
|
|
1212
|
+
}
|
|
1213
|
+
return ` AND t."valid_to" IS NULL`;
|
|
1214
|
+
}
|
|
1215
|
+
function excludeSql(pk, ids, add) {
|
|
1216
|
+
if (ids.length === 0) return "";
|
|
1217
|
+
if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
|
|
1218
|
+
return ` AND t.${quoteIdent(pk)} <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
1219
|
+
}
|
|
1112
1220
|
function compileWhere(table, colSet, where, add) {
|
|
1113
1221
|
const parts = [];
|
|
1114
1222
|
for (const [col, cond] of Object.entries(where)) {
|
|
@@ -1138,6 +1246,15 @@ function compileWhere(table, colSet, where, add) {
|
|
|
1138
1246
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1139
1247
|
}
|
|
1140
1248
|
var cachedVectorSchema = null;
|
|
1249
|
+
var cachedTrgmSchema = null;
|
|
1250
|
+
async function trgmSchemaOf(live) {
|
|
1251
|
+
if (cachedTrgmSchema !== null) return cachedTrgmSchema;
|
|
1252
|
+
const rows = await live.unsafe(
|
|
1253
|
+
"select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'"
|
|
1254
|
+
);
|
|
1255
|
+
cachedTrgmSchema = rows?.[0]?.nspname ?? "public";
|
|
1256
|
+
return cachedTrgmSchema;
|
|
1257
|
+
}
|
|
1141
1258
|
async function vectorSchemaWithGuc(runner) {
|
|
1142
1259
|
if (cachedVectorSchema !== null) {
|
|
1143
1260
|
await runner.unsafe(
|
|
@@ -1253,12 +1370,128 @@ function createOps(tx) {
|
|
|
1253
1370
|
if (!cfg) {
|
|
1254
1371
|
throw new Error(`search(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
|
|
1255
1372
|
}
|
|
1373
|
+
const excl = params.__excludeIds ?? [];
|
|
1374
|
+
if (params.facets !== void 0) {
|
|
1375
|
+
for (const col of params.facets) {
|
|
1376
|
+
if (!cfg.colSet.has(col)) {
|
|
1377
|
+
throw new Error(`search(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (params.validity !== void 0 && cfg.validity !== true) {
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
`search(${table}): validity parametresi verildi ama tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
const validityFor = (adder) => cfg.validity === true ? validitySql(params.validity, adder) : "";
|
|
1256
1387
|
const rawLimit = params.limit ?? 20;
|
|
1257
1388
|
if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
|
|
1258
1389
|
throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
|
|
1259
1390
|
}
|
|
1260
1391
|
const limit = Math.min(Math.max(1, Math.trunc(rawLimit)), 100);
|
|
1261
1392
|
const pool = Math.max(limit * 3, 30);
|
|
1393
|
+
if (params.minScore !== void 0 && (typeof params.minScore !== "number" || !Number.isFinite(params.minScore))) {
|
|
1394
|
+
throw new Error(`search(${table}): minScore sonlu bir say\u0131 olmal\u0131, ${String(params.minScore)} verildi (FR-001)`);
|
|
1395
|
+
}
|
|
1396
|
+
let recencyMul = "";
|
|
1397
|
+
if (params.recency !== void 0) {
|
|
1398
|
+
const { field, halfLife } = params.recency;
|
|
1399
|
+
if (!cfg.colSet.has(field)) {
|
|
1400
|
+
throw new Error(`search(${table}): recency.field "${field}" tabloda yok (FR-004)`);
|
|
1401
|
+
}
|
|
1402
|
+
recencyMul = ` * exp(-ln(2) * extract(epoch from (now() - t.${quoteIdent(field)})) / ${parseHalfLife(halfLife)})`;
|
|
1403
|
+
}
|
|
1404
|
+
let boostMul = "";
|
|
1405
|
+
if (params.boost !== void 0) {
|
|
1406
|
+
const { field, weight } = params.boost;
|
|
1407
|
+
if (!cfg.colSet.has(field)) {
|
|
1408
|
+
throw new Error(`search(${table}): boost.field "${field}" tabloda yok (FR-030)`);
|
|
1409
|
+
}
|
|
1410
|
+
if (typeof weight !== "number" || !Number.isFinite(weight)) {
|
|
1411
|
+
throw new Error(`search(${table}): boost.weight sonlu bir say\u0131 olmal\u0131, ${String(weight)} verildi (FR-030)`);
|
|
1412
|
+
}
|
|
1413
|
+
const g = `greatest(t.${quoteIdent(field)},0)`;
|
|
1414
|
+
boostMul = ` * (1 + ${weight} * ${g}/(1+${g}))`;
|
|
1415
|
+
}
|
|
1416
|
+
const scoreMul = boostMul + recencyMul;
|
|
1417
|
+
if (cfg.chunk !== void 0) {
|
|
1418
|
+
const ck = cfg.chunk;
|
|
1419
|
+
const pidQ = quoteIdent(`parent_${cfg.pk}`);
|
|
1420
|
+
const bpr = Math.min(Math.max(1, Math.trunc(params.blocksPerRow ?? 3)), 10);
|
|
1421
|
+
const wantTextC = params.mode !== "vector" && ck.fts && typeof params.query === "string" && params.query !== "";
|
|
1422
|
+
let qvC = Array.isArray(params.vector) ? params.vector : null;
|
|
1423
|
+
if (qvC === null && ck.embed !== void 0 && params.mode !== "text" && typeof params.query === "string" && params.query !== "") {
|
|
1424
|
+
try {
|
|
1425
|
+
qvC = await embedQuery(ck.embed, params.query);
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
if (!wantTextC) throw e;
|
|
1428
|
+
qvC = null;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (!wantTextC && qvC === null) {
|
|
1432
|
+
throw new Error(
|
|
1433
|
+
`search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
const bindC = [];
|
|
1437
|
+
const addC = (v) => {
|
|
1438
|
+
bindC.push(v);
|
|
1439
|
+
return `$${bindC.length}`;
|
|
1440
|
+
};
|
|
1441
|
+
const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1442
|
+
const liveC = await at();
|
|
1443
|
+
const K2 = 50;
|
|
1444
|
+
const cpool = Math.max(limit * 9, 90);
|
|
1445
|
+
let semC = "";
|
|
1446
|
+
let kwC = "";
|
|
1447
|
+
if (qvC !== null) {
|
|
1448
|
+
const sch = await vectorSchemaWithGuc(liveC);
|
|
1449
|
+
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1450
|
+
const vp = addC(toVectorLiteral(qvC));
|
|
1451
|
+
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)) AS r FROM ${quoteIdent(ck.table)} c WHERE c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
|
|
1452
|
+
}
|
|
1453
|
+
let qpC = "";
|
|
1454
|
+
let qpCIdx = -1;
|
|
1455
|
+
if (wantTextC) {
|
|
1456
|
+
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
1457
|
+
qpC = addC(qTextC);
|
|
1458
|
+
qpCIdx = bindC.length - 1;
|
|
1459
|
+
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c WHERE c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
|
|
1460
|
+
}
|
|
1461
|
+
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
1462
|
+
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
1463
|
+
const scoreC = scoreMul === "" ? "g._score" : `(g._score)${scoreMul}`;
|
|
1464
|
+
const assembleC = (kwIn, withKwn) => {
|
|
1465
|
+
const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
|
|
1466
|
+
let fusedSrc;
|
|
1467
|
+
if (semC !== "" && kwIn !== "") {
|
|
1468
|
+
fusedSrc = `WITH sem AS (${semC}), kw AS (${kwIn}), fused AS (SELECT COALESCE(sem.pid, kw.pid) AS pid, COALESCE(sem.chunk_seq, kw.chunk_seq) AS chunk_seq, (COALESCE(1.0/(${K2} + sem.r), 0) + COALESCE(1.0/(${K2} + kw.r), 0))::float8 AS cscore FROM sem FULL OUTER JOIN kw ON sem.pid = kw.pid AND sem.chunk_seq = kw.chunk_seq)`;
|
|
1469
|
+
} else if (semC !== "") {
|
|
1470
|
+
fusedSrc = `WITH sem AS (${semC}), fused AS (SELECT sem.pid, sem.chunk_seq, (1.0/(${K2} + sem.r))::float8 AS cscore FROM sem)`;
|
|
1471
|
+
} else {
|
|
1472
|
+
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
1473
|
+
}
|
|
1474
|
+
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid WHERE true${whereC}`;
|
|
1475
|
+
return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
1476
|
+
};
|
|
1477
|
+
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
1478
|
+
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
1479
|
+
const tsch = await trgmSchemaOf(liveC);
|
|
1480
|
+
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c WHERE ${qpC} OPERATOR(${quoteIdent(tsch)}.<%) coalesce(c.content,'') ORDER BY r LIMIT ${cpool}`;
|
|
1481
|
+
const retryBindC = bindC.slice();
|
|
1482
|
+
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
1483
|
+
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
1484
|
+
}
|
|
1485
|
+
for (const r of rowsC) delete r._kwn;
|
|
1486
|
+
const outC = asTableRows(table, rowsC).map((r) => ({ ...r, blocks: r.blocks ?? [] }));
|
|
1487
|
+
if (params.facets !== void 0 && params.facets.length > 0) {
|
|
1488
|
+
Object.assign(outC, {
|
|
1489
|
+
_facets: await fetchFacets(liveC, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
logNoResults(table, params.query, params.mode, outC.length);
|
|
1493
|
+
return outC;
|
|
1494
|
+
}
|
|
1262
1495
|
const wantText = params.mode !== "vector" && cfg.ftsCols.length > 0 && typeof params.query === "string" && params.query !== "";
|
|
1263
1496
|
const anyEmbed = cfg.legs.some((l) => l.embed !== void 0);
|
|
1264
1497
|
const vectorAsked = params.mode !== "text" && (Array.isArray(params.vector) || params.using !== void 0 || params.mode === "vector" || anyEmbed && typeof params.query === "string" && params.query !== "");
|
|
@@ -1283,7 +1516,8 @@ function createOps(tx) {
|
|
|
1283
1516
|
bind.push(v);
|
|
1284
1517
|
return `$${bind.length}`;
|
|
1285
1518
|
};
|
|
1286
|
-
const
|
|
1519
|
+
const userWhere = compileWhere(table, cfg.colSet, params.where ?? {}, add);
|
|
1520
|
+
const whereSql = userWhere + excludeSql(cfg.pk, excl, add) + validityFor(add);
|
|
1287
1521
|
const live = await at();
|
|
1288
1522
|
const K = 50;
|
|
1289
1523
|
let semSql = "";
|
|
@@ -1292,7 +1526,7 @@ function createOps(tx) {
|
|
|
1292
1526
|
const sch = await vectorSchemaWithGuc(live);
|
|
1293
1527
|
const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
|
|
1294
1528
|
let exactOrder = "";
|
|
1295
|
-
if (
|
|
1529
|
+
if (userWhere !== "") {
|
|
1296
1530
|
const probeRows = await live.unsafe(
|
|
1297
1531
|
`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(leg.column)} IS NOT NULL${whereSql} LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
|
|
1298
1532
|
bind.slice()
|
|
@@ -1305,21 +1539,208 @@ function createOps(tx) {
|
|
|
1305
1539
|
const vp = add(toVectorLiteral(qv));
|
|
1306
1540
|
semSql = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY (t.${quoteIdent(leg.column)} OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)${exactOrder}) AS r FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(leg.column)} IS NOT NULL${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
1307
1541
|
}
|
|
1542
|
+
let qpRef = "";
|
|
1543
|
+
let qpIdx = -1;
|
|
1308
1544
|
if (wantText) {
|
|
1309
|
-
const
|
|
1545
|
+
const qText = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
1546
|
+
const qp = add(qText);
|
|
1547
|
+
qpIdx = bind.length - 1;
|
|
1548
|
+
qpRef = qp;
|
|
1310
1549
|
kwSql = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(t.palbase_fts, websearch_to_tsquery('simple', ${qp})) DESC) AS r FROM ${quoteIdent(table)} t WHERE t.palbase_fts @@ websearch_to_tsquery('simple', ${qp})${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
1311
1550
|
}
|
|
1312
1551
|
const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1552
|
+
const msP = params.minScore === void 0 ? "" : add(params.minScore);
|
|
1553
|
+
const hlCol = params.highlight === true && wantText ? `, ts_headline('simple', ${ftsExprOf(cfg.ftsCols)}, websearch_to_tsquery('simple', ${qpRef})) AS _highlight` : "";
|
|
1554
|
+
const assemble = (kwIn, withKwn) => {
|
|
1555
|
+
const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
|
|
1556
|
+
let inner;
|
|
1557
|
+
let orderScore;
|
|
1558
|
+
if (semSql !== "" && kwIn !== "") {
|
|
1559
|
+
const score = scoreMul === "" ? "fused._score" : `(fused._score)${scoreMul}`;
|
|
1560
|
+
inner = `WITH sem AS (${semSql}), kw AS (${kwIn}), fused AS (SELECT COALESCE(sem.id, kw.id) AS id, (COALESCE(1.0/(${K} + sem.r), 0) + COALESCE(1.0/(${K} + kw.r), 0))::float8 AS _score FROM sem FULL OUTER JOIN kw ON sem.id = kw.id) SELECT ${colList}, ${score} AS _score${hlCol}${kwnCol} FROM fused JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = fused.id`;
|
|
1561
|
+
orderScore = scoreMul === "" ? "fused._score" : "_score";
|
|
1562
|
+
} else {
|
|
1563
|
+
const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwIn})`;
|
|
1564
|
+
const alias = semSql !== "" ? "sem" : "kw";
|
|
1565
|
+
const base = `(1.0/(${K} + ${alias}.r))::float8`;
|
|
1566
|
+
const score = scoreMul === "" ? base : `(${base})${scoreMul}`;
|
|
1567
|
+
inner = `WITH ${single} SELECT ${colList}, ${score} AS _score${hlCol}${semSql !== "" ? "" : kwnCol} FROM ${alias} JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id`;
|
|
1568
|
+
orderScore = "_score";
|
|
1569
|
+
}
|
|
1570
|
+
return msP === "" ? `${inner} ORDER BY ${orderScore} DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msP} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
1571
|
+
};
|
|
1572
|
+
let rows = await live.unsafe(assemble(kwSql, kwSql !== ""), bind);
|
|
1573
|
+
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
1574
|
+
const tsch = await trgmSchemaOf(live);
|
|
1575
|
+
const expr = ftsExprOf(cfg.ftsCols);
|
|
1576
|
+
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${qpRef} OPERATOR(${quoteIdent(tsch)}.<%) ${expr}${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
1577
|
+
const retryBind = bind.slice();
|
|
1578
|
+
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
1579
|
+
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
1580
|
+
}
|
|
1581
|
+
for (const r of rows) delete r._kwn;
|
|
1582
|
+
const out = asTableRows(table, rows).map((r) => ({ ...r, blocks: [] }));
|
|
1583
|
+
if (params.facets !== void 0 && params.facets.length > 0) {
|
|
1584
|
+
Object.assign(out, {
|
|
1585
|
+
_facets: await fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
logNoResults(table, params.query, params.mode, out.length);
|
|
1589
|
+
return out;
|
|
1590
|
+
},
|
|
1591
|
+
/**
|
|
1592
|
+
* similar (T018, FR-022): "bu satıra benzeyenler". Hedef vektör DB'den
|
|
1593
|
+
* okunur — satır-modu satırın kendi kolonu, chunk-modu parent chunk'larının
|
|
1594
|
+
* şema-nitelikli avg'ı — ve ana arama search'ün vector yoluyla koşar;
|
|
1595
|
+
* kaynak satır sonuçtan düşer. İKİ tur BİLİNÇLİ (plan T018): target-CTE'li
|
|
1596
|
+
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
1597
|
+
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
1598
|
+
*/
|
|
1599
|
+
async similar(table, id, opts = {}) {
|
|
1600
|
+
const cfg = searchConfigFor(table);
|
|
1601
|
+
if (!cfg) {
|
|
1602
|
+
throw new Error(`similar(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
|
|
1603
|
+
}
|
|
1604
|
+
const live = await at();
|
|
1605
|
+
let v;
|
|
1606
|
+
if (cfg.chunk !== void 0) {
|
|
1607
|
+
const sch = await vectorSchemaWithGuc(live);
|
|
1608
|
+
const rows = await live.unsafe(
|
|
1609
|
+
`SELECT ${quoteIdent(sch)}.avg(c.embedding) AS v FROM ${quoteIdent(cfg.chunk.table)} c WHERE c.${quoteIdent(`parent_${cfg.pk}`)} = $1 AND c.embedding IS NOT NULL`,
|
|
1610
|
+
[id]
|
|
1611
|
+
);
|
|
1612
|
+
v = rows?.[0]?.v;
|
|
1316
1613
|
} else {
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1614
|
+
const leg = pickLeg(table, cfg.legs, opts.using);
|
|
1615
|
+
if (leg === null) {
|
|
1616
|
+
throw new Error(`similar(${table}): tabloda vekt\xF6r kolonu yok \u2014 benzerlik semantik vekt\xF6r ister (FR-022)`);
|
|
1617
|
+
}
|
|
1618
|
+
const rows = await live.unsafe(
|
|
1619
|
+
`SELECT t.${quoteIdent(leg.column)} AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} = $1`,
|
|
1620
|
+
[id]
|
|
1621
|
+
);
|
|
1622
|
+
v = rows?.[0]?.v;
|
|
1623
|
+
}
|
|
1624
|
+
if (typeof v !== "string") {
|
|
1625
|
+
throw new Error(`similar(${table}): id "${String(id)}" bulunamad\u0131 ya da embedding'i yok (FR-022)`);
|
|
1626
|
+
}
|
|
1627
|
+
const p = {
|
|
1628
|
+
vector: JSON.parse(v),
|
|
1629
|
+
mode: "vector",
|
|
1630
|
+
where: opts.where,
|
|
1631
|
+
limit: opts.limit,
|
|
1632
|
+
using: opts.using,
|
|
1633
|
+
minScore: opts.minScore,
|
|
1634
|
+
recency: opts.recency,
|
|
1635
|
+
blocksPerRow: opts.blocksPerRow,
|
|
1636
|
+
validity: opts.validity,
|
|
1637
|
+
boost: opts.boost,
|
|
1638
|
+
__excludeIds: [id]
|
|
1639
|
+
};
|
|
1640
|
+
return ops.search(table, p);
|
|
1641
|
+
},
|
|
1642
|
+
/**
|
|
1643
|
+
* recommend (T018, FR-023): positive/negative id kümelerinden öneri.
|
|
1644
|
+
* Hedef vektör DB-İÇİ CTE'lerle türer: pos = avg(embedding of positives),
|
|
1645
|
+
* negative varsa hedef = pos.v + (pos.v - neg.v) — pgvector avg agregası
|
|
1646
|
+
* ve +/- operatörleri ŞEMA-NİTELİKLİ (M-1: search_path'e güvenilmez).
|
|
1647
|
+
* Vektör matematiği istemciye inmez; bulunamayan positive/negative
|
|
1648
|
+
* adlandırılmış hatadır ve kaynak id'ler sonuçtan düşer.
|
|
1649
|
+
*/
|
|
1650
|
+
async recommend(table, opts) {
|
|
1651
|
+
const positive = opts?.positive;
|
|
1652
|
+
if (!Array.isArray(positive) || positive.length === 0) {
|
|
1653
|
+
throw new Error(`recommend(${table}): positive bo\u015F olamaz \u2014 en az bir kaynak id gerekir (FR-023)`);
|
|
1654
|
+
}
|
|
1655
|
+
const negative = Array.isArray(opts.negative) ? opts.negative : [];
|
|
1656
|
+
const cfg = searchConfigFor(table);
|
|
1657
|
+
if (!cfg) {
|
|
1658
|
+
throw new Error(`recommend(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
|
|
1320
1659
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1660
|
+
let leg = null;
|
|
1661
|
+
if (cfg.chunk === void 0) {
|
|
1662
|
+
leg = pickLeg(table, cfg.legs, opts.using);
|
|
1663
|
+
if (leg === null) {
|
|
1664
|
+
throw new Error(`recommend(${table}): tabloda vekt\xF6r kolonu yok \u2014 \xF6neri semantik vekt\xF6r ister (FR-023)`);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const live = await at();
|
|
1668
|
+
const sch = await vectorSchemaWithGuc(live);
|
|
1669
|
+
const bind = [];
|
|
1670
|
+
const add = (x) => {
|
|
1671
|
+
bind.push(x);
|
|
1672
|
+
return `$${bind.length}`;
|
|
1673
|
+
};
|
|
1674
|
+
const avgSel = (ids) => {
|
|
1675
|
+
const inList = ids.map((x) => add(x)).join(", ");
|
|
1676
|
+
return cfg.chunk !== void 0 ? `SELECT ${quoteIdent(sch)}.avg(c.embedding) AS v FROM ${quoteIdent(cfg.chunk.table)} c WHERE c.${quoteIdent(`parent_${cfg.pk}`)} IN (${inList}) AND c.embedding IS NOT NULL` : `SELECT ${quoteIdent(sch)}.avg(t.${quoteIdent(leg.column)}) AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} IN (${inList}) AND t.${quoteIdent(leg.column)} IS NOT NULL`;
|
|
1677
|
+
};
|
|
1678
|
+
const targetSql = negative.length === 0 ? `WITH pos AS (${avgSel(positive)}) SELECT pos.v AS v, (pos.v IS NULL) AS pos_missing FROM pos` : `WITH pos AS (${avgSel(positive)}), neg AS (${avgSel(negative)}) SELECT (pos.v OPERATOR(${quoteIdent(sch)}.+) (pos.v OPERATOR(${quoteIdent(sch)}.-) neg.v)) AS v, (pos.v IS NULL) AS pos_missing, (neg.v IS NULL) AS neg_missing FROM pos, neg`;
|
|
1679
|
+
const rows = await live.unsafe(targetSql, bind);
|
|
1680
|
+
const r0 = rows?.[0];
|
|
1681
|
+
if (r0 === void 0 || r0.pos_missing === true) {
|
|
1682
|
+
throw new Error(`recommend(${table}): positive id'lerin hi\xE7biri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
|
|
1683
|
+
}
|
|
1684
|
+
if (negative.length > 0 && r0.neg_missing === true) {
|
|
1685
|
+
throw new Error(`recommend(${table}): negative id'leri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
|
|
1686
|
+
}
|
|
1687
|
+
if (typeof r0.v !== "string") {
|
|
1688
|
+
throw new Error(`recommend(${table}): hedef vekt\xF6r hesaplanamad\u0131 (FR-023)`);
|
|
1689
|
+
}
|
|
1690
|
+
const p = {
|
|
1691
|
+
vector: JSON.parse(r0.v),
|
|
1692
|
+
mode: "vector",
|
|
1693
|
+
where: opts.where,
|
|
1694
|
+
limit: opts.limit,
|
|
1695
|
+
using: opts.using,
|
|
1696
|
+
minScore: opts.minScore,
|
|
1697
|
+
recency: opts.recency,
|
|
1698
|
+
blocksPerRow: opts.blocksPerRow,
|
|
1699
|
+
validity: opts.validity,
|
|
1700
|
+
boost: opts.boost,
|
|
1701
|
+
__excludeIds: [...positive, ...negative]
|
|
1702
|
+
};
|
|
1703
|
+
return ops.search(table, p);
|
|
1704
|
+
},
|
|
1705
|
+
/**
|
|
1706
|
+
* supersede (T021, FR-029, C-9): validity'li tabloda satırın YENİ
|
|
1707
|
+
* versiyonunu TEK savepoint'te yazar — eski satır kapatılır
|
|
1708
|
+
* (valid_to = now(), superseded_by = yeni pk), yeni satır eklenir, dönüş
|
|
1709
|
+
* yeni satırdır. Yeni pk CLIENT'ta üretilir (row'da verilmemişse
|
|
1710
|
+
* randomUUID — declared şemaların uuid().defaultRandom() standardı) ki
|
|
1711
|
+
* kapatma UPDATE'i INSERT'ten ÖNCE koşabilsin: 0 satır = zaten superseded
|
|
1712
|
+
* ya da yok → INSERT hiç denenmez; INSERT hatası ise savepoint'le
|
|
1713
|
+
* kapatmayı da geri sarar — yarım supersede diye bir durum yoktur.
|
|
1714
|
+
*/
|
|
1715
|
+
async supersede(table, id, row) {
|
|
1716
|
+
const cfg = searchConfigFor(table);
|
|
1717
|
+
if (cfg === null || cfg.validity !== true) {
|
|
1718
|
+
throw new Error(`supersede(${table}): tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`);
|
|
1719
|
+
}
|
|
1720
|
+
const live = await at();
|
|
1721
|
+
return live.savepoint(async (sp) => {
|
|
1722
|
+
const newId = row[cfg.pk] ?? crypto.randomUUID();
|
|
1723
|
+
const closed = await sp.unsafe(
|
|
1724
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now(), "superseded_by" = $1 WHERE ${quoteIdent(cfg.pk)} = $2 AND "valid_to" IS NULL RETURNING ${quoteIdent(cfg.pk)}`,
|
|
1725
|
+
[newId, id]
|
|
1726
|
+
);
|
|
1727
|
+
if (closed.length === 0) {
|
|
1728
|
+
throw new Error(`supersede(${table}): id "${String(id)}" zaten superseded ya da yok (FR-029)`);
|
|
1729
|
+
}
|
|
1730
|
+
const data = { ...row, [cfg.pk]: newId };
|
|
1731
|
+
const cols = Object.keys(data);
|
|
1732
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1733
|
+
const rows = await sp.unsafe(
|
|
1734
|
+
`INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`,
|
|
1735
|
+
asBindParams(table, cols, data)
|
|
1736
|
+
);
|
|
1737
|
+
if (!rows[0]) {
|
|
1738
|
+
throw new Error(
|
|
1739
|
+
`supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
return asTableRow(table, rows[0]);
|
|
1743
|
+
});
|
|
1323
1744
|
},
|
|
1324
1745
|
/** A real SAVEPOINT inside the request's transaction. */
|
|
1325
1746
|
async transaction(cb) {
|
|
@@ -1363,6 +1784,7 @@ function createOps(tx) {
|
|
|
1363
1784
|
var currentSchema = {};
|
|
1364
1785
|
function setSchema(schema) {
|
|
1365
1786
|
cachedVectorSchema = null;
|
|
1787
|
+
cachedTrgmSchema = null;
|
|
1366
1788
|
const s = schema;
|
|
1367
1789
|
currentSchema = (s && "default" in s ? s.default : s) ?? {};
|
|
1368
1790
|
}
|
|
@@ -1831,7 +2253,6 @@ async function createApp(opts) {
|
|
|
1831
2253
|
Notifications: modules.Notifications ?? stubModule("Notifications"),
|
|
1832
2254
|
Flags: modules.Flags ?? stubModule("Flags"),
|
|
1833
2255
|
Realtime: modules.Realtime ?? stubModule("Realtime"),
|
|
1834
|
-
Purchases: modules.Purchases ?? stubModule("Purchases"),
|
|
1835
2256
|
// Named, never undefined. A backend started without a secrets client
|
|
1836
2257
|
// that returned `undefined` here would fail inside the handler as
|
|
1837
2258
|
// "Cannot read properties of undefined", which says nothing about what
|