@lunora/sql-store 1.0.0-alpha.26 → 1.0.0-alpha.27

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.
@@ -46,9 +46,19 @@ interface SqlDialect {
46
46
  real: string;
47
47
  text: string;
48
48
  };
49
- /** Map a stored value back to its JS form, by effective validator `kind` (inverse of `encode`). */
49
+ /**
50
+ * Map a stored value back to its JS form, by effective validator `kind`
51
+ * (inverse of `encode`). NOTE: currently **unused** by the store core, which
52
+ * hard-codes `sqliteDecode` in `decodeGlobalRow` on every engine. Kept on the
53
+ * seam for a future engine-native codec; an override here does not run today.
54
+ */
50
55
  decode: (value: unknown, kind: string | undefined) => unknown;
51
- /** Map a JS value to its bound storage form (boolean→1/0, bigint→string, object→JSON on SQLite; mostly native on PG). */
56
+ /**
57
+ * Map a JS value to its bound storage form (boolean→1/0, bigint→string,
58
+ * object→JSON on SQLite). NOTE: currently **unused** by the store core, which
59
+ * hard-codes `sqliteEncode` as `serializeColumnValue` on every engine. Kept on
60
+ * the seam for a future engine-native codec; an override here does not run today.
61
+ */
52
62
  encode: (value: unknown) => unknown;
53
63
  /** The framework columns every global table carries — the `id` primary key and `_creationTime` — as `{ name, type }` so the DDL builder can quote each name through the engine's dialect. */
