akanjs 3.1.0-rc.25 → 3.1.0-rc.26
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/package.json +1 -1
- package/service/predefinedAdaptor/database.adaptor.ts +57 -25
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +136 -46
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +7 -0
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +7 -0
- package/service/predefinedAdaptor/sql/types.ts +6 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +11 -0
- package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/types.d.ts +12 -0
- package/ui/Tab/Provider.tsx +6 -8
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import type { PromiseOrObject } from "akanjs/base";
|
|
6
6
|
import type { ConstantModel } from "akanjs/constant";
|
|
7
7
|
import type { DatabaseModel, DocumentSchema, SchemaOf } from "akanjs/document";
|
|
8
|
-
import type { Sql } from "postgres";
|
|
8
|
+
import type { ReservedSql, Sql } from "postgres";
|
|
9
9
|
import { adapt } from "../adapt";
|
|
10
10
|
import { ScheduleAdaptorRole } from "./role.adaptor";
|
|
11
11
|
import {
|
|
@@ -185,6 +185,16 @@ export class SqliteDatabase
|
|
|
185
185
|
.run(key, value, Date.now());
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
listMeta(prefix: string) {
|
|
189
|
+
return this.#db
|
|
190
|
+
.query(`SELECT "key", "value" FROM "_akan_meta" WHERE substr("key", 1, ?) = ?`)
|
|
191
|
+
.all(prefix.length, prefix) as { key: string; value: string }[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async deleteMeta(key: string) {
|
|
195
|
+
await this.#client.prepare(`DELETE FROM "_akan_meta" WHERE "key" = ?`).run(key);
|
|
196
|
+
}
|
|
197
|
+
|
|
188
198
|
async transaction<T>(fn: () => PromiseOrObject<T>): Promise<T> {
|
|
189
199
|
const active = this.#transaction.getStore();
|
|
190
200
|
if (active) return await fn();
|
|
@@ -438,6 +448,16 @@ export class PostgresDatabase
|
|
|
438
448
|
.run(key, value, Date.now());
|
|
439
449
|
}
|
|
440
450
|
|
|
451
|
+
async listMeta(prefix: string) {
|
|
452
|
+
return await this.getConnection()
|
|
453
|
+
.prepare(`SELECT "key", "value" FROM "_akan_meta" WHERE substr("key", 1, $1) = $2`)
|
|
454
|
+
.all<{ key: string; value: string }>(prefix.length, prefix);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async deleteMeta(key: string) {
|
|
458
|
+
await this.getConnection().prepare(`DELETE FROM "_akan_meta" WHERE "key" = $1`).run(key);
|
|
459
|
+
}
|
|
460
|
+
|
|
441
461
|
async transaction<T>(fn: () => PromiseOrObject<T>): Promise<T> {
|
|
442
462
|
const active = this.#transaction.getStore();
|
|
443
463
|
if (active) return await fn();
|
|
@@ -491,35 +511,47 @@ export class PostgresDatabase
|
|
|
491
511
|
}
|
|
492
512
|
|
|
493
513
|
async buildIndexConcurrently({ name, next, create, replace }: ConcurrentIndexBuild) {
|
|
514
|
+
await this.#underIndexLock(name, async (reserved) => {
|
|
515
|
+
const valid = async (index: string) =>
|
|
516
|
+
(
|
|
517
|
+
await reserved.unsafe(`SELECT indisvalid AS "valid" FROM pg_index WHERE indexrelid = to_regclass($1)`, [
|
|
518
|
+
quoteIdent(index),
|
|
519
|
+
])
|
|
520
|
+
).at(0)?.valid as boolean | undefined;
|
|
521
|
+
if (!replace) {
|
|
522
|
+
const state = await valid(name);
|
|
523
|
+
if (state) return;
|
|
524
|
+
if (state === false) await reserved.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdent(name)}`);
|
|
525
|
+
await reserved.unsafe(create(name));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
await reserved.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdent(next)}`);
|
|
530
|
+
try {
|
|
531
|
+
await reserved.unsafe(create(next));
|
|
532
|
+
} catch (error) {
|
|
533
|
+
await reserved.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdent(next)}`);
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
await reserved.unsafe(
|
|
537
|
+
`DROP INDEX IF EXISTS ${quoteIdent(name)}; ALTER INDEX ${quoteIdent(next)} RENAME TO ${quoteIdent(name)}`,
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async dropIndexConcurrently(name: string) {
|
|
543
|
+
await this.#underIndexLock(name, async (reserved) => {
|
|
544
|
+
await reserved.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdent(name)}`);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async #underIndexLock(name: string, fn: (reserved: ReservedSql) => Promise<void>) {
|
|
494
549
|
const reserved = await this.#sql.reserve();
|
|
495
550
|
const key = `akan:index:${name}`;
|
|
496
|
-
const valid = async (index: string) =>
|
|
497
|
-
(
|
|
498
|
-
await reserved.unsafe(`SELECT indisvalid AS "valid" FROM pg_index WHERE indexrelid = to_regclass($1)`, [
|
|
499
|
-
quoteIdent(index),
|
|
500
|
-
])
|
|
501
|
-
).at(0)?.valid as boolean | undefined;
|
|
502
551
|
try {
|
|
503
552
|
await reserved.unsafe(`SELECT pg_advisory_lock(hashtext(current_schema() || $1))`, [key]);
|
|
504
553
|
try {
|
|
505
|
-
|
|
506
|
-
const state = await valid(name);
|
|
507
|
-
if (state) return;
|
|
508
|
-
if (state === false) await reserved.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdent(name)}`);
|
|
509
|
-
await reserved.unsafe(create(name));
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
await reserved.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdent(next)}`);
|
|
514
|
-
try {
|
|
515
|
-
await reserved.unsafe(create(next));
|
|
516
|
-
} catch (error) {
|
|
517
|
-
await reserved.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdent(next)}`);
|
|
518
|
-
throw error;
|
|
519
|
-
}
|
|
520
|
-
await reserved.unsafe(
|
|
521
|
-
`DROP INDEX IF EXISTS ${quoteIdent(name)}; ALTER INDEX ${quoteIdent(next)} RENAME TO ${quoteIdent(name)}`,
|
|
522
|
-
);
|
|
554
|
+
await fn(reserved);
|
|
523
555
|
} finally {
|
|
524
556
|
await reserved.unsafe(`SELECT pg_advisory_unlock(hashtext(current_schema() || $1))`, [key]);
|
|
525
557
|
}
|
|
@@ -5,6 +5,7 @@ import { Err } from "akanjs/dictionary";
|
|
|
5
5
|
import {
|
|
6
6
|
createDocumentId,
|
|
7
7
|
type DatabaseModel,
|
|
8
|
+
type DocumentIndexDescriptor,
|
|
8
9
|
type DocumentQuery,
|
|
9
10
|
type DocumentSchema,
|
|
10
11
|
type DocumentScopeOptions,
|
|
@@ -66,12 +67,17 @@ interface DeclaredIndex {
|
|
|
66
67
|
next: string;
|
|
67
68
|
metaKey: string;
|
|
68
69
|
hash: string;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
unbuilt: boolean;
|
|
70
|
+
/** Every descriptor that declares this very index, so a copy an earlier release built under any of them serves it. */
|
|
71
|
+
hashes: Set<string>;
|
|
72
72
|
create: (name: string, concurrently: boolean) => string;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
interface StaleIndex {
|
|
76
|
+
name: string;
|
|
77
|
+
metaKey: string;
|
|
78
|
+
exists: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
type StoredRow = Omit<SqliteDocumentRow, "id">;
|
|
76
82
|
|
|
77
83
|
const freshDefault = (value: unknown) => (Array.isArray(value) ? [...(value as unknown[])] : value);
|
|
@@ -115,8 +121,8 @@ export class SqlDocumentStore {
|
|
|
115
121
|
|
|
116
122
|
async #ensure() {
|
|
117
123
|
this.assertValidRefName(this.table);
|
|
118
|
-
const
|
|
119
|
-
const indexes
|
|
124
|
+
const indexes = await this.#declaredIndexes();
|
|
125
|
+
for (const index of this.schema.indexes) if (!SqlDocumentStore.#builds(index)) this.#warnUnbuilt(index);
|
|
120
126
|
const createSchema = async () => {
|
|
121
127
|
const db = this.owner.getConnection();
|
|
122
128
|
const existed = !!this.owner.buildIndexConcurrently && !!(await this.owner.hasTable?.(this.table));
|
|
@@ -135,9 +141,10 @@ export class SqlDocumentStore {
|
|
|
135
141
|
`table:${this.table}`,
|
|
136
142
|
await descriptorHash({ table: this.table, columns: ["id", "createdAt", "updatedAt", "removedAt", "_doc"] }),
|
|
137
143
|
);
|
|
138
|
-
|
|
144
|
+
const { adopted, stale } = await this.#reconcileIndexes(indexes);
|
|
139
145
|
const concurrent: (DeclaredIndex & { replace: boolean })[] = [];
|
|
140
146
|
for (const index of indexes) {
|
|
147
|
+
if (adopted.has(index.name)) continue;
|
|
141
148
|
const stored = await this.owner.getMeta(index.metaKey);
|
|
142
149
|
const replace = !!stored && stored !== index.hash;
|
|
143
150
|
if (replace)
|
|
@@ -156,51 +163,141 @@ export class SqlDocumentStore {
|
|
|
156
163
|
else await db.execute(index.create(index.name, false));
|
|
157
164
|
await this.owner.setMeta(index.metaKey, index.hash);
|
|
158
165
|
}
|
|
159
|
-
|
|
166
|
+
const drops: StaleIndex[] = [];
|
|
167
|
+
for (const index of stale) {
|
|
168
|
+
if (!index.exists) await this.owner.deleteMeta(index.metaKey);
|
|
169
|
+
else if (this.owner.dropIndexConcurrently) drops.push(index);
|
|
170
|
+
else await this.#dropStaleIndex(index);
|
|
171
|
+
}
|
|
172
|
+
return { concurrent, drops };
|
|
160
173
|
};
|
|
161
|
-
const concurrent
|
|
174
|
+
const { concurrent, drops } = this.owner.lockSchema
|
|
175
|
+
? await this.owner.lockSchema(createSchema)
|
|
176
|
+
: await createSchema();
|
|
162
177
|
for (const { name, next, create, replace, metaKey, hash } of concurrent) {
|
|
163
178
|
await this.owner.buildIndexConcurrently?.({ name, next, create: (target) => create(target, true), replace });
|
|
164
179
|
await this.owner.setMeta(metaKey, hash);
|
|
165
180
|
}
|
|
181
|
+
for (const index of drops) await this.#dropStaleIndex(index);
|
|
166
182
|
await this.owner.getSearchIndex()?.ensureRef(this.constant, this.database);
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
static #builds(index: DocumentIndexDescriptor) {
|
|
186
|
+
return Object.values(index.fields).every((order) => order === 1 || order === -1);
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
async #declaredIndexes(): Promise<DeclaredIndex[]> {
|
|
170
|
-
|
|
171
|
-
this.schema.indexes.map(async (
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const columns =
|
|
177
|
-
|
|
178
|
-
:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
const candidates = await Promise.all(
|
|
191
|
+
this.schema.indexes.filter(SqlDocumentStore.#builds).map(async (descriptor) => {
|
|
192
|
+
const paths = Object.keys(descriptor.fields);
|
|
193
|
+
const base =
|
|
194
|
+
descriptor.name ??
|
|
195
|
+
[this.table, ...paths.map(toSafeRefName), ...(descriptor.unique ? ["unique"] : [])].join("_");
|
|
196
|
+
const columns = paths.map((path) => ({
|
|
197
|
+
path,
|
|
198
|
+
expr: this.compiler.fieldExpr(path),
|
|
199
|
+
isArray: this.compiler.isArrayPath(path),
|
|
200
|
+
}));
|
|
201
|
+
const predicate = descriptor.where ? this.#indexPredicate(descriptor.where, base) : undefined;
|
|
202
|
+
const create = (target: string, concurrently: boolean) =>
|
|
203
|
+
this.dialect.createIndex({
|
|
204
|
+
name: target,
|
|
205
|
+
table: this.table,
|
|
206
|
+
unique: !!descriptor.unique,
|
|
207
|
+
columns,
|
|
208
|
+
concurrently,
|
|
209
|
+
where: predicate,
|
|
210
|
+
});
|
|
211
|
+
const definition = create("", false);
|
|
184
212
|
return {
|
|
185
|
-
name,
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
named: !!descriptor.name,
|
|
214
|
+
base,
|
|
215
|
+
definition,
|
|
188
216
|
|
|
189
|
-
hash: await descriptorHash(predicate ? { ...
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
create: (target: string, concurrently: boolean) =>
|
|
193
|
-
this.dialect.createIndex({
|
|
194
|
-
name: target,
|
|
195
|
-
table: this.table,
|
|
196
|
-
unique: !!index.unique,
|
|
197
|
-
columns,
|
|
198
|
-
concurrently,
|
|
199
|
-
where: predicate,
|
|
200
|
-
}),
|
|
217
|
+
hash: await descriptorHash(predicate ? { ...descriptor, predicate } : descriptor),
|
|
218
|
+
suffix: (await descriptorHash(definition)).slice(0, 8),
|
|
219
|
+
create,
|
|
201
220
|
};
|
|
202
221
|
}),
|
|
203
222
|
);
|
|
223
|
+
const definitionsOf = new Map<string, Set<string>>();
|
|
224
|
+
for (const { base, definition } of candidates)
|
|
225
|
+
definitionsOf.set(base, (definitionsOf.get(base) ?? new Set()).add(definition));
|
|
226
|
+
const namedDefinitions = new Set(candidates.filter(({ named }) => named).map(({ definition }) => definition));
|
|
227
|
+
const indexes = new Map<string, DeclaredIndex>();
|
|
228
|
+
for (const { named, base, definition, hash, suffix, create } of candidates) {
|
|
229
|
+
if (!named && namedDefinitions.has(definition)) continue;
|
|
230
|
+
|
|
231
|
+
const shared = (definitionsOf.get(base)?.size ?? 0) > 1;
|
|
232
|
+
if (
|
|
233
|
+
named &&
|
|
234
|
+
shared &&
|
|
235
|
+
candidates.some((other) => other.named && other.base === base && other.definition !== definition)
|
|
236
|
+
)
|
|
237
|
+
throw new Error(`Two different indexes on ${this.table} are both named ${base}`);
|
|
238
|
+
const declared = named || !shared ? base : `${base}_${suffix}`;
|
|
239
|
+
this.assertValidRefName(declared);
|
|
240
|
+
const name = this.dialect.indexName(declared);
|
|
241
|
+
const existing = indexes.get(name);
|
|
242
|
+
if (existing) {
|
|
243
|
+
existing.hashes.add(hash);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
indexes.set(name, {
|
|
247
|
+
name,
|
|
248
|
+
next: this.dialect.indexName(`${declared}_next`),
|
|
249
|
+
metaKey: `index:${this.table}:${name}`,
|
|
250
|
+
hash,
|
|
251
|
+
hashes: new Set([hash]),
|
|
252
|
+
create,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return [...indexes.values()];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async #reconcileIndexes(indexes: DeclaredIndex[]) {
|
|
259
|
+
const prefix = `index:${this.table}:`;
|
|
260
|
+
const { sql, params } = this.dialect.indexesOf(this.table);
|
|
261
|
+
const rows = await this.owner
|
|
262
|
+
.getConnection()
|
|
263
|
+
.prepare(sql)
|
|
264
|
+
.all<{ name: string; valid: unknown }>(...params);
|
|
265
|
+
const physical = new Map(rows.map(({ name, valid }) => [name, !!valid]));
|
|
266
|
+
const declared = new Set(indexes.map(({ name }) => name));
|
|
267
|
+
const recorded = (await this.owner.listMeta(prefix)).map(({ key, value }) => ({
|
|
268
|
+
name: key.slice(prefix.length),
|
|
269
|
+
metaKey: key,
|
|
270
|
+
hash: value,
|
|
271
|
+
}));
|
|
272
|
+
const recordedNames = new Set(recorded.map(({ name }) => name));
|
|
273
|
+
const undeclared = recorded.filter(({ name }) => !declared.has(name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
274
|
+
|
|
275
|
+
const kept = new Set<string>();
|
|
276
|
+
const adopted = new Set<string>();
|
|
277
|
+
for (const index of indexes) {
|
|
278
|
+
if (recordedNames.has(index.name) || physical.has(index.name)) continue;
|
|
279
|
+
const copy = undeclared.find(({ name, hash }) => !kept.has(name) && index.hashes.has(hash) && physical.get(name));
|
|
280
|
+
if (!copy) continue;
|
|
281
|
+
kept.add(copy.name);
|
|
282
|
+
adopted.add(index.name);
|
|
283
|
+
}
|
|
284
|
+
const stale: StaleIndex[] = undeclared
|
|
285
|
+
.filter(({ name }) => !kept.has(name))
|
|
286
|
+
.map(({ name, metaKey }) => ({ name, metaKey, exists: physical.has(name) }));
|
|
287
|
+
return { adopted, stale };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async #dropStaleIndex({ name, metaKey }: StaleIndex) {
|
|
291
|
+
try {
|
|
292
|
+
if (this.owner.dropIndexConcurrently) await this.owner.dropIndexConcurrently(name);
|
|
293
|
+
else await this.owner.getConnection().execute(`DROP INDEX IF EXISTS ${quoteIdent(name)}`);
|
|
294
|
+
await this.owner.deleteMeta(metaKey);
|
|
295
|
+
SqlDocumentStore.#logger.info(`Dropped index ${name} on ${this.table}, which no declaration builds any more`);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
SqlDocumentStore.#logger.warn(
|
|
298
|
+
`Index ${name} on ${this.table} is no longer declared, and dropping it failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
204
301
|
}
|
|
205
302
|
|
|
206
303
|
#indexPredicate(where: DocumentQuery, name: string) {
|
|
@@ -210,20 +307,13 @@ export class SqlDocumentStore {
|
|
|
210
307
|
return sql === "1 = 1" ? undefined : inlineParams(sql, params);
|
|
211
308
|
}
|
|
212
309
|
|
|
213
|
-
|
|
214
|
-
const dropped =
|
|
215
|
-
(await this.owner.getMeta(index.metaKey)) === index.hash && !built.some(({ name }) => name === index.name);
|
|
216
|
-
if (dropped) {
|
|
217
|
-
await this.owner.getConnection().execute(`DROP INDEX IF EXISTS ${quoteIdent(index.name)}`);
|
|
218
|
-
|
|
219
|
-
await this.owner.setMeta(index.metaKey, "");
|
|
220
|
-
}
|
|
310
|
+
#warnUnbuilt(index: DocumentIndexDescriptor) {
|
|
221
311
|
const paths = Object.keys(index.fields);
|
|
222
312
|
SqlDocumentStore.#logger.warn(
|
|
223
313
|
`Index ${JSON.stringify(index.fields)} on ${this.table} is not built: only 1 and -1 order a key, and "text" is ` +
|
|
224
314
|
`MongoDB's text index, which as a B-tree would copy each whole value and fail long writes on Postgres. Search ` +
|
|
225
315
|
`${paths.join(", ")} through the full-text role, field(String, { text: "desc" }), or declare ` +
|
|
226
|
-
`{ ${paths[0]}: 1 } to look it up by equality
|
|
316
|
+
`{ ${paths[0]}: 1 } to look it up by equality.`,
|
|
227
317
|
);
|
|
228
318
|
}
|
|
229
319
|
|
|
@@ -171,6 +171,13 @@ export class PostgresDialect implements SqlDialect {
|
|
|
171
171
|
|
|
172
172
|
return `${create} (${columns.map(({ expr }) => `${expr} NULLS FIRST`).join(", ")})${predicate}`;
|
|
173
173
|
}
|
|
174
|
+
|
|
175
|
+
indexesOf(table: string): SqlFrag {
|
|
176
|
+
return {
|
|
177
|
+
sql: `SELECT c.relname AS "name", i.indisvalid AS "valid" FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid WHERE i.indrelid = to_regclass(?) AND NOT i.indisprimary`,
|
|
178
|
+
params: [quoteIdent(table)],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
174
181
|
applyUpdate(acc: string, op: DocumentUpdateOperator, path: string, value: unknown): SqlFrag {
|
|
175
182
|
const p = this.#path(path);
|
|
176
183
|
|
|
@@ -93,6 +93,13 @@ export class SqliteDialect implements SqlDialect {
|
|
|
93
93
|
createIndex({ name, table, unique, columns, where }: CreateIndexProps) {
|
|
94
94
|
return `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${quoteIdent(name)} ON ${quoteIdent(table)} (${columns.map(({ expr }) => expr).join(", ")})${where ? ` WHERE ${where}` : ""}`;
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
indexesOf(table: string): SqlFrag {
|
|
98
|
+
return {
|
|
99
|
+
sql: `SELECT "name", 1 AS "valid" FROM "sqlite_master" WHERE "type" = 'index' AND "tbl_name" = ? AND "sql" IS NOT NULL`,
|
|
100
|
+
params: [table],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
96
103
|
applyUpdate(acc: string, op: DocumentUpdateOperator, path: string, value: unknown): SqlFrag {
|
|
97
104
|
const p = this.#path(path);
|
|
98
105
|
|
|
@@ -194,6 +194,8 @@ export interface DocumentDatabaseOwner {
|
|
|
194
194
|
getSearchIndex(): SearchIndex | null;
|
|
195
195
|
getMeta(key: string): Promise<string | undefined> | string | undefined;
|
|
196
196
|
setMeta(key: string, value: string): Promise<void>;
|
|
197
|
+
listMeta(prefix: string): Promise<{ key: string; value: string }[]> | { key: string; value: string }[];
|
|
198
|
+
deleteMeta(key: string): Promise<void>;
|
|
197
199
|
afterCommit(fn: () => PromiseOrObject<void>): Promise<void>;
|
|
198
200
|
transaction?<T>(fn: () => PromiseOrObject<T>): Promise<T>;
|
|
199
201
|
/** Runs `fn` as the only schema change: Postgres races two `CREATE … IF NOT EXISTS` into a duplicate-key error. */
|
|
@@ -204,6 +206,8 @@ export interface DocumentDatabaseOwner {
|
|
|
204
206
|
grantInsight?(table: string): Promise<void>;
|
|
205
207
|
/** Builds an index on a table that already holds rows without blocking its writes; never inside a transaction. */
|
|
206
208
|
buildIndexConcurrently?(build: ConcurrentIndexBuild): Promise<void>;
|
|
209
|
+
/** Drops an index without blocking its table's reads and writes; never inside a transaction. */
|
|
210
|
+
dropIndexConcurrently?(name: string): Promise<void>;
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
export const QUERY_OPERATOR_KEYS = queryOperatorKeys;
|
|
@@ -276,6 +280,8 @@ export interface SqlDialect {
|
|
|
276
280
|
orderTerm(expr: string, direction: 1 | -1): string;
|
|
277
281
|
indexName(name: string): string;
|
|
278
282
|
createIndex(props: CreateIndexProps): string;
|
|
283
|
+
/** Rows `{ name, valid }`, one per index on `table` besides its primary key. */
|
|
284
|
+
indexesOf(table: string): SqlFrag;
|
|
279
285
|
applyUpdate(acc: string, op: DocumentUpdateOperator, path: string, value: unknown): SqlFrag;
|
|
280
286
|
/** The stored document with these top-level fields replaced (value as JSON text) and these removed. */
|
|
281
287
|
mergeDocument(set: [field: string, json: string][], removed: string[]): SqlFrag;
|
|
@@ -40,6 +40,11 @@ export declare class SqliteDatabase extends SqliteDatabase_base implements Datab
|
|
|
40
40
|
getStore(constant: ConstantModel, database: DatabaseModel, schema: SchemaOf): SqlDocumentStore;
|
|
41
41
|
getMeta(key: string): string | undefined;
|
|
42
42
|
setMeta(key: string, value: string): Promise<void>;
|
|
43
|
+
listMeta(prefix: string): {
|
|
44
|
+
key: string;
|
|
45
|
+
value: string;
|
|
46
|
+
}[];
|
|
47
|
+
deleteMeta(key: string): Promise<void>;
|
|
43
48
|
transaction<T>(fn: () => PromiseOrObject<T>): Promise<T>;
|
|
44
49
|
afterCommit(fn: () => PromiseOrObject<void>): Promise<void>;
|
|
45
50
|
checkpoint(mode?: "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE"): void;
|
|
@@ -71,10 +76,16 @@ export declare class PostgresDatabase extends PostgresDatabase_base implements D
|
|
|
71
76
|
getStore(constant: ConstantModel, database: DatabaseModel, schema: SchemaOf): SqlDocumentStore;
|
|
72
77
|
getMeta(key: string): Promise<string | undefined>;
|
|
73
78
|
setMeta(key: string, value: string): Promise<void>;
|
|
79
|
+
listMeta(prefix: string): Promise<{
|
|
80
|
+
key: string;
|
|
81
|
+
value: string;
|
|
82
|
+
}[]>;
|
|
83
|
+
deleteMeta(key: string): Promise<void>;
|
|
74
84
|
transaction<T>(fn: () => PromiseOrObject<T>): Promise<T>;
|
|
75
85
|
afterCommit(fn: () => PromiseOrObject<void>): Promise<void>;
|
|
76
86
|
lockSchema<T>(fn: () => Promise<T>): Promise<T>;
|
|
77
87
|
hasTable(table: string): Promise<boolean>;
|
|
78
88
|
hasValidIndex(name: string): Promise<boolean>;
|
|
79
89
|
buildIndexConcurrently({ name, next, create, replace }: ConcurrentIndexBuild): Promise<void>;
|
|
90
|
+
dropIndexConcurrently(name: string): Promise<void>;
|
|
80
91
|
}
|
|
@@ -30,6 +30,7 @@ export declare class PostgresDialect implements SqlDialect {
|
|
|
30
30
|
orderTerm(expr: string, direction: 1 | -1): string;
|
|
31
31
|
indexName(name: string): string;
|
|
32
32
|
createIndex({ name, table, unique, columns, concurrently, where }: CreateIndexProps): string;
|
|
33
|
+
indexesOf(table: string): SqlFrag;
|
|
33
34
|
applyUpdate(acc: string, op: DocumentUpdateOperator, path: string, value: unknown): SqlFrag;
|
|
34
35
|
mergeDocument(set: [field: string, json: string][], removed: string[]): SqlFrag;
|
|
35
36
|
affectedRows(result: unknown): number;
|
|
@@ -25,6 +25,7 @@ export declare class SqliteDialect implements SqlDialect {
|
|
|
25
25
|
orderTerm(expr: string, direction: 1 | -1): string;
|
|
26
26
|
indexName(name: string): string;
|
|
27
27
|
createIndex({ name, table, unique, columns, where }: CreateIndexProps): string;
|
|
28
|
+
indexesOf(table: string): SqlFrag;
|
|
28
29
|
applyUpdate(acc: string, op: DocumentUpdateOperator, path: string, value: unknown): SqlFrag;
|
|
29
30
|
mergeDocument(set: [field: string, json: string][], removed: string[]): SqlFrag;
|
|
30
31
|
affectedRows(result: unknown): number;
|
|
@@ -198,6 +198,14 @@ export interface DocumentDatabaseOwner {
|
|
|
198
198
|
getSearchIndex(): SearchIndex | null;
|
|
199
199
|
getMeta(key: string): Promise<string | undefined> | string | undefined;
|
|
200
200
|
setMeta(key: string, value: string): Promise<void>;
|
|
201
|
+
listMeta(prefix: string): Promise<{
|
|
202
|
+
key: string;
|
|
203
|
+
value: string;
|
|
204
|
+
}[]> | {
|
|
205
|
+
key: string;
|
|
206
|
+
value: string;
|
|
207
|
+
}[];
|
|
208
|
+
deleteMeta(key: string): Promise<void>;
|
|
201
209
|
afterCommit(fn: () => PromiseOrObject<void>): Promise<void>;
|
|
202
210
|
transaction?<T>(fn: () => PromiseOrObject<T>): Promise<T>;
|
|
203
211
|
/** Runs `fn` as the only schema change: Postgres races two `CREATE … IF NOT EXISTS` into a duplicate-key error. */
|
|
@@ -208,6 +216,8 @@ export interface DocumentDatabaseOwner {
|
|
|
208
216
|
grantInsight?(table: string): Promise<void>;
|
|
209
217
|
/** Builds an index on a table that already holds rows without blocking its writes; never inside a transaction. */
|
|
210
218
|
buildIndexConcurrently?(build: ConcurrentIndexBuild): Promise<void>;
|
|
219
|
+
/** Drops an index without blocking its table's reads and writes; never inside a transaction. */
|
|
220
|
+
dropIndexConcurrently?(name: string): Promise<void>;
|
|
211
221
|
}
|
|
212
222
|
export declare const QUERY_OPERATOR_KEYS: Set<string>;
|
|
213
223
|
export interface SqlFrag {
|
|
@@ -271,6 +281,8 @@ export interface SqlDialect {
|
|
|
271
281
|
orderTerm(expr: string, direction: 1 | -1): string;
|
|
272
282
|
indexName(name: string): string;
|
|
273
283
|
createIndex(props: CreateIndexProps): string;
|
|
284
|
+
/** Rows `{ name, valid }`, one per index on `table` besides its primary key. */
|
|
285
|
+
indexesOf(table: string): SqlFrag;
|
|
274
286
|
applyUpdate(acc: string, op: DocumentUpdateOperator, path: string, value: unknown): SqlFrag;
|
|
275
287
|
/** The stored document with these top-level fields replaced (value as JSON text) and these removed. */
|
|
276
288
|
mergeDocument(set: [field: string, json: string][], removed: string[]): SqlFrag;
|
package/ui/Tab/Provider.tsx
CHANGED
|
@@ -147,14 +147,12 @@ export interface PanelProps {
|
|
|
147
147
|
}
|
|
148
148
|
export const Panel = ({ className, menu, children, loading = "eager" }: PanelProps) => {
|
|
149
149
|
const { menu: currentMenu } = useContext(TabContext);
|
|
150
|
-
const
|
|
151
|
-
|
|
150
|
+
const isShown = currentMenu === menu;
|
|
151
|
+
const [loaded, setLoaded] = useState(isShown);
|
|
152
|
+
const isMounted = loading === "eager" || isShown || (loading === "lazy" && loaded);
|
|
152
153
|
useEffect(() => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
else if (loading === "every") setLoaded(currentMenu === menu);
|
|
156
|
-
}, [currentMenu]);
|
|
154
|
+
setLoaded(isMounted);
|
|
155
|
+
}, [isMounted]);
|
|
157
156
|
|
|
158
|
-
|
|
159
|
-
else return loaded ? <div className={cn(className, currentMenu !== menu && "hidden")}>{children}</div> : null;
|
|
157
|
+
return isMounted ? <div className={cn(className, !isShown && "hidden")}>{children}</div> : null;
|
|
160
158
|
};
|