@lunora/sql-store 1.0.0-alpha.1 → 1.0.0-alpha.11
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/README.md +80 -1
- package/__assets__/package-og.svg +1 -1
- package/dist/index.mjs +2 -2
- package/dist/packem_shared/{createSqlCtxDb-CQFYyQLP.mjs → createSqlCtxDb-BlGq23Wp.mjs} +103 -19
- package/package.json +3 -3
- /package/dist/packem_shared/{sqliteEncode-Dedu92k4.mjs → decodeBigint-Dedu92k4.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createSqlCtxDb, decodeGlobalRow, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges } from './packem_shared/createSqlCtxDb-
|
|
2
|
-
export { decodeBigint, effectiveColumnKind, sqliteDecode, sqliteEncode, tryJsonParse } from './packem_shared/
|
|
1
|
+
export { createSqlCtxDb, decodeGlobalRow, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges } from './packem_shared/createSqlCtxDb-BlGq23Wp.mjs';
|
|
2
|
+
export { decodeBigint, effectiveColumnKind, sqliteDecode, sqliteEncode, tryJsonParse } from './packem_shared/decodeBigint-Dedu92k4.mjs';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { aggregateTableName, rankTableName, ftsTableName, hasTrigger, runRowValidators, assertFlatPredicate, mergeWhere, sortColumnName, resolveRankPartition, encodePartitionKey, decodeCursor, RANK_TIEBREAK, CountRlsUnsupportedError, normalizeIdStructurally, assertValidClientId, aggregateSqlFunction, compileWhereSql, normalizeOrderKeys, buildSeekWhere, resolveRelationPredicates, resolveWith, encodeCursor, NotFoundError, applyOnDelete, ConflictError, normalizeCountArgument, selectIndexForCount, encodeAggregateKey, selectIndexForAggregate, readAggregateValue, renderSql, runTriggers, matchesRankStaticWhere, stringifySearchText, selectIndexForGroupBy, matchesStaticWhere, coerceAggregateNumber, foldAggregateTally, NotUniqueError, throwingScheduler, tokenizeSearch, buildFtsMatch, scoreDocument } from '@lunora/do';
|
|
1
|
+
import { aggregateTableName, rankTableName, ftsTableName, hasTrigger, runRowValidators, assertFlatPredicate, mergeWhere, sortColumnName, resolveRankPartition, encodePartitionKey, decodeCursor, RANK_TIEBREAK, CountRlsUnsupportedError, normalizeIdStructurally, assertValidClientId, aggregateSqlFunction, softDeleteScope, compileWhereSql, normalizeOrderKeys, buildSeekWhere, resolveRelationPredicates, resolveWith, applySelect, encodeCursor, NotFoundError, applyOnDelete, ConflictError, normalizeCountArgument, selectIndexForCount, encodeAggregateKey, selectIndexForAggregate, readAggregateValue, renderSql, runTriggers, matchesRankStaticWhere, stringifySearchText, selectIndexForGroupBy, fanOutScalarCounts, matchesStaticWhere, coerceAggregateNumber, foldAggregateTally, NotUniqueError, throwingScheduler, tokenizeSearch, buildFtsMatch, scoreDocument } from '@lunora/do';
|
|
2
2
|
import { sql } from 'drizzle-orm';
|
|
3
|
-
import { sqliteDecode, effectiveColumnKind, sqliteEncode } from './
|
|
3
|
+
import { sqliteDecode, effectiveColumnKind, sqliteEncode } from './decodeBigint-Dedu92k4.mjs';
|
|
4
4
|
|
|
5
5
|
const physicalColumn = (field) => field === "_id" || field === "id" ? "id" : field;
|
|
6
6
|
const columnRefSql = (field) => sql`${sql.identifier(physicalColumn(field))}`;
|
|
@@ -212,6 +212,9 @@ const searchViaFts = async (exec, dialect, definition, tableName, search, limit)
|
|
|
212
212
|
for (const filter of search.filters) {
|
|
213
213
|
conditions.push(sql`m.${columnRefSql(filter.field)} = ${serializeColumnValue(filter.value)}`);
|
|
214
214
|
}
|
|
215
|
+
if (definition.softDeleteMode) {
|
|
216
|
+
conditions.push(sql`m.${columnRefSql(definition.softDeleteMode.field)} IS NULL`);
|
|
217
|
+
}
|
|
215
218
|
let query = sql`SELECT m.* FROM ${sql.identifier(ftName)} f JOIN ${sql.identifier(tableName)} m ON m.${sql.identifier("id")} = f.${sql.identifier("__id__")} WHERE ${sql.join(conditions, sql` AND `)} ORDER BY f.rank, m.${sql.identifier("_creationTime")} DESC`;
|
|
216
219
|
if (typeof limit === "number") {
|
|
217
220
|
query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
|
|
@@ -225,6 +228,9 @@ const searchViaScan = async (exec, dialect, definition, tableName, search, limit
|
|
|
225
228
|
return [];
|
|
226
229
|
}
|
|
227
230
|
const conditions = search.filters.map((filter) => sql`${columnRefSql(filter.field)} = ${serializeColumnValue(filter.value)}`);
|
|
231
|
+
if (definition.softDeleteMode) {
|
|
232
|
+
conditions.push(sql`${columnRefSql(definition.softDeleteMode.field)} IS NULL`);
|
|
233
|
+
}
|
|
228
234
|
let query = sql`SELECT * FROM ${sql.identifier(tableName)}`;
|
|
229
235
|
if (conditions.length > 0) {
|
|
230
236
|
query = sql`${query} WHERE ${sql.join(conditions, sql` AND `)}`;
|
|
@@ -1122,10 +1128,11 @@ const createSqlCtxDb = (options) => {
|
|
|
1122
1128
|
if (!aggOptions.field) {
|
|
1123
1129
|
throw new Error(`aggregate(${tableName}, { op: "${aggOptions.op}" }): "field" is required for non-count reducers`);
|
|
1124
1130
|
}
|
|
1125
|
-
const
|
|
1131
|
+
const aggScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1132
|
+
const effective = mergeWhere(mergeWhere(aggOptions.baseWhere, aggOptions.where), aggScope);
|
|
1126
1133
|
const resolved = await resolveAggregateRelations(effective, tableName, aggOptions.relationBaseWhere);
|
|
1127
1134
|
const hasRelation = resolved !== effective;
|
|
1128
|
-
if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation) {
|
|
1135
|
+
if (definition.aggregateIndexes && !aggOptions.baseWhere && !hasRelation && !aggScope) {
|
|
1129
1136
|
const planned = selectIndexForAggregate(definition.aggregateIndexes, aggOptions.op, aggOptions.field, aggOptions.where);
|
|
1130
1137
|
if (planned) {
|
|
1131
1138
|
const counterReady = await ensureBackfilled(tableName, planned.index);
|
|
@@ -1159,10 +1166,11 @@ const createSqlCtxDb = (options) => {
|
|
|
1159
1166
|
if (countOptions.restrictsCounts) {
|
|
1160
1167
|
throw new CountRlsUnsupportedError(tableName);
|
|
1161
1168
|
}
|
|
1162
|
-
const
|
|
1169
|
+
const countScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1170
|
+
const effective = mergeWhere(mergeWhere(countOptions.baseWhere, countOptions.where), countScope);
|
|
1163
1171
|
const resolved = await resolveAggregateRelations(effective, tableName, countOptions.relationBaseWhere);
|
|
1164
1172
|
const hasRelation = resolved !== effective;
|
|
1165
|
-
if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation) {
|
|
1173
|
+
if (definition.aggregateIndexes && !countOptions.baseWhere && !hasRelation && !countScope) {
|
|
1166
1174
|
const planned = selectIndexForCount(definition.aggregateIndexes, countOptions.where);
|
|
1167
1175
|
if (planned) {
|
|
1168
1176
|
const counterReady = await ensureBackfilled(tableName, planned.index);
|
|
@@ -1183,7 +1191,7 @@ const createSqlCtxDb = (options) => {
|
|
|
1183
1191
|
const rows = await queryAll(exec, dialect, whereCondition ? sql`${query} WHERE ${whereCondition}` : query);
|
|
1184
1192
|
return Number(rows[0]?.["count"] ?? 0);
|
|
1185
1193
|
},
|
|
1186
|
-
async delete(id, expectedTable) {
|
|
1194
|
+
async delete(id, expectedTable, deleteOptions) {
|
|
1187
1195
|
const tableName = await resolveTableName(id, expectedTable);
|
|
1188
1196
|
if (!tableName) {
|
|
1189
1197
|
return;
|
|
@@ -1194,6 +1202,11 @@ const createSqlCtxDb = (options) => {
|
|
|
1194
1202
|
}
|
|
1195
1203
|
const snapshot = await rawRow(tableName, id);
|
|
1196
1204
|
const existing = decodeRow(definition, snapshot);
|
|
1205
|
+
const hard = deleteOptions?.hard === true;
|
|
1206
|
+
const softField = !hard && definition.softDeleteMode ? definition.softDeleteMode.field : void 0;
|
|
1207
|
+
if (softField && (!existing || existing[softField] !== null && existing[softField] !== void 0)) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1197
1210
|
if (hasMatchingTrigger(tableName, "before", "delete")) {
|
|
1198
1211
|
await fireTriggers("before", "delete", { id, op: "delete", previous: existing ?? void 0, table: tableName });
|
|
1199
1212
|
}
|
|
@@ -1206,10 +1219,10 @@ const createSqlCtxDb = (options) => {
|
|
|
1206
1219
|
`cross-backend cascade from global '${tableName}' into shardBy '${holderTable}' is not supported — would require Query Coordinator fan-out across shards`
|
|
1207
1220
|
);
|
|
1208
1221
|
}
|
|
1209
|
-
const holders = await writer.findMany(holderTable, { where: { [field]: value } });
|
|
1222
|
+
const holders = await writer.findMany(holderTable, { includeDeleted: hard, where: { [field]: value } });
|
|
1210
1223
|
return holders.page;
|
|
1211
1224
|
},
|
|
1212
|
-
onCascade: (_holderTable, holderId) => writer.delete(holderId),
|
|
1225
|
+
onCascade: (_holderTable, holderId) => writer.delete(holderId, void 0, deleteOptions),
|
|
1213
1226
|
onRestrict: (message) => {
|
|
1214
1227
|
throw new ConflictError(message, "restrict");
|
|
1215
1228
|
},
|
|
@@ -1220,6 +1233,23 @@ const createSqlCtxDb = (options) => {
|
|
|
1220
1233
|
});
|
|
1221
1234
|
await ensureBackfilledForTable(tableName);
|
|
1222
1235
|
await ensureRankBackfilledForTable(tableName);
|
|
1236
|
+
if (softField && existing) {
|
|
1237
|
+
const merged = { ...existing, [softField]: clock() };
|
|
1238
|
+
const assignments = sql.join(
|
|
1239
|
+
// eslint-disable-next-line unicorn/no-null -- SQL bind value: an absent column binds `null`, matching the patch path.
|
|
1240
|
+
Object.keys(definition.shape).map((field) => sql`${sql.identifier(field)} = ${serializeColumnValue(merged[field] ?? null)}`),
|
|
1241
|
+
sql`, `
|
|
1242
|
+
);
|
|
1243
|
+
await runGuardedWrite(tableName, "UPDATE", assignments, snapshot);
|
|
1244
|
+
await syncAggregates(tableName, existing, merged);
|
|
1245
|
+
await syncRanks(tableName, id, existing, void 0);
|
|
1246
|
+
await syncSearch(tableName, id, merged);
|
|
1247
|
+
await recordCdc(tableName, id, "update", merged);
|
|
1248
|
+
if (hasMatchingTrigger(tableName, "after", "delete")) {
|
|
1249
|
+
await fireTriggers("after", "delete", { id, op: "delete", previous: existing, table: tableName });
|
|
1250
|
+
}
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1223
1253
|
await runGuardedWrite(tableName, "DELETE", void 0, snapshot);
|
|
1224
1254
|
tableNameCache.delete(id);
|
|
1225
1255
|
await syncAggregates(tableName, existing ?? void 0, void 0);
|
|
@@ -1250,13 +1280,37 @@ const createSqlCtxDb = (options) => {
|
|
|
1250
1280
|
const orderKeys = normalizeOrderKeys(args.orderBy);
|
|
1251
1281
|
const seek = args.cursor ? buildSeekWhere(orderKeys, decodeCursor(args.cursor)) : void 0;
|
|
1252
1282
|
const relationFetcher = relationPredicateFetcher;
|
|
1253
|
-
const
|
|
1254
|
-
if (
|
|
1255
|
-
|
|
1283
|
+
const relationGroupedCounter = async (childTable, whereField, values, policyWhere) => {
|
|
1284
|
+
if (isShardLocalTarget(childTable)) {
|
|
1285
|
+
if (!crossShardCounter) {
|
|
1286
|
+
return crossBackendUnsupported(childTable);
|
|
1287
|
+
}
|
|
1288
|
+
return fanOutScalarCounts(crossShardCounter, childTable, whereField, values, policyWhere);
|
|
1289
|
+
}
|
|
1290
|
+
const childDefinition = schema.tables[childTable];
|
|
1291
|
+
if (!childDefinition) {
|
|
1292
|
+
throw new Error(`unknown table: ${childTable}`);
|
|
1293
|
+
}
|
|
1294
|
+
const softScope = softDeleteScope(childDefinition.softDeleteMode, void 0);
|
|
1295
|
+
const inFilter = { [whereField]: { in: values } };
|
|
1296
|
+
const combined = mergeWhere(mergeWhere(inFilter, policyWhere), softScope);
|
|
1297
|
+
const resolvedCombined = await resolveAggregateRelations(combined, childTable, void 0);
|
|
1298
|
+
const whereCondition2 = compileWhereSql(resolvedCombined, whereSqlStrategy);
|
|
1299
|
+
const fieldRef = columnRefSql(whereField);
|
|
1300
|
+
let groupQuery = sql`SELECT ${fieldRef} AS __fk__, COUNT(*) AS count FROM ${sql.identifier(childTable)}`;
|
|
1301
|
+
if (whereCondition2) {
|
|
1302
|
+
groupQuery = sql`${groupQuery} WHERE ${whereCondition2}`;
|
|
1256
1303
|
}
|
|
1257
|
-
|
|
1304
|
+
groupQuery = sql`${groupQuery} GROUP BY ${fieldRef}`;
|
|
1305
|
+
const groupRows = await queryAll(exec, dialect, groupQuery);
|
|
1306
|
+
const result = /* @__PURE__ */ new Map();
|
|
1307
|
+
for (const row of groupRows) {
|
|
1308
|
+
result.set(row["__fk__"], Number(row["count"] ?? 0));
|
|
1309
|
+
}
|
|
1310
|
+
return result;
|
|
1258
1311
|
};
|
|
1259
1312
|
let predicate = mergeWhere(args.baseWhere, args.where);
|
|
1313
|
+
predicate = mergeWhere(predicate, softDeleteScope(definition.softDeleteMode, args.includeDeleted));
|
|
1260
1314
|
predicate = await resolveRelationPredicates(predicate, {
|
|
1261
1315
|
fetcher: relationFetcher,
|
|
1262
1316
|
maxRelationKeys,
|
|
@@ -1282,21 +1336,30 @@ const createSqlCtxDb = (options) => {
|
|
|
1282
1336
|
const documents = decodeRows(definition, rows);
|
|
1283
1337
|
if (limit === void 0) {
|
|
1284
1338
|
if (args.with) {
|
|
1285
|
-
await resolveWith({
|
|
1339
|
+
await resolveWith({
|
|
1340
|
+
fetcher: relationFetcher,
|
|
1341
|
+
groupedCounter: relationGroupedCounter,
|
|
1342
|
+
parents: documents,
|
|
1343
|
+
schema,
|
|
1344
|
+
tableName,
|
|
1345
|
+
with: args.with
|
|
1346
|
+
});
|
|
1286
1347
|
}
|
|
1287
|
-
return { continueCursor: null, isDone: true, page: documents };
|
|
1348
|
+
return { continueCursor: null, isDone: true, page: applySelect(documents, args.select, args.with) };
|
|
1288
1349
|
}
|
|
1289
1350
|
const hasMore = documents.length > limit;
|
|
1290
1351
|
const page = hasMore ? documents.slice(0, limit) : documents;
|
|
1291
1352
|
const last = page.at(-1);
|
|
1292
1353
|
if (args.with) {
|
|
1293
|
-
await resolveWith({
|
|
1354
|
+
await resolveWith({ fetcher: relationFetcher, groupedCounter: relationGroupedCounter, parents: page, schema, tableName, with: args.with });
|
|
1294
1355
|
}
|
|
1295
1356
|
return {
|
|
1357
|
+
// The cursor is encoded from `last` (the full, unprojected row), so
|
|
1358
|
+
// `applySelect` only trims the returned payload — paging is intact.
|
|
1296
1359
|
// eslint-disable-next-line unicorn/no-null -- public return shape: `continueCursor` is `string | null`; `null` marks the final page.
|
|
1297
1360
|
continueCursor: hasMore && last ? encodeCursor(last, orderKeys) : null,
|
|
1298
1361
|
isDone: !hasMore,
|
|
1299
|
-
page
|
|
1362
|
+
page: applySelect(page, args.select, args.with)
|
|
1300
1363
|
};
|
|
1301
1364
|
},
|
|
1302
1365
|
async get(id, expectedTable) {
|
|
@@ -1322,10 +1385,11 @@ const createSqlCtxDb = (options) => {
|
|
|
1322
1385
|
if (agg.op !== "count" && !agg.field) {
|
|
1323
1386
|
throw new Error(`groupBy(${tableName}, { agg: { op: "${agg.op}" } }): "field" is required for non-count reducers`);
|
|
1324
1387
|
}
|
|
1325
|
-
const
|
|
1388
|
+
const groupScope = softDeleteScope(definition.softDeleteMode, void 0);
|
|
1389
|
+
const effective = mergeWhere(mergeWhere(groupOptions.baseWhere, groupOptions.where), groupScope);
|
|
1326
1390
|
const resolved = await resolveAggregateRelations(effective, tableName, groupOptions.relationBaseWhere);
|
|
1327
1391
|
const hasRelation = resolved !== effective;
|
|
1328
|
-
if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation) {
|
|
1392
|
+
if (definition.aggregateIndexes && !groupOptions.baseWhere && !hasRelation && !groupScope) {
|
|
1329
1393
|
const indexed = await tryIndexedGroupBy(tableName, definition.aggregateIndexes, agg, groupOptions);
|
|
1330
1394
|
if (indexed !== void 0) {
|
|
1331
1395
|
return indexed;
|
|
@@ -1452,6 +1516,26 @@ const createSqlCtxDb = (options) => {
|
|
|
1452
1516
|
await fireTriggers("after", "update", { doc: merged, id, op: "update", previous: existing, table: tableName });
|
|
1453
1517
|
}
|
|
1454
1518
|
},
|
|
1519
|
+
async restore(id, expectedTable) {
|
|
1520
|
+
const tableName = await resolveTableName(id, expectedTable);
|
|
1521
|
+
if (!tableName) {
|
|
1522
|
+
throw new Error(`document not found: ${id}`);
|
|
1523
|
+
}
|
|
1524
|
+
const definition = schema.tables[tableName];
|
|
1525
|
+
const field = definition?.softDeleteMode?.field;
|
|
1526
|
+
if (!definition || !field) {
|
|
1527
|
+
throw new Error(`ctx.db.restore: table "${tableName}" is not a .softDelete() table`);
|
|
1528
|
+
}
|
|
1529
|
+
const snapshot = await rawRow(tableName, id);
|
|
1530
|
+
const wasDeleted = snapshot?.[field] !== null && snapshot?.[field] !== void 0;
|
|
1531
|
+
await writer.patch(id, { [field]: null }, expectedTable);
|
|
1532
|
+
if (wasDeleted) {
|
|
1533
|
+
const row = decodeRow(definition, snapshot);
|
|
1534
|
+
if (row !== null) {
|
|
1535
|
+
await syncRanks(tableName, id, void 0, row);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1455
1539
|
query(tableName) {
|
|
1456
1540
|
const definition = schema.tables[tableName];
|
|
1457
1541
|
if (!definition) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/sql-store",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.11",
|
|
4
4
|
"description": "Internal dialect-parameterized SQL store core for Lunora .global() backends (D1, PlanetScale)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/sql-store"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist",
|
|
28
|
+
"./dist",
|
|
29
29
|
"README.md",
|
|
30
30
|
"LICENSE.md",
|
|
31
31
|
"__assets__"
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/do": "1.0.0-alpha.
|
|
53
|
+
"@lunora/do": "1.0.0-alpha.11",
|
|
54
54
|
"drizzle-orm": "^0.45.2"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
File without changes
|