54
64
  frameworkColumns: () => ReadonlyArray<{
package/dist/dialect.d.ts CHANGED
@@ -46,9 +46,19 @@ interface SqlDialect {
46
46
  real: string;
47
47
  text: string;
48
48
  };
49
- /** Map a stored value back to its JS form, by effective validator `kind` (inverse of `encode`). */
49
+ /**
50
+ * Map a stored value back to its JS form, by effective validator `kind`
51
+ * (inverse of `encode`). NOTE: currently **unused** by the store core, which
52
+ * hard-codes `sqliteDecode` in `decodeGlobalRow` on every engine. Kept on the
53
+ * seam for a future engine-native codec; an override here does not run today.
54
+ */
50
55
  decode: (value: unknown, kind: string | undefined) => unknown;
51
- /** Map a JS value to its bound storage form (boolean→1/0, bigint→string, object→JSON on SQLite; mostly native on PG). */
56
+ /**
57
+ * Map a JS value to its bound storage form (boolean→1/0, bigint→string,
58
+ * object→JSON on SQLite). NOTE: currently **unused** by the store core, which
59
+ * hard-codes `sqliteEncode` as `serializeColumnValue` on every engine. Kept on
60
+ * the seam for a future engine-native codec; an override here does not run today.
61
+ */
52
62
  encode: (value: unknown) => unknown;
53
63
  /** The framework columns every global table carries — the `id` primary key and `_creationTime` — as `{ name, type }` so the DDL builder can quote each name through the engine's dialect. */
54
64
  frameworkColumns: () => ReadonlyArray<{
package/dist/index.d.mts CHANGED
@@ -168,6 +168,12 @@ declare const effectiveColumnKind: (validator: ValidatorLike) => string | undefi
168
168
  * - `object`/`array`/`record`: JSON string → parsed value.
169
169
  * - `union`/`any`: parsed back only when the stored string is a JSON non-scalar
170
170
  * (a scalar union member round-trips through SQLite's native column type).
171
+ * CAVEAT: a union/any member is stored verbatim by {@link sqliteEncode}, so a
172
+ * legitimate *string* value that itself looks like JSON (`'{"a":1}'`, `'[1,2]'`)
173
+ * is ambiguous on read and decodes back to the parsed object/array, not the
174
+ * original string. This is inherent to sharing one TEXT column between a string
175
+ * and an object member; disambiguating would require a breaking storage-format
176
+ * change (tagging encoded non-scalars), so it is documented rather than fixed.
171
177
  * - everything else (string/number/date/timestamp/id/literal): verbatim.
172
178
  */
173
179
  declare const sqliteDecode: (raw: unknown, kind: string | undefined) => unknown;
package/dist/index.d.ts CHANGED
@@ -168,6 +168,12 @@ declare const effectiveColumnKind: (validator: ValidatorLike) => string | undefi
168
168
  * - `object`/`array`/`record`: JSON string → parsed value.
169
169
  * - `union`/`any`: parsed back only when the stored string is a JSON non-scalar
170
170
  * (a scalar union member round-trips through SQLite's native column type).
171
+ * CAVEAT: a union/any member is stored verbatim by {@link sqliteEncode}, so a
172
+ * legitimate *string* value that itself looks like JSON (`'{"a":1}'`, `'[1,2]'`)
173
+ * is ambiguous on read and decodes back to the parsed object/array, not the
174
+ * original string. This is inherent to sharing one TEXT column between a string
175
+ * and an object member; disambiguating would require a breaking storage-format
176
+ * change (tagging encoded non-scalars), so it is documented rather than fixed.
171
177
  * - everything else (string/number/date/timestamp/id/literal): verbatim.
172
178
  */
173
179
  declare const sqliteDecode: (raw: unknown, kind: string | undefined) => unknown;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export { createSqlCtxDb, decodeGlobalRow, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges } from './packem_shared/createSqlCtxDb-CuoIlIqO.mjs';
1
+ export { createSqlCtxDb, decodeGlobalRow, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges } from './packem_shared/createSqlCtxDb-KdluzmSA.mjs';
2
2
  export { decodeBigint, effectiveColumnKind, sqliteDecode, sqliteEncode, tryJsonParse } from './packem_shared/decodeBigint-Dedu92k4.mjs';
@@ -3,6 +3,16 @@ import { LunoraError } from '@lunora/errors';
3
3
  import { sql } from 'drizzle-orm';
4
4
  import { sqliteDecode, sqliteEncode, effectiveColumnKind } from './decodeBigint-Dedu92k4.mjs';
5
5
 
6
+ const evictOldestEntry = (map, capacity) => {
7
+ if (map.size < capacity) {
8
+ return;
9
+ }
10
+ const oldest = map.keys().next().value;
11
+ if (oldest !== void 0) {
12
+ map.delete(oldest);
13
+ }
14
+ };
15
+
6
16
  const physicalColumn = (field) => field === "_id" || field === "id" ? "id" : field;
7
17
  const columnRefSql = (field) => sql`${sql.identifier(physicalColumn(field))}`;
8
18
  const ID_ORDER_FIELDS = /* @__PURE__ */ new Set(["_id", "id"]);
@@ -158,11 +168,8 @@ const createTableNameCache = () => {
158
168
  set: (id, table) => {
159
169
  if (map.has(id)) {
160
170
  map.delete(id);
161
- } else if (map.size >= TABLE_NAME_CACHE_CAPACITY) {
162
- const oldest = map.keys().next().value;
163
- if (oldest !== void 0) {
164
- map.delete(oldest);
165
- }
171
+ } else {
172
+ evictOldestEntry(map, TABLE_NAME_CACHE_CAPACITY);
166
173
  }
167
174
  map.set(id, table);
168
175
  }
@@ -282,6 +289,10 @@ const createSearchBuilder = (search, tableName) => {
282
289
  };
283
290
  return builder;
284
291
  };
292
+ const rankIndexFieldsUnchanged = (index, previous, next) => {
293
+ const fields = [...index.partitionBy ?? [], ...index.sortBy.map((key) => key.field), ...index.where ? Object.keys(index.where) : []];
294
+ return fields.every((field) => previous[field] === next[field]);
295
+ };
285
296
  const andBranch = (conditions) => conditions.length === 1 ? conditions[0] : sql`(${sql.join(conditions, sql` AND `)})`;
286
297
  const buildRankBeforeBranches = (engine, index, sortColumns, own, rowId) => {
287
298
  const branches = [];
@@ -938,11 +949,18 @@ const createSqlCtxDb = (options) => {
938
949
  }
939
950
  };
940
951
  const syncRanks = async (tableName, id, previous, next) => {
941
- const indexes = schema.tables[tableName]?.rankIndexes;
952
+ const definition = schema.tables[tableName];
953
+ const indexes = definition?.rankIndexes;
942
954
  if (!indexes || indexes.length === 0) {
943
955
  return;
944
956
  }
957
+ const softField = definition?.softDeleteMode?.field;
958
+ const nextFieldValue = softField === void 0 || next === void 0 ? void 0 : next[softField];
959
+ const nextSoftDeleted = nextFieldValue !== null && nextFieldValue !== void 0;
945
960
  for (const index of indexes) {
961
+ if (previous !== void 0 && next !== void 0 && rankIndexFieldsUnchanged(index, previous, next)) {
962
+ continue;
963
+ }
946
964
  const cacheKey = `${tableName}::rank::${index.name}`;
947
965
  const cached = rankBackfilled.get(cacheKey);
948
966
  const exists = cached ?? await rankTableExists(tableName, index.name);
@@ -953,7 +971,7 @@ const createSqlCtxDb = (options) => {
953
971
  if (previous) {
954
972
  await queryRun(exec, dialect, sql`DELETE FROM ${sql.identifier(rankTable)} WHERE ${sql.identifier("__id__")} = ${id}`);
955
973
  }
956
- if (next) {
974
+ if (next && !nextSoftDeleted) {
957
975
  if (index.where && !matchesRankStaticWhere(next, index.where)) {
958
976
  continue;
959
977
  }
@@ -1091,6 +1109,9 @@ const createSqlCtxDb = (options) => {
1091
1109
  const row = rowsIndexed2[0];
1092
1110
  return [{ key: { ...planned.partial }, value: readAggregateValue(agg.op, { count: row.count, value: aggregateScalar(row.value) }) }];
1093
1111
  }
1112
+ if (partialKeys.length > 0) {
1113
+ return void 0;
1114
+ }
1094
1115
  const rowsIndexed = await queryAll(
1095
1116
  exec,
1096
1117
  dialect,
@@ -1467,7 +1488,7 @@ const createSqlCtxDb = (options) => {
1467
1488
  id = generateId();
1468
1489
  usedExplicitId = false;
1469
1490
  }
1470
- const creationTime = typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
1491
+ const creationTime = insertOptions?.allowExplicitId && typeof withDefaults["_creationTime"] === "number" ? withDefaults["_creationTime"] : clock();
1471
1492
  const documentWithMeta = { ...withDefaults, _creationTime: creationTime, _id: id };
1472
1493
  if (hasMatchingTrigger(tableName, "before", "insert")) {
1473
1494
  await fireTriggers("before", "insert", { doc: { ...documentWithMeta }, id, op: "insert", table: tableName });
@@ -1552,7 +1573,7 @@ const createSqlCtxDb = (options) => {
1552
1573
  if (wasDeleted) {
1553
1574
  const row = decodeRow(definition, snapshot);
1554
1575
  if (row !== null) {
1555
- await syncRanks(tableName, id, void 0, row);
1576
+ await syncRanks(tableName, id, void 0, { ...row, [field]: null });
1556
1577
  }
1557
1578
  }
1558
1579
  },
@@ -1754,7 +1775,7 @@ const createSqlCtxDb = (options) => {
1754
1775
  }
1755
1776
  return { continueCursor, isDone: !hasMore, page: documents };
1756
1777
  },
1757
- async replace(id, document, expectedTable) {
1778
+ async replace(id, document, expectedTable, replaceOptions) {
1758
1779
  const tableName = await resolveTableName(id, expectedTable);
1759
1780
  if (!tableName) {
1760
1781
  throw new LunoraError("INTERNAL", `document not found: ${id}`);
@@ -1769,7 +1790,7 @@ const createSqlCtxDb = (options) => {
1769
1790
  }
1770
1791
  const needsPrevious = hasTrigger(schema, tableName, "update") || (definition.aggregateIndexes ?? []).length > 0 || (definition.rankIndexes ?? []).length > 0;
1771
1792
  const previous = needsPrevious ? decodeRow(definition, snapshot) ?? void 0 : void 0;
1772
- const creationTime = typeof document["_creationTime"] === "number" ? document["_creationTime"] : clock();
1793
+ const creationTime = replaceOptions?.allowExplicitId && typeof document["_creationTime"] === "number" ? document["_creationTime"] : clock();
1773
1794
  const replaced = { ...document, _creationTime: creationTime, _id: id };
1774
1795
  applyOnUpdate(definition, document, replaced, auth);
1775
1796
  runRowValidators(definition, replaced);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/sql-store",
3
- "version": "1.0.0-alpha.26",
3
+ "version": "1.0.0-alpha.27",
4
4
  "description": "Internal dialect-parameterized SQL store core for Lunora .global() backends (D1, PlanetScale)",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -50,8 +50,8 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@lunora/do": "1.0.0-alpha.26",
54
- "@lunora/errors": "1.0.0-alpha.3",
53
+ "@lunora/do": "1.0.0-alpha.27",
54
+ "@lunora/errors": "1.0.0-alpha.4",
55
55
  "drizzle-orm": "^0.45.2"
56
56
  },
57
57
  "engines": {