@lunora/do 1.0.0-alpha.37 → 1.0.0-alpha.39

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/index.mjs CHANGED
@@ -3,16 +3,17 @@ export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, norma
3
3
  export { aggregateTableName, coerceAggregateNumber, encodeAggregateKey, foldAggregateTally, readAggregateValue } from './packem_shared/aggregateTableName-CxNqY1Sl.mjs';
4
4
  export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy } from './packem_shared/CountRlsUnsupportedError-BGxj0pgS.mjs';
5
5
  export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, ensureAuthMetricsTables, readAuthMetrics, recordAuthEvent } from './packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
6
- export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './packem_shared/context-telemetry-DWfYDxCS.mjs';
7
- export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-Ca21uDuU.mjs';
6
+ export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './packem_shared/context-telemetry-CqcOObBl.mjs';
7
+ export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-DbtlWwcG.mjs';
8
8
  export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration } from './packem_shared/DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
9
9
  export { SCAN_DEP, createDependencyTracker, depKey } from './packem_shared/SCAN_DEP-DLJF8dsj.mjs';
10
10
  export { renderSql } from './packem_shared/renderSql-D6eUcn2N.mjs';
11
11
  export { diffExternalSource } from './packem_shared/diffExternalSource-CovHfdyo.mjs';
