@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.
- package/dist/engine/client.d.ts +3 -0
- package/dist/engine/client.js +8 -0
- package/dist/engine/database.js +189 -35
- package/dist/engine/errors.d.ts +2 -2
- package/dist/engine/errors.js +1 -1
- package/dist/engine/live-aggregate.d.ts +12 -0
- package/dist/engine/live-aggregate.js +226 -0
- package/dist/engine/live-patch.d.ts +21 -0
- package/dist/engine/live-patch.js +31 -0
- package/dist/engine/live.d.ts +12 -0
- package/dist/engine/live.js +52 -49
- package/dist/engine/optimizer.js +11 -6
- package/dist/engine/query-cache.js +2 -13
- package/dist/engine/query-generations.d.ts +8 -0
- package/dist/engine/query-generations.js +61 -0
- package/dist/engine/query-identity.d.ts +3 -0
- package/dist/engine/query-identity.js +41 -0
- package/dist/engine/query.d.ts +6 -11
- package/dist/engine/query.js +294 -428
- package/dist/engine/sql-domains.d.ts +4 -0
- package/dist/engine/sql-domains.js +121 -0
- package/dist/engine/sql-semantics.js +7 -4
- package/dist/engine/typed-live.js +28 -20
- package/dist/engine/windows.d.ts +12 -0
- package/dist/engine/windows.js +386 -0
- package/dist/plan/model.d.ts +1 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +6 -1
- package/sql-feature-matrix.json +20 -27
package/dist/engine/client.d.ts
CHANGED
|
@@ -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>;
|
package/dist/engine/client.js
CHANGED
|
@@ -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();
|
package/dist/engine/database.js
CHANGED
|
@@ -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 {
|
|
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|${
|
|
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 =
|
|
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
|
|
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(
|
|
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 (
|
|
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 =
|
|
3894
|
-
for (const
|
|
3895
|
-
const
|
|
3896
|
-
|
|
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
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
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}
|
|
8351
|
+
parts.push(`${table.id}:${String(table.revision)}`);
|
|
8214
8352
|
}
|
|
8215
8353
|
try {
|
|
8216
|
-
|
|
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
|
};
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -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;
|
package/dist/engine/errors.js
CHANGED
|
@@ -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
|
+
}
|