@minnowdb/core 0.7.8 → 0.7.10
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 +2 -2
- package/dist/engine/client.js +6 -4
- package/dist/engine/database.js +457 -18
- package/dist/engine/keyed-live.js +55 -18
- package/dist/engine/live.d.ts +95 -8
- package/dist/engine/live.js +311 -148
- package/dist/engine/typed-live.d.ts +12 -1
- package/dist/engine/typed-live.js +89 -12
- package/dist/engine/worker-server.js +12 -5
- package/dist/worker-protocol/index.d.ts +1 -1
- package/dist/worker-protocol/index.js +1 -1
- package/package.json +1 -1
package/dist/engine/client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type CompactionJobRecord, type GarbageCollectionJobRecord, type Storage
|
|
|
2
2
|
import { type BatchRow } from "./batch.js";
|
|
3
3
|
import type { Catalog } from "./catalog.js";
|
|
4
4
|
import type { BufferPoolStats, StagedWriteResult, StagedUpsertResult, BufferedFlushResult, BufferedWriterOptions, CancelCompactionJobResult, CollectGarbageOptions, CollectGarbageStepOptions, CompactTableOptions, CompactTableResult, CompactTableStepOptions, CompactionJobProgress, CreateTableInput, MigrateOptions, DeleteBatchResult, ExecuteOptions, ExecuteResult, GarbageCollectionProgress, GarbageCollectionResult, MaintenanceStatus, InsertBatchResult, QueryOptions, QueryExecutionStats, QueryCursorOptions, QuerySpillCleanupOptions, QuerySpillCleanupResult, RunStatementOptions, SnapshotExportOptions, SnapshotImportOptions, TableDefinition, UpdateBatchResult, UpsertBatchResult, VisibleSegmentPage, VisibleSegmentPageOptions } from "./database.js";
|
|
5
|
-
import type { LiveQueryInput, LiveQueryInvalidation, LiveQueryObserveOptions, LiveQueryStats, LiveQuerySubscribeOptions } from "./live.js";
|
|
5
|
+
import type { LiveQueryDelivery, LiveQueryInput, LiveQueryInvalidation, LiveQueryObserveOptions, LiveQueryStats, LiveQuerySubscribeOptions } from "./live.js";
|
|
6
6
|
import type { CompiledQuery, CompiledStatement, QueryResult, QueryValue } from "./query.js";
|
|
7
7
|
import type { AnySchema, UntypedSchema, AnyTable, BatchColumnName, BatchDeleteInput, BatchInsertInput, BatchInsertRow, BatchKeyValue, BatchReadOptions, BatchReadRow, BatchUpdateChanges, BatchUpdateInput, BatchUpsertOptions, SchemaDefinition, TableName } from "./schema.js";
|
|
8
8
|
import { type WireMigrationStep } from "./schema-wire.js";
|
|
@@ -65,7 +65,7 @@ export interface ClientMigrationResult {
|
|
|
65
65
|
steps: WireMigrationStep[];
|
|
66
66
|
}
|
|
67
67
|
interface EventRoute {
|
|
68
|
-
onChange?: (result: QueryResult) => void;
|
|
68
|
+
onChange?: (result: QueryResult, delivery: LiveQueryDelivery) => void;
|
|
69
69
|
onInvalidate?: (invalidation: LiveQueryInvalidation) => void;
|
|
70
70
|
onError?: (error: unknown) => void;
|
|
71
71
|
onComplete?: () => void;
|
package/dist/engine/client.js
CHANGED
|
@@ -556,9 +556,10 @@ class MinnowDatabaseClient {
|
|
|
556
556
|
const route = this.#events.get(response.handleId);
|
|
557
557
|
if (route === void 0)
|
|
558
558
|
return;
|
|
559
|
-
if (response.event === "change")
|
|
560
|
-
|
|
561
|
-
|
|
559
|
+
if (response.event === "change") {
|
|
560
|
+
const { result, delivery } = response.payload;
|
|
561
|
+
route.onChange?.(decodeQueryResult(result), delivery);
|
|
562
|
+
} else if (response.event === "invalidate") {
|
|
562
563
|
route.onInvalidate?.(response.payload);
|
|
563
564
|
} else if (response.event === "error") {
|
|
564
565
|
route.onError?.(rehydrateResponseError(response.payload));
|
|
@@ -698,7 +699,8 @@ class ClientLiveQuerySet {
|
|
|
698
699
|
try {
|
|
699
700
|
const created = await this.client._invoke(this.handleId, "observe", [
|
|
700
701
|
subscriptionId,
|
|
701
|
-
query
|
|
702
|
+
query,
|
|
703
|
+
{ suppressUnchanged: options.suppressUnchanged === true }
|
|
702
704
|
]);
|
|
703
705
|
this.#subscriptionIds.add(subscriptionId);
|
|
704
706
|
return new ClientLiveSubscription(this.client, subscriptionId, created.dependencyTableIds, () => this.#subscriptionIds.delete(subscriptionId), state);
|
package/dist/engine/database.js
CHANGED
|
@@ -21,7 +21,7 @@ import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBy
|
|
|
21
21
|
import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES } from "./memory.js";
|
|
22
22
|
import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE } from "./live.js";
|
|
23
23
|
import { chooseJoinOrder, optimizePlan, qualifyCorrelatedReferences, renderPlan } from "./optimizer.js";
|
|
24
|
-
import { encodeSqlEqualityValue, readUntypedText } from "./sql-semantics.js";
|
|
24
|
+
import { encodeSqlEqualityValue, readUntypedText, compareSqlValues } from "./sql-semantics.js";
|
|
25
25
|
import { exactNumericAsNumber, exactNumericValue, externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue } from "./sql-domains.js";
|
|
26
26
|
import { toCatalog } from "./catalog.js";
|
|
27
27
|
import { applyColumnSteps, assertColumnDroppable, compileGeneratedColumnExpression, declaredForeignKeys, isDestructiveStep, planMigration } from "./schema.js";
|
|
@@ -1124,11 +1124,11 @@ class MinnowDatabase {
|
|
|
1124
1124
|
this.#activeReadReservations -= 1;
|
|
1125
1125
|
}
|
|
1126
1126
|
}
|
|
1127
|
-
async #queryWithReadReservation(sql, options) {
|
|
1127
|
+
async #queryWithReadReservation(sql, options, probe, storeMemo = true) {
|
|
1128
1128
|
throwIfAborted(options.signal);
|
|
1129
1129
|
await this.#settleExpiredStatementTransaction();
|
|
1130
1130
|
throwIfAborted(options.signal);
|
|
1131
|
-
return this.#openTransaction === void 0 ? this.#withReadReservation(() => this.#queryUnreserved(sql, options)) : this.#queryUnreserved(sql, options);
|
|
1131
|
+
return this.#openTransaction === void 0 ? this.#withReadReservation(() => this.#queryUnreserved(sql, options, probe, storeMemo)) : this.#queryUnreserved(sql, options, probe, storeMemo);
|
|
1132
1132
|
}
|
|
1133
1133
|
async #assistAutomaticCollection() {
|
|
1134
1134
|
if (!this.#autoCollect && !this.#manualCollectionDebtInitialized) {
|
|
@@ -2857,7 +2857,7 @@ class MinnowDatabase {
|
|
|
2857
2857
|
async query(sql, options = {}) {
|
|
2858
2858
|
return this.#queryWithReadReservation(sql, options);
|
|
2859
2859
|
}
|
|
2860
|
-
async #queryUnreserved(sql, options) {
|
|
2860
|
+
async #queryUnreserved(sql, options, probe, storeMemo = true) {
|
|
2861
2861
|
throwIfAborted(options.signal);
|
|
2862
2862
|
const open = this.#openTransaction;
|
|
2863
2863
|
if (open !== void 0) {
|
|
@@ -2883,9 +2883,9 @@ class MinnowDatabase {
|
|
|
2883
2883
|
}
|
|
2884
2884
|
}
|
|
2885
2885
|
const plan = bindPlanParameters(compiled, options.params);
|
|
2886
|
-
const
|
|
2886
|
+
const readProbe = this.store.getCatalogProbe.bind(this.store);
|
|
2887
2887
|
const memoizable = options.memoize !== false && cacheableQueryInput(sql, options.params ?? []) && plan.usesStatementDatetime !== true && plan.usesVolatileFunctions !== true && plan.usesSequenceCalls !== true && options.version === void 0 && options.executionMemoryBudgetBytes === void 0 && options.spillToStorage === void 0 && options.spillPageRows === void 0;
|
|
2888
|
-
const result = !memoizable ? await this.#queryCompiled(plan, options) : await this.#memoizedQuery(plan, `res ${queryResultMemoKey(sql, options.params ?? [])}`, options, probe);
|
|
2888
|
+
const result = !memoizable ? await this.#queryCompiled(plan, options, probe) : await this.#memoizedQuery(plan, `res ${queryResultMemoKey(sql, options.params ?? [])}`, options, readProbe, probe, storeMemo);
|
|
2889
2889
|
throwIfAborted(options.signal);
|
|
2890
2890
|
return externalizeQueryResult(result);
|
|
2891
2891
|
}
|
|
@@ -2974,9 +2974,9 @@ class MinnowDatabase {
|
|
|
2974
2974
|
});
|
|
2975
2975
|
}
|
|
2976
2976
|
}
|
|
2977
|
-
async #memoizedQuery(plan, key, options, probe) {
|
|
2977
|
+
async #memoizedQuery(plan, key, options, probe, before, store = true) {
|
|
2978
2978
|
throwIfAborted(options.signal);
|
|
2979
|
-
|
|
2979
|
+
before ??= await probe();
|
|
2980
2980
|
throwIfAborted(options.signal);
|
|
2981
2981
|
const cached = this.#cacheGet(`${key}${String(before.catalogEpoch)}`);
|
|
2982
2982
|
if (cached !== void 0) {
|
|
@@ -2985,6 +2985,8 @@ class MinnowDatabase {
|
|
|
2985
2985
|
}
|
|
2986
2986
|
const result = await this.#queryCompiled(plan, options, before);
|
|
2987
2987
|
throwIfAborted(options.signal);
|
|
2988
|
+
if (!store)
|
|
2989
|
+
return result;
|
|
2988
2990
|
const bytes = queryResultRetainedBytes(result);
|
|
2989
2991
|
if (bytes <= RESULT_MEMO_MAX_BYTES) {
|
|
2990
2992
|
const after = await probe();
|
|
@@ -3729,19 +3731,15 @@ class MinnowDatabase {
|
|
|
3729
3731
|
return this.store.getCatalogProbe();
|
|
3730
3732
|
},
|
|
3731
3733
|
manifestPage: (afterVersion, limit) => this.store.listManifestPage(afterVersion, limit),
|
|
3732
|
-
dependencyTableIds: async (query) => {
|
|
3734
|
+
dependencyTableIds: async (query, probe) => {
|
|
3733
3735
|
const compiled = compileLiveQuery(query);
|
|
3734
|
-
const plan = await this.#applyCatalogRewrites(compiled);
|
|
3736
|
+
const plan = await this.#applyCatalogRewrites(compiled, probe);
|
|
3735
3737
|
const tables = await this.#findRealBlockTables(plan);
|
|
3736
3738
|
return new Set([...tables.values()].map((record) => record.id));
|
|
3737
3739
|
},
|
|
3738
|
-
execute:
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
if (query.kind === "typed-query")
|
|
3742
|
-
return this.#queryCompiled(query.plan);
|
|
3743
|
-
return this.query(query.sql, { params: query.params });
|
|
3744
|
-
},
|
|
3740
|
+
execute: (query, context) => this.#liveExecute(query, context),
|
|
3741
|
+
executeMaintainable: (query, context) => this.#liveExecuteMaintainable(compileLiveQuery(query), context),
|
|
3742
|
+
maintain: (_query, result, state, tableIds, after, until, probe) => this.#liveMaintain(result, state, tableIds, after, until, probe),
|
|
3745
3743
|
changeCanAffect: (query, tableIds, after, until) => this.#liveChangeCanAffect(query, tableIds, after, until)
|
|
3746
3744
|
}, {
|
|
3747
3745
|
...options,
|
|
@@ -3754,6 +3752,205 @@ class MinnowDatabase {
|
|
|
3754
3752
|
this.#liveSets.add(set);
|
|
3755
3753
|
return set;
|
|
3756
3754
|
}
|
|
3755
|
+
async #liveExecute(query, context) {
|
|
3756
|
+
if (context === void 0) {
|
|
3757
|
+
if (typeof query === "string")
|
|
3758
|
+
return this.query(query);
|
|
3759
|
+
if (query.kind === "typed-query")
|
|
3760
|
+
return this.#queryCompiled(query.plan);
|
|
3761
|
+
return this.query(query.sql, { params: query.params });
|
|
3762
|
+
}
|
|
3763
|
+
const { probe, memoize } = context;
|
|
3764
|
+
if (typeof query !== "string" && query.kind === "typed-query") {
|
|
3765
|
+
const plan = query.plan;
|
|
3766
|
+
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));
|
|
3768
|
+
}
|
|
3769
|
+
if (typeof query === "string")
|
|
3770
|
+
return this.#queryWithReadReservation(query, {}, probe, memoize);
|
|
3771
|
+
return this.#queryWithReadReservation(query.sql, { params: query.params }, probe, memoize);
|
|
3772
|
+
}
|
|
3773
|
+
async #liveMaintenancePlan(compiled, probe) {
|
|
3774
|
+
if (compiled.usesStatementDatetime === true || compiled.usesVolatileFunctions === true || compiled.usesSequenceCalls === true) {
|
|
3775
|
+
return void 0;
|
|
3776
|
+
}
|
|
3777
|
+
const plan = await this.#applyCatalogRewrites(compiled, probe);
|
|
3778
|
+
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)) {
|
|
3780
|
+
return void 0;
|
|
3781
|
+
}
|
|
3782
|
+
const rowLocal = (expression) => {
|
|
3783
|
+
if (expression.kind === "subquery" || expression.kind === "exists" || expression.kind === "window" || expression.kind === "wildcard" || expression.kind === "parameter" || hasAggregate(expression)) {
|
|
3784
|
+
return false;
|
|
3785
|
+
}
|
|
3786
|
+
return childExpressions(expression).every(rowLocal);
|
|
3787
|
+
};
|
|
3788
|
+
if (!plan.select.every((item) => rowLocal(item.expression)))
|
|
3789
|
+
return void 0;
|
|
3790
|
+
if (!plan.predicates.every(({ left, right }) => rowLocal(left) && rowLocal(right))) {
|
|
3791
|
+
return void 0;
|
|
3792
|
+
}
|
|
3793
|
+
if (!plan.orderBy.every((term) => rowLocal(term.expression)))
|
|
3794
|
+
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
|
+
const publicColumns = plan.select.map((item) => item.alias);
|
|
3801
|
+
if (publicColumns.some((name) => name.startsWith(LIVE_HIDDEN_PREFIX)))
|
|
3802
|
+
return void 0;
|
|
3803
|
+
const fullPlan = clonePlanTree(plan);
|
|
3804
|
+
const qualifiedKey = `${base.alias}.${keyColumn.name}`;
|
|
3805
|
+
fullPlan.select.push({
|
|
3806
|
+
expression: { kind: "column", reference: qualifiedKey },
|
|
3807
|
+
alias: LIVE_KEY_ALIAS
|
|
3808
|
+
});
|
|
3809
|
+
const orderTerms = [];
|
|
3810
|
+
for (const [index, term] of plan.orderBy.entries()) {
|
|
3811
|
+
let alias;
|
|
3812
|
+
if (term.expression.kind === "column" && !term.expression.reference.includes(".")) {
|
|
3813
|
+
const output = term.expression.reference;
|
|
3814
|
+
if (publicColumns.includes(output))
|
|
3815
|
+
alias = output;
|
|
3816
|
+
} else if (term.expression.kind === "literal" && typeof term.expression.value === "number") {
|
|
3817
|
+
const position = term.expression.value;
|
|
3818
|
+
if (!Number.isInteger(position) || position < 1 || position > publicColumns.length) {
|
|
3819
|
+
return void 0;
|
|
3820
|
+
}
|
|
3821
|
+
alias = publicColumns[position - 1];
|
|
3822
|
+
}
|
|
3823
|
+
if (alias === void 0) {
|
|
3824
|
+
alias = `${LIVE_ORDER_ALIAS}${String(index)}`;
|
|
3825
|
+
fullPlan.select.push({ expression: clonePlanTree(term.expression), alias });
|
|
3826
|
+
}
|
|
3827
|
+
orderTerms.push({ alias, descending: term.direction === "desc", nulls: term.nulls });
|
|
3828
|
+
}
|
|
3829
|
+
const deltaPlan = clonePlanTree(fullPlan);
|
|
3830
|
+
deltaPlan.orderBy = [];
|
|
3831
|
+
delete deltaPlan.limit;
|
|
3832
|
+
delete deltaPlan.offset;
|
|
3833
|
+
const margin = plan.limit === void 0 || orderTerms.length === 0 ? 0 : Math.max(LIVE_WINDOW_MARGIN_MIN, Math.min(plan.limit, LIVE_WINDOW_MARGIN_MAX));
|
|
3834
|
+
if (margin > 0)
|
|
3835
|
+
fullPlan.limit = (plan.limit ?? 0) + margin;
|
|
3836
|
+
return {
|
|
3837
|
+
tableId: table.id,
|
|
3838
|
+
keyColumnId: keyColumn.id,
|
|
3839
|
+
qualifiedKey,
|
|
3840
|
+
fullPlan,
|
|
3841
|
+
deltaPlan,
|
|
3842
|
+
publicColumns,
|
|
3843
|
+
columnDomains: [],
|
|
3844
|
+
orderTerms,
|
|
3845
|
+
limit: plan.limit,
|
|
3846
|
+
offset: plan.offset ?? 0,
|
|
3847
|
+
margin,
|
|
3848
|
+
complete: true,
|
|
3849
|
+
rows: [],
|
|
3850
|
+
keys: [],
|
|
3851
|
+
order: []
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
async #liveExecuteMaintainable(compiled, context) {
|
|
3855
|
+
const probe = context?.probe ?? await this.store.getCatalogProbe();
|
|
3856
|
+
const state = await this.#liveMaintenancePlan(compiled, probe);
|
|
3857
|
+
if (state === void 0)
|
|
3858
|
+
return void 0;
|
|
3859
|
+
let executed;
|
|
3860
|
+
try {
|
|
3861
|
+
executed = externalizeQueryResult(await this.#withReadReservation(() => this.#queryCompiled(state.fullPlan, {}, probe)));
|
|
3862
|
+
} catch {
|
|
3863
|
+
return void 0;
|
|
3864
|
+
}
|
|
3865
|
+
const split = splitLiveHiddenColumns(executed, state);
|
|
3866
|
+
return liveMaintainedOutcome({
|
|
3867
|
+
...state,
|
|
3868
|
+
columnDomains: split.result.columnDomains,
|
|
3869
|
+
complete: state.fullPlan.limit === void 0 || split.result.rows.length < state.fullPlan.limit,
|
|
3870
|
+
rows: split.result.rows,
|
|
3871
|
+
keys: split.keys,
|
|
3872
|
+
order: split.order
|
|
3873
|
+
}, void 0);
|
|
3874
|
+
}
|
|
3875
|
+
async #liveMaintain(retained, state, tableIds, after, until, probe) {
|
|
3876
|
+
if (tableIds.length !== 1 || tableIds[0] !== state.tableId)
|
|
3877
|
+
return void 0;
|
|
3878
|
+
const context = await this.#liveProofContext(after, until);
|
|
3879
|
+
const entry = await this.#liveProofTable(context, state.tableId);
|
|
3880
|
+
if (entry === void 0)
|
|
3881
|
+
return void 0;
|
|
3882
|
+
for (const version of context.changedVersions.get(state.tableId) ?? []) {
|
|
3883
|
+
if (!entry.coveredVersions.has(version))
|
|
3884
|
+
return void 0;
|
|
3885
|
+
}
|
|
3886
|
+
const changed = await this.#liveChangedKeys(entry, state.keyColumnId);
|
|
3887
|
+
if (changed === void 0)
|
|
3888
|
+
return void 0;
|
|
3889
|
+
const { changedKeys, existingKeys } = changed;
|
|
3890
|
+
if (changedKeys.size === 0)
|
|
3891
|
+
return { result: retained, state, changed: false };
|
|
3892
|
+
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))
|
|
3898
|
+
affected.add(index);
|
|
3899
|
+
}
|
|
3900
|
+
const fullLimit = state.limit === void 0 ? void 0 : state.limit + state.margin;
|
|
3901
|
+
const truncated = !state.complete;
|
|
3902
|
+
if (truncated && affected.size > 0 && state.orderTerms.length === 0)
|
|
3903
|
+
return void 0;
|
|
3904
|
+
if (state.offset > 0) {
|
|
3905
|
+
for (const token of existingKeys)
|
|
3906
|
+
if (!retainedTokens.has(token))
|
|
3907
|
+
return void 0;
|
|
3908
|
+
}
|
|
3909
|
+
const deltaPlan = {
|
|
3910
|
+
...state.deltaPlan,
|
|
3911
|
+
predicates: [
|
|
3912
|
+
...state.deltaPlan.predicates,
|
|
3913
|
+
{
|
|
3914
|
+
left: { kind: "column", reference: state.qualifiedKey },
|
|
3915
|
+
operator: "IN",
|
|
3916
|
+
right: {
|
|
3917
|
+
kind: "list",
|
|
3918
|
+
items: [...changedKeys.values()].map((value) => ({ kind: "literal", value }))
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
]
|
|
3922
|
+
};
|
|
3923
|
+
const delta = splitLiveHiddenColumns(externalizeQueryResult(await this.#withReadReservation(() => this.#queryCompiled(deltaPlan, {}, probe))), state);
|
|
3924
|
+
const compare = liveOrderComparator(state.orderTerms);
|
|
3925
|
+
const added = {
|
|
3926
|
+
rows: delta.result.rows,
|
|
3927
|
+
keys: delta.keys,
|
|
3928
|
+
order: delta.order,
|
|
3929
|
+
previousIndex: new Int32Array(delta.result.rows.length).fill(-1)
|
|
3930
|
+
};
|
|
3931
|
+
let merged;
|
|
3932
|
+
try {
|
|
3933
|
+
const kept = filterLiveRows(state, (index) => !affected.has(index));
|
|
3934
|
+
if (state.offset > 0 && added.rows.length > 0) {
|
|
3935
|
+
if (kept.rows.length === 0 || added.rows.some((_, index) => compare(added.order, index, kept.order, 0) < 0)) {
|
|
3936
|
+
return void 0;
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
merged = mergeLiveRows(kept, added, compare, state.orderTerms.length > 0);
|
|
3940
|
+
} catch {
|
|
3941
|
+
return void 0;
|
|
3942
|
+
}
|
|
3943
|
+
if (fullLimit !== void 0 && merged.rows.length > fullLimit)
|
|
3944
|
+
merged = trimLiveRows(merged, fullLimit);
|
|
3945
|
+
if (truncated && affected.size > 0 && state.limit !== void 0) {
|
|
3946
|
+
const oldEdge = state.rows.length - 1;
|
|
3947
|
+
const visibleEdge = Math.min(merged.rows.length, state.limit) - 1;
|
|
3948
|
+
if (merged.rows.length < state.limit || oldEdge < 0 || visibleEdge < 0 || compare(merged.order, visibleEdge, state.order, oldEdge) > 0) {
|
|
3949
|
+
return void 0;
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
return liveMaintainedOutcome({ ...state, rows: merged.rows, keys: merged.keys, order: merged.order }, retained, merged.previousIndex);
|
|
3953
|
+
}
|
|
3757
3954
|
async #liveChangeCanAffect(query, tableIds, after, until) {
|
|
3758
3955
|
const plan = typeof query === "string" ? this.#compileCached(query) : query.kind === "typed-query" ? query.plan : bindPlanParameters(this.#compileCached(query.sql), query.params);
|
|
3759
3956
|
if (planContainsFts(plan))
|
|
@@ -3855,6 +4052,51 @@ class MinnowDatabase {
|
|
|
3855
4052
|
}
|
|
3856
4053
|
return context;
|
|
3857
4054
|
}
|
|
4055
|
+
#liveChangedKeys(entry, keyColumnId) {
|
|
4056
|
+
const cached = entry.changedKeys.get(keyColumnId);
|
|
4057
|
+
if (cached !== void 0)
|
|
4058
|
+
return cached;
|
|
4059
|
+
const pending = (async () => {
|
|
4060
|
+
const blockIds = [];
|
|
4061
|
+
const existingBlockIds = /* @__PURE__ */ new Set();
|
|
4062
|
+
let deltaRows = 0;
|
|
4063
|
+
for (const segment of entry.windowSegments) {
|
|
4064
|
+
if (segment.kind === "base")
|
|
4065
|
+
continue;
|
|
4066
|
+
const ids = segment.columnBlockIds[keyColumnId];
|
|
4067
|
+
if (ids === void 0)
|
|
4068
|
+
return void 0;
|
|
4069
|
+
deltaRows += segment.rowCount;
|
|
4070
|
+
if (deltaRows > LIVE_MAINTENANCE_MAX_DELTA_ROWS)
|
|
4071
|
+
return void 0;
|
|
4072
|
+
blockIds.push(...ids);
|
|
4073
|
+
if (segment.kind !== "insert")
|
|
4074
|
+
for (const id of ids)
|
|
4075
|
+
existingBlockIds.add(id);
|
|
4076
|
+
}
|
|
4077
|
+
const changedKeys = /* @__PURE__ */ new Map();
|
|
4078
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
4079
|
+
if (blockIds.length > 0) {
|
|
4080
|
+
const decoded = await this.#loadDecodedBlocks([...new Set(blockIds)]);
|
|
4081
|
+
for (const blockId of blockIds) {
|
|
4082
|
+
const column = decoded.get(blockId);
|
|
4083
|
+
if (column === void 0)
|
|
4084
|
+
return void 0;
|
|
4085
|
+
for (const value of column.values) {
|
|
4086
|
+
if (value === null)
|
|
4087
|
+
continue;
|
|
4088
|
+
const token = liveKeyToken(value);
|
|
4089
|
+
changedKeys.set(token, value);
|
|
4090
|
+
if (existingBlockIds.has(blockId))
|
|
4091
|
+
existingKeys.add(token);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
return { changedKeys, existingKeys };
|
|
4096
|
+
})();
|
|
4097
|
+
entry.changedKeys.set(keyColumnId, pending);
|
|
4098
|
+
return pending;
|
|
4099
|
+
}
|
|
3858
4100
|
#liveProofTable(context, tableId) {
|
|
3859
4101
|
const cached = context.tables.get(tableId);
|
|
3860
4102
|
if (cached !== void 0)
|
|
@@ -3875,7 +4117,7 @@ class MinnowDatabase {
|
|
|
3875
4117
|
coveredVersions.add(committed);
|
|
3876
4118
|
windowSegments.push(segment);
|
|
3877
4119
|
}
|
|
3878
|
-
return { table, windowSegments, coveredVersions };
|
|
4120
|
+
return { table, windowSegments, coveredVersions, changedKeys: /* @__PURE__ */ new Map() };
|
|
3879
4121
|
})();
|
|
3880
4122
|
context.tables.set(tableId, entry);
|
|
3881
4123
|
return entry;
|
|
@@ -14189,6 +14431,203 @@ function searchableFtsColumns(table) {
|
|
|
14189
14431
|
return void 0;
|
|
14190
14432
|
return visibleTableColumns(table).filter((column) => column.type !== "boolean").map((column) => column.name);
|
|
14191
14433
|
}
|
|
14434
|
+
const LIVE_HIDDEN_PREFIX = "__minnow_live_";
|
|
14435
|
+
const LIVE_KEY_ALIAS = `${LIVE_HIDDEN_PREFIX}key`;
|
|
14436
|
+
const LIVE_ORDER_ALIAS = `${LIVE_HIDDEN_PREFIX}order_`;
|
|
14437
|
+
const LIVE_MAINTENANCE_MAX_DELTA_ROWS = 2048;
|
|
14438
|
+
const LIVE_WINDOW_MARGIN_MIN = 16;
|
|
14439
|
+
const LIVE_WINDOW_MARGIN_MAX = 64;
|
|
14440
|
+
function sameLiveRow(left, right, columns) {
|
|
14441
|
+
for (const column of columns) {
|
|
14442
|
+
const a = left[column] ?? null;
|
|
14443
|
+
const b = right[column] ?? null;
|
|
14444
|
+
if (a instanceof Date || b instanceof Date) {
|
|
14445
|
+
if (!(a instanceof Date && b instanceof Date) || !Object.is(dateMilliseconds(a), dateMilliseconds(b))) {
|
|
14446
|
+
return false;
|
|
14447
|
+
}
|
|
14448
|
+
} else if (!Object.is(a, b))
|
|
14449
|
+
return false;
|
|
14450
|
+
}
|
|
14451
|
+
return true;
|
|
14452
|
+
}
|
|
14453
|
+
function liveKeyToken(value) {
|
|
14454
|
+
if (typeof value === "number")
|
|
14455
|
+
return `n:${String(value)}`;
|
|
14456
|
+
if (typeof value === "string")
|
|
14457
|
+
return `s:${value}`;
|
|
14458
|
+
if (typeof value === "boolean")
|
|
14459
|
+
return value ? "b:1" : "b:0";
|
|
14460
|
+
if (value instanceof Date)
|
|
14461
|
+
return `d:${String(dateMilliseconds(value))}`;
|
|
14462
|
+
return "z";
|
|
14463
|
+
}
|
|
14464
|
+
function splitLiveHiddenColumns(executed, state) {
|
|
14465
|
+
const publicCount = state.publicColumns.length;
|
|
14466
|
+
const hidden = executed.columns.slice(publicCount);
|
|
14467
|
+
const count = executed.rows.length;
|
|
14468
|
+
const keys = new Array(count);
|
|
14469
|
+
const order = state.orderTerms.map(() => new Array(count));
|
|
14470
|
+
for (let index = 0; index < count; index += 1) {
|
|
14471
|
+
const row = executed.rows[index] ?? {};
|
|
14472
|
+
keys[index] = row[LIVE_KEY_ALIAS] ?? null;
|
|
14473
|
+
for (const [term, { alias }] of state.orderTerms.entries()) {
|
|
14474
|
+
const values = order[term];
|
|
14475
|
+
if (values !== void 0)
|
|
14476
|
+
values[index] = row[alias] ?? null;
|
|
14477
|
+
}
|
|
14478
|
+
for (const column of hidden)
|
|
14479
|
+
Reflect.deleteProperty(row, column);
|
|
14480
|
+
}
|
|
14481
|
+
return {
|
|
14482
|
+
result: {
|
|
14483
|
+
columns: executed.columns.slice(0, publicCount),
|
|
14484
|
+
columnDomains: executed.columnDomains.slice(0, publicCount),
|
|
14485
|
+
rows: executed.rows
|
|
14486
|
+
},
|
|
14487
|
+
keys,
|
|
14488
|
+
order
|
|
14489
|
+
};
|
|
14490
|
+
}
|
|
14491
|
+
function liveMaintainedOutcome(state, previous, previousIndex) {
|
|
14492
|
+
const visibleCount = state.limit === void 0 ? state.rows.length : Math.min(state.rows.length, state.limit);
|
|
14493
|
+
const visible = state.rows.slice(0, visibleCount);
|
|
14494
|
+
if (previous === void 0) {
|
|
14495
|
+
return {
|
|
14496
|
+
result: {
|
|
14497
|
+
columns: [...state.publicColumns],
|
|
14498
|
+
columnDomains: [...state.columnDomains],
|
|
14499
|
+
rows: visible
|
|
14500
|
+
},
|
|
14501
|
+
state,
|
|
14502
|
+
changed: true
|
|
14503
|
+
};
|
|
14504
|
+
}
|
|
14505
|
+
const previousCount = previous.rows.length;
|
|
14506
|
+
const retained = new Int32Array(visibleCount);
|
|
14507
|
+
let same = visibleCount === previousCount;
|
|
14508
|
+
let kept = 0;
|
|
14509
|
+
for (let index = 0; index < visibleCount; index += 1) {
|
|
14510
|
+
const was = previousIndex?.[index] ?? -1;
|
|
14511
|
+
if (was >= 0 && was < previousCount) {
|
|
14512
|
+
retained[index] = was;
|
|
14513
|
+
kept += 1;
|
|
14514
|
+
if (was !== index)
|
|
14515
|
+
same = false;
|
|
14516
|
+
continue;
|
|
14517
|
+
}
|
|
14518
|
+
retained[index] = -1;
|
|
14519
|
+
const before = previous.rows[index];
|
|
14520
|
+
const now = visible[index];
|
|
14521
|
+
if (before !== void 0 && now !== void 0 && sameLiveRow(before, now, previous.columns)) {
|
|
14522
|
+
visible[index] = before;
|
|
14523
|
+
retained[index] = index;
|
|
14524
|
+
kept += 1;
|
|
14525
|
+
} else
|
|
14526
|
+
same = false;
|
|
14527
|
+
}
|
|
14528
|
+
if (same)
|
|
14529
|
+
return { result: previous, state, changed: false };
|
|
14530
|
+
return {
|
|
14531
|
+
result: {
|
|
14532
|
+
columns: [...state.publicColumns],
|
|
14533
|
+
columnDomains: [...state.columnDomains],
|
|
14534
|
+
rows: visible
|
|
14535
|
+
},
|
|
14536
|
+
state,
|
|
14537
|
+
changed: true,
|
|
14538
|
+
...kept > 0 ? { retained } : {}
|
|
14539
|
+
};
|
|
14540
|
+
}
|
|
14541
|
+
function filterLiveRows(state, keep) {
|
|
14542
|
+
const rows = [];
|
|
14543
|
+
const keys = [];
|
|
14544
|
+
const order = state.order.map(() => new Array());
|
|
14545
|
+
const previous = [];
|
|
14546
|
+
for (let index = 0; index < state.rows.length; index += 1) {
|
|
14547
|
+
const row = state.rows[index];
|
|
14548
|
+
if (row === void 0 || !keep(index))
|
|
14549
|
+
continue;
|
|
14550
|
+
rows.push(row);
|
|
14551
|
+
keys.push(state.keys[index] ?? null);
|
|
14552
|
+
for (const [term, values] of state.order.entries()) {
|
|
14553
|
+
order[term]?.push(values[index] ?? null);
|
|
14554
|
+
}
|
|
14555
|
+
previous.push(index);
|
|
14556
|
+
}
|
|
14557
|
+
return { rows, keys, order, previousIndex: Int32Array.from(previous) };
|
|
14558
|
+
}
|
|
14559
|
+
function trimLiveRows(rows, count) {
|
|
14560
|
+
return {
|
|
14561
|
+
rows: rows.rows.slice(0, count),
|
|
14562
|
+
keys: rows.keys.slice(0, count),
|
|
14563
|
+
order: rows.order.map((values) => values.slice(0, count)),
|
|
14564
|
+
previousIndex: rows.previousIndex.slice(0, count)
|
|
14565
|
+
};
|
|
14566
|
+
}
|
|
14567
|
+
function liveOrderComparator(terms) {
|
|
14568
|
+
return (leftOrder, leftIndex, rightOrder, rightIndex) => {
|
|
14569
|
+
for (const [term, { descending, nulls }] of terms.entries()) {
|
|
14570
|
+
const a = leftOrder[term]?.[leftIndex] ?? null;
|
|
14571
|
+
const b = rightOrder[term]?.[rightIndex] ?? null;
|
|
14572
|
+
if (a === null || b === null) {
|
|
14573
|
+
if (a === null && b === null)
|
|
14574
|
+
continue;
|
|
14575
|
+
const nullsFirst = nulls === "first" || nulls === void 0 && descending;
|
|
14576
|
+
return a === null ? nullsFirst ? -1 : 1 : nullsFirst ? 1 : -1;
|
|
14577
|
+
}
|
|
14578
|
+
let comparison = compareSqlValues(a, b);
|
|
14579
|
+
if (descending)
|
|
14580
|
+
comparison = -comparison;
|
|
14581
|
+
if (comparison !== 0)
|
|
14582
|
+
return comparison;
|
|
14583
|
+
}
|
|
14584
|
+
return 0;
|
|
14585
|
+
};
|
|
14586
|
+
}
|
|
14587
|
+
function mergeLiveRows(kept, added, compare, ordered) {
|
|
14588
|
+
if (added.rows.length === 0)
|
|
14589
|
+
return kept;
|
|
14590
|
+
const terms = kept.order.length;
|
|
14591
|
+
const total = kept.rows.length + added.rows.length;
|
|
14592
|
+
const rows = new Array(total);
|
|
14593
|
+
const keys = new Array(total);
|
|
14594
|
+
const order = Array.from({ length: terms }, () => new Array(total));
|
|
14595
|
+
const previousIndex = new Int32Array(total);
|
|
14596
|
+
const take = (source, from, to) => {
|
|
14597
|
+
rows[to] = source.rows[from] ?? {};
|
|
14598
|
+
keys[to] = source.keys[from] ?? null;
|
|
14599
|
+
for (let term = 0; term < terms; term += 1) {
|
|
14600
|
+
const values = order[term];
|
|
14601
|
+
if (values !== void 0)
|
|
14602
|
+
values[to] = source.order[term]?.[from] ?? null;
|
|
14603
|
+
}
|
|
14604
|
+
previousIndex[to] = source.previousIndex[from] ?? -1;
|
|
14605
|
+
};
|
|
14606
|
+
if (!ordered) {
|
|
14607
|
+
for (let index = 0; index < kept.rows.length; index += 1)
|
|
14608
|
+
take(kept, index, index);
|
|
14609
|
+
for (let index = 0; index < added.rows.length; index += 1) {
|
|
14610
|
+
take(added, index, kept.rows.length + index);
|
|
14611
|
+
}
|
|
14612
|
+
return { rows, keys, order, previousIndex };
|
|
14613
|
+
}
|
|
14614
|
+
const addedIndexes = added.rows.map((_, index) => index);
|
|
14615
|
+
addedIndexes.sort((left, right) => compare(added.order, left, added.order, right));
|
|
14616
|
+
let keptIndex = 0;
|
|
14617
|
+
let addedPosition = 0;
|
|
14618
|
+
for (let to = 0; to < total; to += 1) {
|
|
14619
|
+
const addedIndex = addedIndexes[addedPosition];
|
|
14620
|
+
const takeAdded = addedIndex !== void 0 && (keptIndex >= kept.rows.length || compare(added.order, addedIndex, kept.order, keptIndex) < 0);
|
|
14621
|
+
if (takeAdded) {
|
|
14622
|
+
take(added, addedIndex, to);
|
|
14623
|
+
addedPosition += 1;
|
|
14624
|
+
} else {
|
|
14625
|
+
take(kept, keptIndex, to);
|
|
14626
|
+
keptIndex += 1;
|
|
14627
|
+
}
|
|
14628
|
+
}
|
|
14629
|
+
return { rows, keys, order, previousIndex };
|
|
14630
|
+
}
|
|
14192
14631
|
function getUniqueKeyColumn(table) {
|
|
14193
14632
|
if (table.uniqueKeyColumnId === void 0)
|
|
14194
14633
|
return void 0;
|