12
- export { materializeExternalRows, materializeExternalRowsIncremental, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-DlQWMlw_.mjs';
13
- export { isSoftDeleted, isSourceDue, liftSourceId, pullExternalSourceIncrementalTick, pullExternalSourceTick } from './packem_shared/isSoftDeleted-YLKR6JYw.mjs';
12
+ export { materializeExternalRows, materializeExternalRowsIncremental, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-BtEGs1Fv.mjs';
13
+ export { isSoftDeleted, isSourceDue, liftSourceId, pullExternalSourceIncrementalTick, pullExternalSourceTick } from './packem_shared/isSoftDeleted-CFJmhjFP.mjs';
14
14
  export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric } from './packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
15
- export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-DVk02KpP.mjs';
15
+ export { GEO_DEFAULT_PRECISION, boundingBoxGeohashes, coveringGeohashes, encodeGeohash, haversineMeters, pointInBoundingBox } from './packem_shared/GEO_DEFAULT_PRECISION-BWnsNmpP.mjs';
16
+ export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-CnZkbXH_.mjs';
16
17
  export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
17
18
  export { MAIL_RETENTION, MAIL_TABLE, clearCapturedMail, ensureMailTable, readCapturedMail, recordCapturedMail } from './packem_shared/MAIL_RETENTION-CPpgl-dX.mjs';
18
19
  export { default as NotFoundError } from './packem_shared/NotFoundError-C70b9hLw.mjs';
@@ -20,23 +21,24 @@ export { armRestore, readBookmark } from './packem_shared/armRestore-4Px61hHS.mj
20
21
  export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-WQY8m62C.mjs';
21
22
  export { RANK_TIEBREAK, encodePartitionKey, matchesRankStaticWhere, rankTableName, resolveRankPartition, sortColumnName } from './packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs';
22
23
  export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-DnSvbjil.mjs';
23
- export { serveRelationFanout } from './packem_shared/serveRelationFanout-FdrPflv1.mjs';
24
+ export { serveRelationFanout } from './packem_shared/serveRelationFanout-k7HTh3CC.mjs';
24
25
  export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs';
25
26
  export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from './packem_shared/applyOnDelete-BXSq3S70.mjs';
26
27
  export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_shared/RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs';
27
28
  export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
28
29
  export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
29
30
  export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs';
30
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-CBlw_ir3.mjs';
31
+ export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-CJGwzfoT.mjs';
31
32
  export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
32
33
  export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-iFAA8FbD.mjs';
33
34
  export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
34
35
  export { ConflictError } from './packem_shared/ConflictError-CLoq37xH.mjs';
35
36
  export { hasTrigger, runTriggers } from './packem_shared/hasTrigger-5N6_Fx0A.mjs';
37
+ export { selectExpiredIds } from './packem_shared/selectExpiredIds-C5W29Upb.mjs';
36
38
  export { compileWhereSql } from './packem_shared/compileWhereSql-DE6yfRcQ.mjs';
37
39
  export { CDC_LOG_TABLE, applyCdcChanges, readCdcChanges, trimCdcChanges } from './packem_shared/CDC_LOG_TABLE-uwOJxJJZ.mjs';
38
- export { backfillAggregateIndexes, backfillRankIndexes } from './packem_shared/backfillAggregateIndexes-DDoT-UUI.mjs';
39
- export { runShardMigrations } from './packem_shared/runShardMigrations-DFzx6Qld.mjs';
40
+ export { backfillAggregateIndexes, backfillRankIndexes } from './packem_shared/backfillAggregateIndexes-BGTwynwh.mjs';
41
+ export { runShardMigrations } from './packem_shared/runShardMigrations-BVax6rYu.mjs';
40
42
  export { stableStringify } from './packem_shared/stableStringify-mC40mZts.mjs';
41
43
  export { stableWireKey } from './packem_shared/stableWireKey-DKuXO7T5.mjs';
42
44
  export { subscriptionListDeltas } from './packem_shared/subscriptionListDeltas-CT76bYny.mjs';
@@ -25,6 +25,7 @@ const ADMIN_FUNCTIONS = {
25
25
  getFanoutMetrics: "__lunora_admin__:getFanoutMetrics",
26
26
  getFunctionStats: "__lunora_admin__:getFunctionStats",
27
27
  getIssues: "__lunora_admin__:getIssues",
28
+ getMetricHistory: "__lunora_admin__:getMetricHistory",
28
29
  getMetricSeries: "__lunora_admin__:getMetricSeries",
29
30
  listSubscriptions: "__lunora_admin__:listSubscriptions",
30
31
  listTableIndexes: "__lunora_admin__:listTableIndexes",
@@ -0,0 +1,114 @@
1
+ const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
2
+ const GEO_DEFAULT_PRECISION = 9;
3
+ const EARTH_RADIUS_METERS = 63710088e-1;
4
+ const CELL_WIDTH_METERS = [Number.POSITIVE_INFINITY, 5009400, 1252300, 156500, 39100, 4900, 1200, 152.9, 38.2, 4.77, 1.19, 0.149, 0.037];
5
+ const clampPrecision = (precision) => Math.min(Math.max(Math.trunc(precision), 1), 12);
6
+ const encodeGeohash = (point, precision) => {
7
+ const chars = clampPrecision(precision);
8
+ let latMin = -90;
9
+ let latMax = 90;
10
+ let lngMin = -180;
11
+ let lngMax = 180;
12
+ let hash = "";
13
+ let bit = 0;
14
+ let index = 0;
15
+ let even = true;
16
+ while (hash.length < chars) {
17
+ if (even) {
18
+ const mid = (lngMin + lngMax) / 2;
19
+ if (point.lng >= mid) {
20
+ index = index * 2 + 1;
21
+ lngMin = mid;
22
+ } else {
23
+ index *= 2;
24
+ lngMax = mid;
25
+ }
26
+ } else {
27
+ const mid = (latMin + latMax) / 2;
28
+ if (point.lat >= mid) {
29
+ index = index * 2 + 1;
30
+ latMin = mid;
31
+ } else {
32
+ index *= 2;
33
+ latMax = mid;
34
+ }
35
+ }
36
+ even = !even;
37
+ if (bit < 4) {
38
+ bit += 1;
39
+ } else {
40
+ hash += BASE32[index] ?? "0";
41
+ bit = 0;
42
+ index = 0;
43
+ }
44
+ }
45
+ return hash;
46
+ };
47
+ const haversineMeters = (a, b) => {
48
+ const toRadians = (degrees) => degrees * Math.PI / 180;
49
+ const dLat = toRadians(b.lat - a.lat);
50
+ const dLng = toRadians(b.lng - a.lng);
51
+ const lat1 = toRadians(a.lat);
52
+ const lat2 = toRadians(b.lat);
53
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
54
+ return 2 * EARTH_RADIUS_METERS * Math.asin(Math.min(1, Math.sqrt(h)));
55
+ };
56
+ const precisionForRadius = (radiusMeters) => {
57
+ for (let length = CELL_WIDTH_METERS.length - 1; length >= 1; length -= 1) {
58
+ const width = CELL_WIDTH_METERS[length];
59
+ if (width !== void 0 && width >= radiusMeters) {
60
+ return length;
61
+ }
62
+ }
63
+ return 1;
64
+ };
65
+ const NEIGHBOURS = {
66
+ east: ["bc01fg45238967deuvhjyznpkmstqrwx", "bc01fg45238967deuvhjyznpkmstqrwx"],
67
+ north: ["p0r21436x8zb9dcf5h7kjnmqesgutwvy", "bc01fg45238967deuvhjyznpkmstqrwx"],
68
+ south: ["14365h7k9dcfesgujnmqp0r2twvyx8zb", "238967debc01fg45kmstqrwxuvhjyznp"],
69
+ west: ["238967debc01fg45kmstqrwxuvhjyznp", "238967debc01fg45kmstqrwxuvhjyznp"]
70
+ };
71
+ const BORDERS = {
72
+ east: ["bcfguvyz", "prxz"],
73
+ north: ["prxz", "bcfguvyz"],
74
+ south: ["028b", "0145hjnp"],
75
+ west: ["0145hjnp", "028b"]
76
+ };
77
+ const adjacent = (hash, direction) => {
78
+ const lower = hash.toLowerCase();
79
+ const lastChar = lower.at(-1) ?? "";
80
+ let base = lower.slice(0, -1);
81
+ const type = lower.length % 2 === 0 ? 0 : 1;
82
+ if (BORDERS[direction][type].includes(lastChar) && base !== "") {
83
+ base = adjacent(base, direction);
84
+ }
85
+ return base + (BASE32[NEIGHBOURS[direction][type].indexOf(lastChar)] ?? "");
86
+ };
87
+ const coveringGeohashes = (center, radiusMeters) => {
88
+ const precision = precisionForRadius(Math.max(radiusMeters, 1));
89
+ const origin = encodeGeohash(center, precision);
90
+ const north = adjacent(origin, "north");
91
+ const south = adjacent(origin, "south");
92
+ const cells = [
93
+ origin,
94
+ north,
95
+ south,
96
+ adjacent(origin, "east"),
97
+ adjacent(origin, "west"),
98
+ adjacent(north, "east"),
99
+ adjacent(north, "west"),
100
+ adjacent(south, "east"),
101
+ adjacent(south, "west")
102
+ ];
103
+ return [...new Set(cells)];
104
+ };
105
+ const pointInBoundingBox = (point, box) => point.lat >= box.sw.lat && point.lat <= box.ne.lat && point.lng >= box.sw.lng && point.lng <= box.ne.lng;
106
+ const boundingBoxCenter = (box) => {
107
+ return { lat: (box.sw.lat + box.ne.lat) / 2, lng: (box.sw.lng + box.ne.lng) / 2 };
108
+ };
109
+ const boundingBoxGeohashes = (box) => {
110
+ const center = boundingBoxCenter(box);
111
+ return coveringGeohashes(center, Math.max(haversineMeters(center, box.ne), 1));
112
+ };
113
+
114
+ export { GEO_DEFAULT_PRECISION, boundingBoxCenter, boundingBoxGeohashes, coveringGeohashes, encodeGeohash, haversineMeters, pointInBoundingBox };
@@ -6,8 +6,9 @@ import { mergeWhere, CountRlsUnsupportedError, selectIndexForGroupBy, selectInde
6
6
  import { appendCdcChange } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
7
7
  export { CDC_LOG_TABLE, applyCdcChanges, bumpCdcEpoch, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
8
8
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
9
- import { i as isFtsAvailable, D as DOC_COLUMN$1, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT, d as aggUpsertSql, j as jsonPathSql, q as quoteIdentifier, t as tableColumns, e as qualifiedJsonPathSql } from './do-sql-BCHCWtrD.mjs';
9
+ import { i as isFtsAvailable, D as DOC_COLUMN$1, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT, g as geoTableName, d as aggUpsertSql, j as jsonPathSql, q as quoteIdentifier, t as tableColumns, e as qualifiedJsonPathSql } from './do-sql-CGAgiQUz.mjs';
10
10
  import { param } from './renderSql-D6eUcn2N.mjs';
11
+ import { encodeGeohash, GEO_DEFAULT_PRECISION, coveringGeohashes, boundingBoxGeohashes, pointInBoundingBox, haversineMeters } from './GEO_DEFAULT_PRECISION-BWnsNmpP.mjs';
11
12
  import { sortColumnName, matchesRankStaticWhere, encodePartitionKey, rankTableName, resolveRankPartition, RANK_TIEBREAK } from './RANK_TIEBREAK-CXhdcA1o.mjs';
12
13
  import { stringifySearchText, ftsTableName, tokenizeSearch, buildFtsMatch, scoreDocument } from './buildFtsMatch-BLEMawrp.mjs';
13
14
  import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
@@ -21,10 +22,10 @@ import { createSystemReader } from './createSystemReader-D12eNH13.mjs';
21
22
  import { ConflictError } from './ConflictError-CLoq37xH.mjs';
22
23
  import { runTriggers } from './hasTrigger-5N6_Fx0A.mjs';
23
24
  import { compileWhereSql } from './compileWhereSql-DE6yfRcQ.mjs';
24
- export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-DDoT-UUI.mjs';
25
+ export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-BGTwynwh.mjs';
25
26
  export { C as CLIENT_WATERMARK_TABLE, G as GLOBAL_SHAPE_SNAPSHOT_TABLE, I as IDEMPOTENCY_TABLE, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, e as deleteGlobalShapeSnapshotsForConnection, m as migrateClientWatermark, b as migrateGlobalShapeSnapshot, r as readClientWatermark, f as readGlobalShapeSnapshot, g as readIdempotent, t as trimIdempotent, w as writeGlobalShapeSnapshot, h as writeIdempotent } from './ctx-db-idempotency-BdcNpvY4.mjs';
26
- export { runShardMigrations } from './runShardMigrations-DFzx6Qld.mjs';
27
- export { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
27
+ export { runShardMigrations } from './runShardMigrations-BVax6rYu.mjs';
28
+ export { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-DTeFiHYS.mjs';
28
29
 
29
30
  const rankIndexFieldsUnchanged = (index, previous, next) => {
30
31
  const fields = [...index.partitionBy ?? [], ...index.sortBy.map((key) => key.field), ...index.where ? Object.keys(index.where) : []];
@@ -314,8 +315,28 @@ const createCompanionSync = (deps) => {
314
315
  }
315
316
  }
316
317
  };
318
+ const syncGeo = (tableName, id, document) => {
319
+ const indexes = schema.tables[tableName]?.geoIndexes;
320
+ if (!indexes || indexes.length === 0) {
321
+ return;
322
+ }
323
+ for (const index of indexes) {
324
+ const geoTable = geoTableName(tableName, index.name);
325
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(geoTable)} WHERE ${sql.identifier("__id__")} = ${id}`);
326
+ const point = document?.[index.field];
327
+ if (point !== null && typeof point === "object" && typeof point.lat === "number" && typeof point.lng === "number") {
328
+ const { lat, lng } = point;
329
+ const hash = encodeGeohash({ lat, lng }, index.precision ?? GEO_DEFAULT_PRECISION);
330
+ runDrizzle(
331
+ sql$1,
332
+ sql`INSERT INTO ${sql.identifier(geoTable)} (${sql.identifier("__id__")}, ${sql.identifier("__geohash__")}, ${sql.identifier("__lat__")}, ${sql.identifier("__lng__")}) VALUES (${id}, ${hash}, ${lat}, ${lng})`
333
+ );
334
+ }
335
+ }
336
+ };
317
337
  const syncCompanionsForInsert = (tableName, id, document) => {
318
338
  syncSearch(tableName, id, document);
339
+ syncGeo(tableName, id, document);
319
340
  syncAggregates(tableName, void 0, document);
320
341
  syncRanks(tableName, id, void 0, document);
321
342
  invalidateCache(tableName, id);
@@ -329,6 +350,7 @@ const createCompanionSync = (deps) => {
329
350
  ensureRankBackfilledForTable,
330
351
  syncAggregates,
331
352
  syncCompanionsForInsert,
353
+ syncGeo,
332
354
  syncRanks,
333
355
  syncSearch
334
356
  };
@@ -599,6 +621,123 @@ const searchViaScan = (sql$1, tableName, search, limit, scopeCondition) => {
599
621
  const docs = scored.map((entry) => entry.doc);
600
622
  return typeof limit === "number" ? docs.slice(0, Math.max(0, Math.floor(limit))) : docs;
601
623
  };
624
+ const createGeoBuilder = (geo, tableName) => {
625
+ const staged = geo;
626
+ const builder = {
627
+ near: (point, radiusMeters) => {
628
+ if (staged.within) {
629
+ throw new LunoraError("INTERNAL", `geo index "${staged.indexName}" on table "${tableName}": call .near() or .within(), not both`);
630
+ }
631
+ staged.near = { point: { lat: point.lat, lng: point.lng }, radiusMeters };
632
+ return builder;
633
+ },
634
+ within: (box) => {
635
+ if (staged.near) {
636
+ throw new LunoraError("INTERNAL", `geo index "${staged.indexName}" on table "${tableName}": call .near() or .within(), not both`);
637
+ }
638
+ staged.within = { ne: { lat: box.ne.lat, lng: box.ne.lng }, sw: { lat: box.sw.lat, lng: box.sw.lng } };
639
+ return builder;
640
+ }
641
+ };
642
+ return builder;
643
+ };
644
+ const readGeoPoint = (document, field) => {
645
+ const value = document[field];
646
+ if (value === null || typeof value !== "object") {
647
+ return void 0;
648
+ }
649
+ const { lat, lng } = value;
650
+ return typeof lat === "number" && typeof lng === "number" ? { lat, lng } : void 0;
651
+ };
652
+ const scoreGeoRow = (record, geo) => {
653
+ const point = readGeoPoint(record, geo.definition.field);
654
+ if (!point) {
655
+ return void 0;
656
+ }
657
+ const creationTime = typeof record["_creationTime"] === "number" ? record["_creationTime"] : 0;
658
+ if (geo.near) {
659
+ const distance = haversineMeters(geo.near.point, point);
660
+ return distance <= geo.near.radiusMeters ? { creationTime, distance } : void 0;
661
+ }
662
+ return pointInBoundingBox(point, geo.within) ? { creationTime, distance: 0 } : void 0;
663
+ };
664
+ const runGeoFetch = (sql$1, tableName, geo, limit, scopeCondition) => {
665
+ if (!geo.near && !geo.within) {
666
+ throw new LunoraError("INTERNAL", `geo index "${geo.indexName}" on table "${tableName}": call .near(point, radius) or .within(box)`);
667
+ }
668
+ const prefixes = geo.near ? coveringGeohashes(geo.near.point, geo.near.radiusMeters) : boundingBoxGeohashes(geo.within);
669
+ const geoTable = geoTableName(tableName, geo.indexName);
670
+ const prefixClauses = prefixes.map(
671
+ (prefix) => sql`(g.${sql.identifier("__geohash__")} >= ${prefix} AND g.${sql.identifier("__geohash__")} < ${`${prefix}{`})`
672
+ );
673
+ const whereClauses = [sql`(${sql.join(prefixClauses, sql` OR `)})`];
674
+ if (scopeCondition) {
675
+ whereClauses.push(scopeCondition);
676
+ }
677
+ const query = sql`SELECT m.id, m._creationTime, m.${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(geoTable)} g JOIN ${sql.identifier(tableName)} m ON m.id = g.${sql.identifier("__id__")} WHERE ${sql.join(whereClauses, sql` AND `)}`;
678
+ const rows = runDrizzle(sql$1, query).toArray();
679
+ const scored = [];
680
+ for (const row of rows) {
681
+ const record = rowToDocument(row);
682
+ const score = record ? scoreGeoRow(record, geo) : void 0;
683
+ if (record && score) {
684
+ scored.push({ creationTime: score.creationTime, distance: score.distance, doc: record });
685
+ }
686
+ }
687
+ scored.sort((a, b) => a.distance - b.distance || b.creationTime - a.creationTime);
688
+ const docs = scored.map((entry) => entry.doc);
689
+ return typeof limit === "number" ? docs.slice(0, Math.max(0, Math.floor(limit))) : docs;
690
+ };
691
+ const runGeoTerminal = (sql, tableName, stage, scopeCondition, limit) => {
692
+ const { geo } = stage;
693
+ if (!geo) {
694
+ throw new LunoraError("INTERNAL", "runGeoTerminal called without a staged geo query");
695
+ }
696
+ const filtered = stage.inMemoryFilters.length > 0;
697
+ const docs = runGeoFetch(sql, tableName, geo, filtered ? void 0 : limit, scopeCondition);
698
+ if (!filtered) {
699
+ return docs;
700
+ }
701
+ const result = [];
702
+ for (const record of docs) {
703
+ if (stage.inMemoryFilters.every((predicate) => predicate(record))) {
704
+ result.push(record);
705
+ if (typeof limit === "number" && result.length >= limit) {
706
+ break;
707
+ }
708
+ }
709
+ }
710
+ return result;
711
+ };
712
+ const runPlainFetch = (sql$1, tableName, stage, scopeCondition, orderClause, limit) => {
713
+ const whereClauses = [];
714
+ for (const condition of stage.sqlConditions) {
715
+ whereClauses.push(sql`${jsonPathSql(condition.field)} ${sql.raw(condition.comparator)} ${serializeSqlValue(condition.value)}`);
716
+ }
717
+ if (scopeCondition) {
718
+ whereClauses.push(scopeCondition);
719
+ }
720
+ let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
721
+ if (whereClauses.length > 0) {
722
+ query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
723
+ }
724
+ query = sql`${query} ORDER BY ${orderClause}`;
725
+ if (typeof limit === "number" && stage.inMemoryFilters.length === 0) {
726
+ query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
727
+ }
728
+ const rows = runDrizzle(sql$1, query).toArray();
729
+ const docs = [];
730
+ for (const row of rows) {
731
+ const record = rowToDocument(row);
732
+ if (record && stage.inMemoryFilters.every((predicate) => predicate(record))) {
733
+ docs.push(record);
734
+ if (typeof limit === "number" && docs.length >= limit) {
735
+ break;
736
+ }
737
+ }
738
+ }
739
+ return docs;
740
+ };
602
741
  const doWhereSqlStrategy = { fieldRef: jsonPathSql, serialize: serializeSqlValue };
603
742
  const makeRelationExistsSqlStrategy = (onRead) => {
604
743
  let aliasCounter = 0;
@@ -774,36 +913,10 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
774
913
  if (stage.search) {
775
914
  return runSearchFetch(limit);
776
915
  }
777
- const whereClauses = [];
778
- for (const condition of stage.sqlConditions) {
779
- whereClauses.push(sql`${jsonPathSql(condition.field)} ${sql.raw(condition.comparator)} ${serializeSqlValue(condition.value)}`);
780
- }
781
- if (scopeCondition) {
782
- whereClauses.push(scopeCondition);
916
+ if (stage.geo) {
917
+ return runGeoTerminal(sql$1, tableName, stage, scopeCondition, limit);
783
918
  }
784
- let query = sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN$1)} FROM ${sql.identifier(tableName)}`;
785
- if (whereClauses.length > 0) {
786
- query = sql`${query} WHERE ${sql.join(whereClauses, sql` AND `)}`;
787
- }
788
- query = sql`${query} ORDER BY ${buildOrderClause()}`;
789
- if (typeof limit === "number" && stage.inMemoryFilters.length === 0) {
790
- query = sql`${query} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit))))}`;
791
- }
792
- const rows = runDrizzle(sql$1, query).toArray();
793
- const docs = [];
794
- for (const row of rows) {
795
- const record = rowToDocument(row);
796
- if (!record) {
797
- continue;
798
- }
799
- if (stage.inMemoryFilters.every((predicate) => predicate(record))) {
800
- docs.push(record);
801
- if (typeof limit === "number" && docs.length >= limit) {
802
- break;
803
- }
804
- }
805
- }
806
- return docs;
919
+ return runPlainFetch(sql$1, tableName, stage, scopeCondition, buildOrderClause(), limit);
807
920
  };
808
921
  const reader = {
809
922
  // eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
@@ -828,6 +941,9 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
828
941
  if (stage.search) {
829
942
  throw new LunoraError("INTERNAL", "pagination is not supported on search queries; use .take(n) or .collect()");
830
943
  }
944
+ if (stage.geo) {
945
+ throw new LunoraError("INTERNAL", "pagination is not supported on geo queries; use .take(n) or .collect()");
946
+ }
831
947
  return paginateStage(sql$1, tableName, stage, options, scopeCondition);
832
948
  },
833
949
  // eslint-disable-next-line @typescript-eslint/require-await -- TableReaderLike returns Promises (the D1 twin awaits real I/O); the DO impl is synchronous over local SQLite
@@ -842,6 +958,20 @@ const buildReader = (sql$1, schema, tableName, onIndexUse = () => void 0) => {
842
958
  }
843
959
  return rows[0] ?? null;
844
960
  },
961
+ withGeoIndex(indexName, build) {
962
+ const definition = (tableDefinition.geoIndexes ?? []).find((index) => index.name === indexName);
963
+ if (!definition) {
964
+ throw new LunoraError("INTERNAL", `unknown geo index "${indexName}" on table "${tableName}"`);
965
+ }
966
+ onIndexUse(tableName, indexName, "geo");
967
+ const geoStage = { definition, indexName };
968
+ stage.geo = geoStage;
969
+ build(createGeoBuilder(geoStage, tableName));
970
+ if (!geoStage.near && !geoStage.within) {
971
+ throw new LunoraError("INTERNAL", `geo index "${indexName}" on table "${tableName}" requires a .near(point, radius) or .within(box) call`);
972
+ }
973
+ return reader;
974
+ },
845
975
  withIndex(indexName, range) {
846
976
  const definition = tableDefinition.indexes.find((index) => index.name === indexName);
847
977
  if (!definition) {
@@ -1090,6 +1220,7 @@ const createShardCtxDb = (options) => {
1090
1220
  ensureRankBackfilledForTable,
1091
1221
  syncAggregates,
1092
1222
  syncCompanionsForInsert,
1223
+ syncGeo,
1093
1224
  syncRanks,
1094
1225
  syncSearch
1095
1226
  } = createCompanionSync({
@@ -1291,6 +1422,7 @@ const createShardCtxDb = (options) => {
1291
1422
  sql`UPDATE ${sql.identifier(tableName)} SET ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(merged)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
1292
1423
  );
1293
1424
  syncSearch(tableName, id, merged);
1425
+ syncGeo(tableName, id, void 0);
1294
1426
  syncAggregates(tableName, existing, merged);
1295
1427
  syncRanks(tableName, id, existing, void 0);
1296
1428
  cache?.invalidate(tableName, id);
@@ -1308,6 +1440,7 @@ const createShardCtxDb = (options) => {
1308
1440
  sql`DELETE FROM ${sql.identifier(tableName)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
1309
1441
  );
1310
1442
  syncSearch(tableName, id, void 0);
1443
+ syncGeo(tableName, id, void 0);
1311
1444
  syncAggregates(tableName, existing, void 0);
1312
1445
  syncRanks(tableName, id, existing, void 0);
1313
1446
  cache?.invalidate(tableName, id);
@@ -1682,6 +1815,7 @@ const createShardCtxDb = (options) => {
1682
1815
  sql`UPDATE ${sql.identifier(tableName)} SET ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(merged)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
1683
1816
  );
1684
1817
  syncSearch(tableName, id, merged);
1818
+ syncGeo(tableName, id, merged);
1685
1819
  syncAggregates(tableName, existing, merged);
1686
1820
  syncRanks(tableName, id, existing, merged);
1687
1821
  cache?.invalidate(tableName, id);
@@ -1878,6 +2012,7 @@ const createShardCtxDb = (options) => {
1878
2012
  sql`UPDATE ${sql.identifier(tableName)} SET _creationTime = ${creationTime}, ${sql.identifier(DOC_COLUMN$1)} = ${JSON.stringify(replaced)} WHERE id = ${id} AND ${sql.identifier(DOC_COLUMN$1)} = ${existingJson}`
1879
2013
  );
1880
2014
  syncSearch(tableName, id, replaced);
2015
+ syncGeo(tableName, id, replaced);
1881
2016
  syncAggregates(tableName, previous, replaced);
1882
2017
  syncRanks(tableName, id, previous, replaced);
1883
2018
  cache?.invalidate(tableName, id);