@minnowdb/core 0.7.10 → 0.8.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.
@@ -1,3 +1,4 @@
1
+ import { type LiveQueryPatchOptions } from "./live-patch.js";
1
2
  import { type CompactionJobRecord, type GarbageCollectionJobRecord, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult } from "../storage/types.js";
2
3
  import { type BatchRow } from "./batch.js";
3
4
  import type { Catalog } from "./catalog.js";
@@ -264,6 +265,8 @@ export declare class ClientLiveQuerySet {
264
265
  constructor(client: MinnowDatabaseClient, handleId: string, created: Promise<unknown>);
265
266
  /** Registers a query (SQL or a compiled-plan envelope) and re-runs it on relevant changes. */
266
267
  subscribe(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<ClientLiveSubscription>;
268
+ /** Delivers changed row payloads; the worker transport still sends its result snapshot. */
269
+ subscribePatches(query: LiveQueryInput, options: LiveQueryPatchOptions): Promise<ClientLiveSubscription>;
267
270
  /** Registers dependency observation while leaving execution/result mapping to an adapter. */
268
271
  observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<ClientLiveSubscription>;
269
272
  stats(): Promise<LiveQueryStats>;
@@ -1,3 +1,4 @@
1
+ import { createLiveQueryPatch } from "./live-patch.js";
1
2
  import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, PostingBuildConflictError, SnapshotManifestMissingError, SnapshotImportConflictError, SchemaConflictError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, StorageCorruptionError, StorageFormatVersionError, OpfsUncertainOutcomeError } from "../storage/types.js";
2
3
  import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
3
4
  import { parseRpcResponse, MAX_DATABASE_RPC_IN_FLIGHT, protocolVersion } from "../worker-protocol/index.js";
@@ -683,6 +684,13 @@ class ClientLiveQuerySet {
683
684
  throw error;
684
685
  }
685
686
  }
687
+ subscribePatches(query, options) {
688
+ return this.subscribe(query, {
689
+ onChange: (result, delivery) => options.onPatch(createLiveQueryPatch(result, delivery), delivery),
690
+ ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
691
+ ...options.onComplete === void 0 ? {} : { onComplete: options.onComplete.bind(options) }
692
+ });
693
+ }
686
694
  async observe(query, options) {
687
695
  await this.#created;
688
696
  const subscriptionId = crypto.randomUUID();
@@ -1,3 +1,6 @@
1
+ import { LiveAggregate } from "./live-aggregate.js";
2
+ import { QueryGenerations } from "./query-generations.js";
3
+ import { applyWindowFunctionsAsync } from "./windows.js";
1
4
  import { crossJoinPlan } from "../plan/model.js";
2
5
  import { definedVectors, toColumnarBatch } from "./batch.js";
3
6
  import { ArtifactCache } from "./artifact-cache.js";
@@ -16,7 +19,7 @@ import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize
16
19
  import { boundedMaintenanceBatchItems, simpleDataTypes, floorWholeNumberProduct, BlockReadBatchTooLargeError, validateColumnDefault, validateEnumValues, secondaryIndexColumnIds, secondaryIndexDirections, secondaryUniqueKeyNamespace, CompactionJobConflictError, CompactionBacklogError, GarbageCollectionJobConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_INDEXED_STRING_CHARACTERS, MAX_CATALOG_NAME_CHARACTERS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MAINTENANCE_BATCH_ITEMS, MAX_STORAGE_BULK_READ_ITEMS, MAX_BLOCK_READ_BATCH_BYTES, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_POSTING_BUILD_TTL_MS, MAX_TEMP_OWNER_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, validateStorageId, SnapshotManifestMissingError, SchemaConflictError, TableInUseError, TableRecordConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError } from "../storage/types.js";
17
20
  import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity } from "../storage/snapshot.js";
18
21
  import { Snapshot, TransactionManager } from "../transactions/index.js";
19
- import { applyWindowFunctions, bindPendingSelectShapes, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDeferredInsertExpression, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, forEachNestedBlock, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, volatileScalarFunctionNames, annotatePlanIntegerDivision, clonePlanTree, planMayHaveIntegerDivision, foldIdentifierCase, extendGroupByWithKeyDependents, expandRowReferences } from "./query.js";
22
+ import { bindPendingSelectShapes, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDeferredInsertExpression, isDefaultInsertValue, referencedColumns, childExpressions, mapChildExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, forEachNestedBlock, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, volatileScalarFunctionNames, annotatePlanIntegerDivision, clonePlanTree, planMayHaveIntegerDivision, foldIdentifierCase, extendGroupByWithKeyDependents, expandRowReferences } from "./query.js";
20
23
  import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES } from "./query-cache.js";
21
24
  import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES } from "./memory.js";
22
25
  import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE } from "./live.js";
@@ -576,6 +579,7 @@ class MinnowDatabase {
576
579
  #liveSets = /* @__PURE__ */ new Set();
577
580
  #liveProofContexts = /* @__PURE__ */ new Map();
578
581
  #artifactCache;
582
+ #queryGenerations;
579
583
  #queryExecutionMemoryBudgetBytes;
580
584
  #ftsAutoIndexRows;
581
585
  #autoCompact;
@@ -657,6 +661,7 @@ class MinnowDatabase {
657
661
  if (!Number.isSafeInteger(this.#transactionOwnerLeaseMs) || this.#transactionOwnerLeaseMs <= 0) {
658
662
  throw new RangeError("Transaction owner lease lifetime must be a positive whole number");
659
663
  }
664
+ this.#queryGenerations = new QueryGenerations((after, limit) => this.store.listManifestPage(after, limit));
660
665
  this.#artifactCache = new ArtifactCache(options.bufferPoolBytes ?? 64 * 1024 * 1024);
661
666
  const queryBudget = options.executionMemoryBudgetBytes;
662
667
  this.#queryExecutionMemoryBudgetBytes = queryBudget === null ? Number.MAX_SAFE_INTEGER : positiveWholeNumber(queryBudget ?? DEFAULT_QUERY_MEMORY_BUDGET_BYTES, "Query execution memory budget");
@@ -2748,6 +2753,12 @@ class MinnowDatabase {
2748
2753
  const expected = schema2?.[index];
2749
2754
  if (expected === void 0)
2750
2755
  return;
2756
+ if (column.unknown === true)
2757
+ return;
2758
+ if (expected.unknown === true) {
2759
+ schema2 = schema2?.map((entry, position) => position === index ? { ...column, name: entry.name } : entry);
2760
+ return;
2761
+ }
2751
2762
  if (column.type === expected.type && column.sqlDomain?.kind === "numeric" && expected.sqlDomain?.kind === "numeric") {
2752
2763
  if (JSON.stringify(column.sqlDomain) !== JSON.stringify(expected.sqlDomain)) {
2753
2764
  schema2 = schema2?.map((entry, position) => position === index ? { ...entry, sqlDomain: { kind: "numeric" } } : entry);
@@ -2769,7 +2780,7 @@ class MinnowDatabase {
2769
2780
  if (source.windowed !== void 0) {
2770
2781
  const innerKey = await this.#blockResultCacheKey(source.windowed.block, snapshot, visibility, realTables, cacheResults);
2771
2782
  throwIfAborted(signal);
2772
- const columnarKey2 = innerKey === void 0 ? void 0 : `ctw|${JSON.stringify(source.windowed.windows)}|${innerKey}`;
2783
+ const columnarKey2 = innerKey === void 0 ? void 0 : `ctw|${planMemoKey(source.windowed.windows)}|${innerKey}`;
2773
2784
  if (columnarKey2 !== void 0) {
2774
2785
  const hit = this.#cacheGet(columnarKey2);
2775
2786
  if (hit !== void 0) {
@@ -2779,8 +2790,10 @@ class MinnowDatabase {
2779
2790
  }
2780
2791
  }
2781
2792
  const { result: inner, schema: innerSchema } = await this.#executeBlockWithSchemaCached(source.windowed.block, snapshot, visibility, memory, realTables, typedSchemas, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
2782
- const windowed = applyWindowFunctions(inner, source.windowed.windows, {
2783
- copyRows: cacheResults
2793
+ const windowed = await applyWindowFunctionsAsync(inner, source.windowed.windows, {
2794
+ copyRows: cacheResults,
2795
+ memoryContext: memory,
2796
+ ...signal === void 0 ? {} : { signal }
2784
2797
  });
2785
2798
  const schema2 = [
2786
2799
  ...innerSchema,
@@ -2978,7 +2991,16 @@ class MinnowDatabase {
2978
2991
  throwIfAborted(options.signal);
2979
2992
  before ??= await probe();
2980
2993
  throwIfAborted(options.signal);
2981
- const cached = this.#cacheGet(`${key}${String(before.catalogEpoch)}`);
2994
+ const dependencyKey = `deps ${String(before.schemaEpoch)} ${key}`;
2995
+ let dependencies = this.#cacheGet(dependencyKey);
2996
+ if (dependencies === void 0) {
2997
+ const rewritten = await this.#applyCatalogRewrites(plan, before);
2998
+ dependencies = [...(await this.#findRealBlockTables(rewritten)).values()].map((table) => table.id).sort();
2999
+ this.#cachePut(dependencyKey, dependencies, 64 + dependencies.reduce((bytes2, id) => bytes2 + id.length * 2 + 16, 0));
3000
+ }
3001
+ const generation = await this.#queryGenerations.key(dependencies, before.manifestVersion);
3002
+ const resultKey = `${key}${String(before.schemaEpoch)}${generation}`;
3003
+ const cached = this.#cacheGet(resultKey);
2982
3004
  if (cached !== void 0) {
2983
3005
  options.onStats?.({ peakMemoryBytes: 0 });
2984
3006
  return copyQueryResult(cached);
@@ -2992,7 +3014,7 @@ class MinnowDatabase {
2992
3014
  const after = await probe();
2993
3015
  throwIfAborted(options.signal);
2994
3016
  if (after.catalogEpoch === before.catalogEpoch) {
2995
- this.#cachePut(`${key}${String(before.catalogEpoch)}`, copyQueryResult(result), bytes);
3017
+ this.#cachePut(resultKey, copyQueryResult(result), bytes);
2996
3018
  }
2997
3019
  }
2998
3020
  return result;
@@ -3083,7 +3105,34 @@ class MinnowDatabase {
3083
3105
  if (plan.base.table !== DUAL_TABLE || plan.joins.length > 0) {
3084
3106
  throw new TypeError("NEXTVAL and CURRVAL currently require a SELECT without FROM");
3085
3107
  }
3086
- const resolved = structuredClone(plan);
3108
+ const resolved = resolveStatementDatetimes(structuredClone(plan), this.#now());
3109
+ const evaluate = (expression) => evaluateRowExpression(expression, DUAL_TABLE, {});
3110
+ const literal = (value) => ({
3111
+ kind: "literal",
3112
+ value,
3113
+ internalSqlValue: true
3114
+ });
3115
+ const omitted = resolved.limit === 0;
3116
+ const matches = !omitted && resolved.predicates.every((predicate) => evaluate({ kind: "condition", ...predicate }) === true);
3117
+ if (!omitted)
3118
+ resolved.predicates = matches ? [] : [
3119
+ {
3120
+ left: literal(false),
3121
+ operator: "=",
3122
+ right: literal(true)
3123
+ }
3124
+ ];
3125
+ const discarded = !matches;
3126
+ if (discarded) {
3127
+ const omit = (expression) => {
3128
+ if (expression.kind === "call" && sequenceFunctionNames.has(expression.name))
3129
+ return literal(0);
3130
+ return mapChildExpressions(expression, omit);
3131
+ };
3132
+ for (const item of resolved.select)
3133
+ item.expression = omit(item.expression);
3134
+ return resolved;
3135
+ }
3087
3136
  const rewrite = async (expression) => {
3088
3137
  if (expression.kind === "call" && (expression.name === "NEXTVAL" || expression.name === "CURRVAL")) {
3089
3138
  const argument = expression.arguments[0];
@@ -3107,6 +3156,21 @@ class MinnowDatabase {
3107
3156
  this.#sequenceCurrValues.set(name, value);
3108
3157
  return { kind: "literal", value };
3109
3158
  }
3159
+ if (expression.kind === "case") {
3160
+ for (const branch of expression.branches) {
3161
+ if (evaluate(await rewrite(branch.when)) === true)
3162
+ return rewrite(branch.then);
3163
+ }
3164
+ return expression.otherwise === void 0 ? literal(null) : rewrite(expression.otherwise);
3165
+ }
3166
+ if (expression.kind === "call" && expression.name === "COALESCE") {
3167
+ for (const argument of expression.arguments) {
3168
+ const value = evaluate(await rewrite(argument));
3169
+ if (value !== null)
3170
+ return literal(value);
3171
+ }
3172
+ return literal(null);
3173
+ }
3110
3174
  if (expression.kind === "binary" || expression.kind === "condition" || expression.kind === "logical") {
3111
3175
  expression.left = await rewrite(expression.left);
3112
3176
  expression.right = await rewrite(expression.right);
@@ -3124,13 +3188,6 @@ class MinnowDatabase {
3124
3188
  }
3125
3189
  } else if (expression.kind === "not") {
3126
3190
  expression.operand = await rewrite(expression.operand);
3127
- } else if (expression.kind === "case") {
3128
- for (const branch of expression.branches) {
3129
- branch.when = await rewrite(branch.when);
3130
- branch.then = await rewrite(branch.then);
3131
- }
3132
- if (expression.otherwise !== void 0)
3133
- expression.otherwise = await rewrite(expression.otherwise);
3134
3191
  }
3135
3192
  return expression;
3136
3193
  };
@@ -3757,14 +3814,14 @@ class MinnowDatabase {
3757
3814
  if (typeof query === "string")
3758
3815
  return this.query(query);
3759
3816
  if (query.kind === "typed-query")
3760
- return this.#queryCompiled(query.plan);
3817
+ return externalizeQueryResult(await this.#withReadReservation(() => this.#queryCompiled(query.plan)));
3761
3818
  return this.query(query.sql, { params: query.params });
3762
3819
  }
3763
3820
  const { probe, memoize } = context;
3764
3821
  if (typeof query !== "string" && query.kind === "typed-query") {
3765
3822
  const plan = query.plan;
3766
3823
  const memoizable = plan.usesStatementDatetime !== true && plan.usesVolatileFunctions !== true && plan.usesSequenceCalls !== true;
3767
- return this.#withReadReservation(() => memoizable ? this.#memoizedQuery(plan, `typed ${planMemoKey(plan)}`, {}, this.store.getCatalogProbe.bind(this.store), probe, memoize) : this.#queryCompiled(plan, {}, probe));
3824
+ return externalizeQueryResult(await this.#withReadReservation(() => memoizable ? this.#memoizedQuery(plan, `typed ${planMemoKey(plan)}`, {}, this.store.getCatalogProbe.bind(this.store), probe, memoize) : this.#queryCompiled(plan, {}, probe)));
3768
3825
  }
3769
3826
  if (typeof query === "string")
3770
3827
  return this.#queryWithReadReservation(query, {}, probe, memoize);
@@ -3776,9 +3833,49 @@ class MinnowDatabase {
3776
3833
  }
3777
3834
  const plan = await this.#applyCatalogRewrites(compiled, probe);
3778
3835
  const base = plan.base;
3779
- if (plan.joins.length > 0 || base.derived !== void 0 || base.union !== void 0 || base.recursive !== void 0 || base.windowed !== void 0 || plan.pendingSelectShape !== void 0 || plan.distinctWildcard === true || plan.groupBy.length > 0 || plan.having.length > 0 || plan.limitParameter !== void 0 || plan.offsetParameter !== void 0 || planReadsBeyondSingleScan(plan) || planContainsFts(plan)) {
3836
+ if (base.table === DUAL_TABLE || base.derived !== void 0 || base.union !== void 0 || base.recursive !== void 0 || base.windowed !== void 0 || plan.pendingSelectShape !== void 0 || plan.distinctWildcard === true || plan.limitParameter !== void 0 || plan.offsetParameter !== void 0 || planReadsBeyondSingleScan(plan) || planContainsFts(plan)) {
3780
3837
  return void 0;
3781
3838
  }
3839
+ if (plan.joins.length > 0) {
3840
+ const join = plan.joins[0];
3841
+ if (plan.joins.length !== 1 || join === void 0 || !["inner", "left"].includes(join.kind) || join.on !== void 0 || join.derived !== void 0 || join.union !== void 0 || join.windowed !== void 0 || join.recursive !== void 0)
3842
+ return void 0;
3843
+ const lookup = await this.#findTable(join.table);
3844
+ const lookupKey = getUniqueKeyColumn(lookup);
3845
+ if (lookupKey === void 0 || lookupKey.sqlDomain !== void 0)
3846
+ return void 0;
3847
+ const lookupReference = `${join.alias}.${lookupKey.name}`;
3848
+ const joinedKey = [join.left, join.right].find((expression) => expression.kind === "column" && expression.reference === lookupReference);
3849
+ if (joinedKey === void 0)
3850
+ return void 0;
3851
+ }
3852
+ const table = await this.#findTable(base.table);
3853
+ const keyColumn = getUniqueKeyColumn(table);
3854
+ if (keyColumn === void 0 || keyColumn.hidden === true || keyColumn.sqlDomain !== void 0)
3855
+ return void 0;
3856
+ const aggregate = LiveAggregate.plan(plan, `${base.alias}.${keyColumn.name}`);
3857
+ if (aggregate !== void 0)
3858
+ return {
3859
+ tableId: table.id,
3860
+ keyColumnId: keyColumn.id,
3861
+ qualifiedKey: `${base.alias}.${keyColumn.name}`,
3862
+ fullPlan: aggregate.inputPlan,
3863
+ deltaPlan: aggregate.inputPlan,
3864
+ publicColumns: plan.select.map((item) => item.alias),
3865
+ columnDomains: [],
3866
+ orderTerms: [],
3867
+ limit: void 0,
3868
+ offset: 0,
3869
+ margin: 0,
3870
+ complete: true,
3871
+ rows: [],
3872
+ keys: [],
3873
+ positions: /* @__PURE__ */ new Map(),
3874
+ order: [],
3875
+ aggregate
3876
+ };
3877
+ if (plan.groupBy.length > 0 || plan.having.length > 0)
3878
+ return void 0;
3782
3879
  const rowLocal = (expression) => {
3783
3880
  if (expression.kind === "subquery" || expression.kind === "exists" || expression.kind === "window" || expression.kind === "wildcard" || expression.kind === "parameter" || hasAggregate(expression)) {
3784
3881
  return false;
@@ -3792,11 +3889,6 @@ class MinnowDatabase {
3792
3889
  }
3793
3890
  if (!plan.orderBy.every((term) => rowLocal(term.expression)))
3794
3891
  return void 0;
3795
- const table = await this.#findTable(base.table);
3796
- const keyColumn = getUniqueKeyColumn(table);
3797
- if (keyColumn === void 0 || keyColumn.hidden === true || keyColumn.sqlDomain !== void 0) {
3798
- return void 0;
3799
- }
3800
3892
  const publicColumns = plan.select.map((item) => item.alias);
3801
3893
  if (publicColumns.some((name) => name.startsWith(LIVE_HIDDEN_PREFIX)))
3802
3894
  return void 0;
@@ -3848,6 +3940,7 @@ class MinnowDatabase {
3848
3940
  complete: true,
3849
3941
  rows: [],
3850
3942
  keys: [],
3943
+ positions: /* @__PURE__ */ new Map(),
3851
3944
  order: []
3852
3945
  };
3853
3946
  }
@@ -3856,6 +3949,19 @@ class MinnowDatabase {
3856
3949
  const state = await this.#liveMaintenancePlan(compiled, probe);
3857
3950
  if (state === void 0)
3858
3951
  return void 0;
3952
+ if (state.aggregate !== void 0) {
3953
+ try {
3954
+ const input = await this.#withReadReservation(() => this.#queryCompiled(state.fullPlan, {}, probe));
3955
+ const aggregate = state.aggregate.patch(input, /* @__PURE__ */ new Set(), liveKeyToken);
3956
+ return {
3957
+ result: aggregate.result(),
3958
+ state: { ...state, aggregate },
3959
+ retainedBytes: aggregate.retainedBytes
3960
+ };
3961
+ } catch {
3962
+ return void 0;
3963
+ }
3964
+ }
3859
3965
  let executed;
3860
3966
  try {
3861
3967
  executed = externalizeQueryResult(await this.#withReadReservation(() => this.#queryCompiled(state.fullPlan, {}, probe)));
@@ -3889,12 +3995,40 @@ class MinnowDatabase {
3889
3995
  const { changedKeys, existingKeys } = changed;
3890
3996
  if (changedKeys.size === 0)
3891
3997
  return { result: retained, state, changed: false };
3998
+ if (state.aggregate !== void 0) {
3999
+ const deltaPlan2 = {
4000
+ ...state.deltaPlan,
4001
+ predicates: [
4002
+ ...state.deltaPlan.predicates,
4003
+ {
4004
+ left: { kind: "column", reference: state.qualifiedKey },
4005
+ operator: "IN",
4006
+ right: {
4007
+ kind: "list",
4008
+ items: [...changedKeys.values()].map((value) => ({ kind: "literal", value }))
4009
+ }
4010
+ }
4011
+ ]
4012
+ };
4013
+ const input = await this.#withReadReservation(() => this.#queryCompiled(deltaPlan2, {}, probe));
4014
+ const aggregate = state.aggregate.patch(input, new Set(changedKeys.keys()), liveKeyToken);
4015
+ const result = aggregate.result();
4016
+ const changed2 = result.rows.length !== retained.rows.length || result.rows.some((row, index) => {
4017
+ const previous = retained.rows[index];
4018
+ return previous === void 0 || !sameLiveRow(previous, row, result.columns);
4019
+ });
4020
+ return {
4021
+ result,
4022
+ state: { ...state, aggregate },
4023
+ retainedBytes: aggregate.retainedBytes,
4024
+ changed: changed2
4025
+ };
4026
+ }
3892
4027
  const affected = /* @__PURE__ */ new Set();
3893
- const retainedTokens = /* @__PURE__ */ new Set();
3894
- for (const [index, key] of state.keys.entries()) {
3895
- const token = liveKeyToken(key);
3896
- retainedTokens.add(token);
3897
- if (changedKeys.has(token))
4028
+ const retainedTokens = state.positions;
4029
+ for (const token of changedKeys.keys()) {
4030
+ const index = retainedTokens.get(token);
4031
+ if (index !== void 0)
3898
4032
  affected.add(index);
3899
4033
  }
3900
4034
  const fullLimit = state.limit === void 0 ? void 0 : state.limit + state.margin;
@@ -5423,11 +5557,15 @@ class MinnowDatabase {
5423
5557
  set.notifyLocalCommit();
5424
5558
  }
5425
5559
  async run(query) {
5426
- const probe = this.store.getCatalogProbe.bind(this.store);
5427
- if (query.plan.usesStatementDatetime === true || query.plan.usesVolatileFunctions === true) {
5428
- return (await this.#queryCompiled(query.plan)).rows;
5429
- }
5430
- return (await this.#memoizedQuery(query.plan, `typed ${planMemoKey(query.plan)}`, {}, probe)).rows;
5560
+ await this.#settleExpiredStatementTransaction();
5561
+ const open = this.#openTransaction;
5562
+ const plan = query.plan;
5563
+ const execute = async () => {
5564
+ const probe = this.store.getCatalogProbe.bind(this.store);
5565
+ return plan.usesStatementDatetime === true || plan.usesVolatileFunctions === true || plan.usesSequenceCalls === true ? this.#queryCompiled(plan) : this.#memoizedQuery(plan, `typed ${planMemoKey(plan)}`, {}, probe);
5566
+ };
5567
+ const result = open === void 0 ? await this.#withReadReservation(execute) : await this.#duringTransaction(open, () => open.session.queryPlan(plan));
5568
+ return externalizeQueryResult(result).rows;
5431
5569
  }
5432
5570
  async migrate(definition = this.#schema, options = {}) {
5433
5571
  if (definition === void 0) {
@@ -8210,10 +8348,12 @@ ${notes.map((note) => `-- ${note}`).join("\n")}`;
8210
8348
  const table = realTables.get(name);
8211
8349
  if (table === void 0)
8212
8350
  return void 0;
8213
- parts.push(`${table.id}=${String(snapshot.version ?? -1)}`);
8351
+ parts.push(`${table.id}:${String(table.revision)}`);
8214
8352
  }
8215
8353
  try {
8216
- return `blk\0${parts.join(";")}\0${JSON.stringify(block)}`;
8354
+ const ids = collectRealTableNames(block).sort().map((name) => realTables.get(name)?.id ?? name);
8355
+ const generation = await this.#queryGenerations.key(ids, snapshot.version);
8356
+ return `blk\0${parts.join(";")}\0${generation}\0${planMemoKey(block)}`;
8217
8357
  } catch {
8218
8358
  return void 0;
8219
8359
  }
@@ -14489,6 +14629,18 @@ function splitLiveHiddenColumns(executed, state) {
14489
14629
  };
14490
14630
  }
14491
14631
  function liveMaintainedOutcome(state, previous, previousIndex) {
14632
+ const positions = /* @__PURE__ */ new Map();
14633
+ let retainedBytes = 128 + planMemoKey(state.fullPlan).length * 2 + planMemoKey(state.deltaPlan).length * 2;
14634
+ for (const [index, key] of state.keys.entries()) {
14635
+ const token = liveKeyToken(key);
14636
+ positions.set(token, index);
14637
+ retainedBytes += 64 + token.length * 2;
14638
+ }
14639
+ for (const row of state.rows)
14640
+ retainedBytes += 48 + estimateValuesBytes(Object.values(row)) * 2;
14641
+ for (const values of state.order)
14642
+ retainedBytes += values.length * 8 + estimateValuesBytes(values) * 2;
14643
+ state = { ...state, positions };
14492
14644
  const visibleCount = state.limit === void 0 ? state.rows.length : Math.min(state.rows.length, state.limit);
14493
14645
  const visible = state.rows.slice(0, visibleCount);
14494
14646
  if (previous === void 0) {
@@ -14499,6 +14651,7 @@ function liveMaintainedOutcome(state, previous, previousIndex) {
14499
14651
  rows: visible
14500
14652
  },
14501
14653
  state,
14654
+ retainedBytes,
14502
14655
  changed: true
14503
14656
  };
14504
14657
  }
@@ -14526,7 +14679,7 @@ function liveMaintainedOutcome(state, previous, previousIndex) {
14526
14679
  same = false;
14527
14680
  }
14528
14681
  if (same)
14529
- return { result: previous, state, changed: false };
14682
+ return { result: previous, state, retainedBytes, changed: false };
14530
14683
  return {
14531
14684
  result: {
14532
14685
  columns: [...state.publicColumns],
@@ -14534,6 +14687,7 @@ function liveMaintainedOutcome(state, previous, previousIndex) {
14534
14687
  rows: visible
14535
14688
  },
14536
14689
  state,
14690
+ retainedBytes,
14537
14691
  changed: true,
14538
14692
  ...kept > 0 ? { retained } : {}
14539
14693
  };
@@ -17,10 +17,10 @@ export declare class DatabaseReadBacklogError extends Error {
17
17
  }
18
18
  /** A live-query owner reached one of its documented resident-resource ceilings. */
19
19
  export declare class LiveQueryLimitError extends Error {
20
- readonly resource: "set" | "group" | "subscription";
20
+ readonly resource: "set" | "group" | "subscription" | "byte";
21
21
  readonly limit: number;
22
22
  readonly name = "LiveQueryLimitError";
23
- constructor(resource: "set" | "group" | "subscription", limit: number);
23
+ constructor(resource: "set" | "group" | "subscription" | "byte", limit: number);
24
24
  }
25
25
  export declare class UniqueConstraintError extends Error {
26
26
  readonly tableName: string;
@@ -22,7 +22,7 @@ class LiveQueryLimitError extends Error {
22
22
  limit;
23
23
  name = "LiveQueryLimitError";
24
24
  constructor(resource, limit) {
25
- super(resource === "set" ? `A database cannot retain more than ${String(limit)} live-query sets` : `A live-query set cannot retain more than ${String(limit)} ${resource} records`);
25
+ super(resource === "set" ? `A database cannot retain more than ${String(limit)} live-query sets` : resource === "byte" ? `A live-query set cannot retain more than ${String(limit)} modeled bytes` : `A live-query set cannot retain more than ${String(limit)} ${resource} records`);
26
26
  this.resource = resource;
27
27
  this.limit = limit;
28
28
  }
@@ -0,0 +1,12 @@
1
+ import type { CompiledQuery, QueryResult, QueryValue } from "../plan/model.js";
2
+ /** Single-table COUNT/SUM/AVG contributions; SQL still evaluates filters and arguments. */
3
+ export declare class LiveAggregate {
4
+ #private;
5
+ readonly inputPlan: CompiledQuery;
6
+ readonly keyAlias: string;
7
+ private constructor();
8
+ static plan(plan: CompiledQuery, qualifiedKey: string): LiveAggregate | undefined;
9
+ patch(result: QueryResult, changed: ReadonlySet<string>, token: (value: QueryValue) => string): LiveAggregate;
10
+ result(): QueryResult;
11
+ get retainedBytes(): number;
12
+ }