Repository navigation
Expand file tree
/
Copy pathtypes.ts
More file actions
4094 lines (3939 loc) · 221 KB
/
Copy pathtypes.ts
File metadata and controls
4094 lines (3939 loc) · 221 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Database Provider Types & Interfaces
* Strategy Pattern implementation for multi-database support
*/
// Re-export common types from main types file
export type {
DatabaseType,
DatabaseConnection,
ColumnSchema,
// `ObjectDetail` below is defined over these two, so a provider implementing
// `describeObject` needs them from here rather than reaching past this module (#789).
IndexSchema,
ForeignKeySchema,
ExclusionConstraintSchema,
QueryResult,
QueryWarning,
} from "../types";
import type {
DatabaseType,
DatabaseConnection,
QueryResult,
QueryWarning,
ColumnSchema,
IndexSchema,
ForeignKeySchema,
ExclusionConstraintSchema,
} from "../types";
import { declaresDialect, dialectSpec } from "./query-dialects";
// ============================================================================
// Pool Configuration
// ============================================================================
export interface PoolConfig {
/** Minimum number of connections in pool (default: 2) */
min: number;
/** Maximum number of connections in pool (default: 10) */
max: number;
/** Close idle connections after this time in ms (default: 30000) */
idleTimeout: number;
/** Wait for connection timeout in ms (default: 60000) */
acquireTimeout: number;
}
export const DEFAULT_POOL_CONFIG: PoolConfig = {
min: 2,
max: 10,
idleTimeout: 30000,
acquireTimeout: 60000,
};
/** Query timeout in milliseconds (default: 60 seconds) */
export const DEFAULT_QUERY_TIMEOUT = 60000;
// ============================================================================
// Health Information
// ============================================================================
export interface SlowQuery {
query: string;
calls: number;
avgTime: string;
}
export interface ActiveSession {
pid: number | string;
user: string;
database: string;
state: string;
query: string;
duration: string;
}
export interface HealthInfo {
/**
* The count of connections currently open, or absent when the engine cannot
* measure it at all.
*
* Optional for the same reason `DatabaseOverview.activeConnections` is: absence
* and zero are different facts, and a provider that cannot read the figure must
* omit the key rather than send a fabricated 0. This is the field the agent's
* curated "health" reading forwards to the model (`src/lib/agent/tools.ts`), so a
* fabricated 0 here is not a display quirk - it is telling the model a fact about
* a server it could not measure. Most providers compose this straight from
* `DatabaseOverview.activeConnections`, which is where the absence already comes
* from (ScyllaDB has no `system_views` keyspace; a Cassandra role can be denied
* the grant); no `?? 0` fallback belongs at that seam.
*/
activeConnections?: number;
databaseSize: string;
cacheHitRatio: string;
slowQueries: SlowQuery[];
activeSessions: ActiveSession[];
}
// ============================================================================
// Maintenance Operations
// ============================================================================
/**
* The six maintenance operations of the engines that shipped before etcd, the type each of their providers'
* own `runMaintenance` dispatches on.
*
* Kept to these six (#1089, section 7.2): `MongoDBProvider.runMaintenance` ends its work in a `switch` over
* exactly these members with no `default`, so a seventh member here makes that method's result possibly
* undefined to the compiler, and the etcd work edits no other provider's file. Every operation a provider may
* declare and be asked to run is `MaintenanceOperation`.
*/
export type MaintenanceType = "vacuum" | "analyze" | "reindex" | "kill" | "optimize" | "check";
/**
* Every maintenance operation a provider may declare in `maintenanceOperations` and be asked to run through
* `runMaintenance`: the six of `MaintenanceType`, etcd's three and Milvus's two.
*
* `compact`, `defragment` and `disarm` are etcd's history compaction, member defragmentation and alarm disarm
* (#1089, section 7.2), operations of their own rather than `vacuum` and `optimize` under other words: a reused
* `vacuum` is audited as VACUUM and read by the monitoring Tables tab's vacuum column, and `optimize` has no global
* card, so a defragmentation declared as it would have had no control at all.
*
* `load` and `release` are Milvus's per-collection Load and Release (#424): a load
* reads a collection into the query-node memory every client of the cluster shares, and a release frees it, which no
* existing member means; the maintenance route audits them as LOAD and RELEASE.
*/
export type MaintenanceOperation = MaintenanceType | "compact" | "defragment" | "disarm" | "load" | "release";
export interface MaintenanceResult {
success: boolean;
executionTime: number;
message: string;
/**
* The table this operation produced, when its answer is something to read rather
* than only a sentence. `fields` is present whenever `rows` is.
*
* Redis `analyze` is `INFO`: each `key:value` line is one row and the section it sat
* under travels with it, so the count in the message is `rows.length` (#1453). Both
* are absent on every operation that only succeeded or failed.
*/
rows?: Record<string, unknown>[];
/** Column order for `rows`. */
fields?: string[];
}
/**
* What ONE maintenance operation can be pointed at on ONE engine, declared next to
* that engine's own wording for it.
*
* `maintenanceOperations` says only that an operation EXISTS here, and #427 measured
* what a surface built on that alone offers. Oracle declares `optimize`, so the
* per-table button sent `optimize` with a TABLE name - and `oracle.ts` built
* `ALTER INDEX "<target>" REBUILD` from it, so every click answered ORA-01418
* (reproduced 2026-08-25 against Oracle AI Database 26ai Free before this change). The target
* grammar differs between engines that declare the SAME `MaintenanceType`, so a
* generic label-to-operation mapping is wrong by construction and the declaration has
* to travel with the provider.
*
* `perEntity` and `global` are independent because both halves occur: Couchbase's
* `reindex` is BUILD INDEX for ONE keyspace and has no whole-database form (its global
* card answered *"The reindex operation requires a target"*), while SQLite's `VACUUM`
* rewrites the whole file and ignores a target entirely (a per-table control there
* names one table and vacuums the database). An operation whose target is a session or
* query id - every engine's `kill` - is `false` for both: the Sessions panel supplies
* that id, and no table or global control can.
*/
export interface MaintenanceOperationSpec {
/** This engine's own wording for a control that runs the operation. */
label: string;
/** Runs against ONE object the browser lists: a table, a collection, a keyspace. */
perEntity: boolean;
/** Runs with no target at all, over the whole database. */
global: boolean;
/**
* The heading of this operation's own card in the Operations tab's Global Operations section (#1089, section 7.2).
*
* Declaring it IS the request for that card: the tab draws one card, with `label` on its button, this heading and
* `description` as its body, for every operation `maintenanceControl` offers globally with a title, and withholds
* the card it would otherwise draw for the operation from `ProviderLabels`. Absent keeps the operation on the
* tab's own cards, which is every provider that shipped before the field existed.
*/
title?: string;
/** The body of this operation's card, and of its typed dialog when it asks for one (#1089, section 7.2). */
description?: string;
/**
* How a control of this operation is confirmed before it sends anything.
*
* `"typed"`: the operation's card asks for the connection's name, typed exactly (#1089, section 7.2). Honoured on a
* declared card only, so a spec that asks declares `title`, `description` and `perEntity: false`: no per-row
* control asks, and the typed dialog takes its title and body from those two fields.
*
* `"typed-target"`: a per-row control asks for its target object's own name, typed exactly and case-sensitively,
* never a fixed word and never the connection's name (spec 3.11). Honoured on a per-row control only, so a spec
* that asks declares `perEntity: true` and `global: false`: a whole-database run has no object to name.
*
* `tests/unit/db/maintenance-confirmation-capability.test.ts` holds every shipped provider to both rules.
*/
confirmation?: "typed" | "typed-target";
/**
* The per-row control's dialog reads `POST /api/db/maintenance/preview` and shows the provider's
* `MaintenancePreview` before it offers the confirm button (spec 3.11). Declared only beside an implemented
* `DatabaseProvider.previewMaintenance`: the route answers 400 for an operation whose spec does not declare it.
*/
preview?: true;
/**
* The object kinds a per-entity control of this operation runs on, by kind id (#786).
*
* Absent means every relation kind, which is what every surface offered before the field
* existed. Db2 declares it because RUNSTATS and REORG run on a table and a materialized query
* table and are refused on a view (SQLSTATE 428DY), and its views are relations too.
*/
kinds?: readonly string[];
}
/**
* The grammar the Create Table form writes for this engine (`src/components/CreateTableModal.tsx`).
* Plain data: `/api/db/provider-meta` sends capabilities to the browser as JSON.
* Each engine's measured reasons live in `docs/providers/<type-id>.md`, section "Create Table form".
*/
export interface CreateTableSpec {
/**
* The auto-increment primary key. Absent: the engine cannot express one and the form does not offer it.
* `clause` is everything after the column NAME, `PRIMARY KEY` included, stored whole because engines
* disagree on where `PRIMARY KEY` sits inside it. `sequence`: the clause defaults from a sequence the
* form creates first in the table's own container (`CREATE SEQUENCE IF NOT EXISTS`, `DEFAULT nextval(...)`).
*/
readonly autoIncrement?: { readonly clause: string; readonly sequence?: true };
/** Whether `PRIMARY KEY` and `UNIQUE` are column constraints this engine takes. False hides both checkboxes; NOT NULL stays. */
readonly columnConstraints: boolean;
/** The plain integer type of a key column that is not auto-increment. Must be one of `types`, or the dropdown trigger renders empty. */
readonly keyType: string;
/** The types the dropdown lists, in order; the first is an added column's default. */
readonly types: readonly [string, ...string[]];
}
/**
* What one per-row maintenance operation will do, as its provider reads it before an admin confirms (spec 3.11).
*
* Answered by `DatabaseProvider.previewMaintenance` through `POST /api/db/maintenance/preview`, and published:
* `src/exports/types.ts` names it.
*/
export interface MaintenancePreview {
/** One sentence saying what the operation will do to this object. */
readonly summary: string;
/** The figures the summary rests on, each printed as a label and its value. */
readonly facts: readonly { readonly label: string; readonly value: string }[];
/** A preflight that refuses the operation: the dialog shows it and offers no confirm button. */
readonly refusal?: string;
/** How fresh or exact the facts are, for example "as reported by the server, possibly several seconds old". */
readonly note?: string;
}
/** One rule of a `PreviewProjection`: which declared types it reads, and how. */
export interface PreviewProjectionRule {
/**
* A regular expression, as source text so the declaration stays plain data, tested against
* `ColumnSchema.type`. The author anchors it.
*/
readonly type: string;
/**
* The expression the column is read through, `{column}` standing for the quoted column name,
* or null to leave the column out of the preview.
*/
readonly expression: string | null;
}
/** How a preview reads its columns (see `ProviderCapabilities.previewProjection`). */
export interface PreviewProjection {
/** Tested in order; the first match wins, and a column no rule matches is read as it is. */
readonly rules: readonly PreviewProjectionRule[];
/** Why a left-out column is not read, written after the list of them in the comment. */
readonly omittedNote: string;
/** The comment above a preview whose column list was not loaded, so nothing could be projected. */
readonly unprojectedNote: string;
}
/**
* A preview that reads a recent window, newest first (see `ProviderCapabilities.previewTimeWindow`):
* `SELECT * FROM <table> WHERE <column> >= <since> ORDER BY <column> DESC`, under `note`.
*/
export interface PreviewTimeWindow {
/** The time column, written through `quoteIdentifier`. */
readonly column: string;
/** The window's lower bound as the engine's own expression, InfluxDB 3's `now() - INTERVAL '1 hour'`. */
readonly since: string;
/**
* The preview's first comment line, written without its `-- `: why an empty preview is empty.
* Nothing reads it back, and no provider recognises its own generated text.
*/
readonly note: string;
/**
* The commented lines Generate Query writes below its statement, each without its `-- `.
* `{table}` is filled with the quoted table and `{column}` with the first `float` or
* `integer` column of the described columns, quoted, else `"value"`.
*/
readonly examples: readonly string[];
}
/** Where a surface wants to put a control: on one row, or on a whole-database card. */
export type MaintenancePlacement = "perEntity" | "global";
/**
* Whether ONE maintenance control may be offered in ONE place, and the wording to
* give it - the single gate both maintenance surfaces ask, so that they cannot
* disagree about what a provider declared.
*
* `label` is undefined rather than a fallback string on purpose: only the provider
* may name the operation, so a caller that gets no name keeps its own generic word.
* That is also the whole of the compatibility story for the optional
* `maintenanceOperationSpecs` - a provider that declares no spec is offered in both
* placements under the caller's own wording, which is what both surfaces did before
* #U9.
*
* A spec's card fields, `title`, `description` and `confirmation`, travel with the answer
* only where the spec declares them, for the Operations tab's declared cards (#1089,
* section 7.2): a spec that declares none answers exactly what it answered before they
* existed. `preview` travels on the same terms, for the per-row dialog (spec 3.11).
*/
export function maintenanceControl(
capabilities: ProviderCapabilities | undefined,
type: MaintenanceOperation,
placement: MaintenancePlacement,
kind?: string,
): {
offered: boolean;
label?: string;
title?: string;
description?: string;
confirmation?: "typed" | "typed-target";
preview?: true;
} {
// Unknown capabilities are not a permission: `/api/db/provider-meta` answers with
// nothing both while it is in flight and when it failed, and failing open there
// puts the dead buttons back on exactly the connections the #272/#282 gates exist
// for.
if (capabilities?.supportsMaintenance !== true || !capabilities.maintenanceOperations.includes(type)) {
return { offered: false };
}
const spec = capabilities.maintenanceOperationSpecs?.[type];
if (spec === undefined) {
return { offered: true };
}
// A row of a kind the operation does not run on is offered nothing. Only a caller that names
// the row's kind is asked this, so the Operations and Tables tabs, which list tables, are not.
const kindRefused = kind !== undefined && spec.kinds !== undefined && !spec.kinds.includes(kind);
return {
offered: spec[placement] && !kindRefused,
label: spec.label,
...(spec.title === undefined ? {} : { title: spec.title }),
...(spec.description === undefined ? {} : { description: spec.description }),
...(spec.confirmation === undefined ? {} : { confirmation: spec.confirmation }),
...(spec.preview === undefined ? {} : { preview: spec.preview }),
};
}
/** The placements of one maintenance operation a connected server accepted, as a provider measured them (#1387). */
export type MeasuredMaintenancePlacements = Readonly<Record<MaintenancePlacement, boolean>>;
/** The maintenance half of `ProviderCapabilities`: the operations and how each is targeted. */
export type MaintenanceDeclaration = Pick<ProviderCapabilities, "maintenanceOperations" | "maintenanceOperationSpecs">;
/**
* A provider's declared maintenance narrowed to what the connected server accepted (#1387).
*
* The PostgreSQL and MySQL type ids each serve a family of wire-compatible engines, and the
* declaration is the engine-family default: CockroachDB, RisingWave, YugabyteDB, TiDB and Vitess
* each refuse part of it. A provider that measures its server at connect hands the answer here,
* one entry per operation it asked about, and the declaration loses every placement the server
* refused. An operation left with no placement leaves the list, so the gate both surfaces ask
* (`maintenanceControl`) offers it nowhere and the route refuses it before the engine is reached.
*
* An operation the measurement does not name keeps its declaration: `kill` takes a session id and
* no probe here can ask about it. `measured` undefined is "not measured", the unconnected provider
* `POST /api/db/provider-meta` reads, and answers the declaration unchanged.
*/
export function narrowMaintenance(
declared: Required<MaintenanceDeclaration>,
measured: Partial<Record<MaintenanceOperation, MeasuredMaintenancePlacements>> | undefined,
): Required<MaintenanceDeclaration> {
if (measured === undefined) return declared;
const maintenanceOperations: MaintenanceOperation[] = [];
const maintenanceOperationSpecs: Partial<Record<MaintenanceOperation, MaintenanceOperationSpec>> = {};
for (const operation of declared.maintenanceOperations) {
const spec = declared.maintenanceOperationSpecs[operation];
const accepted = measured[operation];
if (spec === undefined || accepted === undefined) {
maintenanceOperations.push(operation);
if (spec !== undefined) maintenanceOperationSpecs[operation] = spec;
continue;
}
const perEntity = spec.perEntity && accepted.perEntity;
const global = spec.global && accepted.global;
if (!perEntity && !global) continue;
maintenanceOperations.push(operation);
maintenanceOperationSpecs[operation] = { ...spec, perEntity, global };
}
return { maintenanceOperations, maintenanceOperationSpecs };
}
/**
* Declared capabilities with the CONNECTED provider's maintenance laid over them (#1387).
*
* The two maintenance surfaces read capabilities from `POST /api/db/provider-meta`, which never
* connects (#457), so it answers the type id's declaration and not what this server accepts.
* `POST /api/db/monitoring` does connect, and its payload carries the connected provider's
* maintenance declaration as `maintenance`; the tabs fed by it take that half from there. Either
* side absent answers the declared capabilities as they are, so an embedded host whose monitoring
* payload carries no `maintenance` sees exactly what it saw before.
*/
export function withConnectedMaintenance(
capabilities: ProviderCapabilities | undefined,
connected: MaintenanceDeclaration | undefined,
): ProviderCapabilities | undefined {
if (capabilities === undefined || connected === undefined) return capabilities;
return { ...capabilities, ...connected };
}
/**
* Whether a session row may offer Terminate, asked by both session lists (#1424).
*
* `/api/db/maintenance` refuses `kill` unless the provider declares maintenance and lists `kill`
* in `maintenanceOperations`, so a control drawn anywhere else can only produce that refusal.
* Unknown capabilities are not a permission, for the reason `maintenanceControl` gives.
*/
export function offersSessionTermination(capabilities: ProviderCapabilities | undefined): boolean {
return capabilities?.supportsMaintenance === true && capabilities.maintenanceOperations.includes("kill");
}
/**
* The six members of `MaintenanceType`, as a value. A record rather than a list, so a seventh member of the type
* fails to compile here until it is placed.
*/
const MAINTENANCE_TYPE_MEMBERS: Readonly<Record<MaintenanceType, true>> = {
vacuum: true,
analyze: true,
reindex: true,
kill: true,
optimize: true,
check: true,
};
/** One per-row control a provider declares outside `MaintenanceType` (spec 3.11). */
export interface DeclaredEntityOperation {
readonly type: MaintenanceOperation;
/** The spec's `label`, the only wording the control has: no surface has a generic verb for it. */
readonly label: string;
}
/**
* Every declared operation outside `MaintenanceType` that a per-row control may offer, in declaration order (spec 3.11).
*
* The four per-row surfaces, the Operations tab, the monitoring Tables tab and both row menus, draw their own
* candidates from `MaintenanceType`, with their own icons and fallback verbs, and append these after them under a
* generic icon. An operation is listed only where `maintenanceControl` offers it per row and its spec names it:
* unknown capabilities, an engine with no maintenance and an operation with no spec list nothing, because an operation
* outside `MaintenanceType` has no generic wording to fall back on. etcd's three declare `perEntity: false`.
*
* A row menu names its row's `kind`, and is then offered an operation only on the kinds its spec names (#786); the two
* tabs list tables and name none, as they do for their own candidates.
*/
export function declaredEntityOperations(
capabilities: ProviderCapabilities | undefined,
kind?: string,
): readonly DeclaredEntityOperation[] {
return [...new Set(capabilities?.maintenanceOperations ?? [])].flatMap((type): DeclaredEntityOperation[] => {
if (Object.hasOwn(MAINTENANCE_TYPE_MEMBERS, type)) return [];
const control = maintenanceControl(capabilities, type, "perEntity", kind);
return control.offered && control.label !== undefined ? [{ type, label: control.label }] : [];
});
}
/**
* What a typed confirmation asks the person to type (#1089, section 5.5): a text, such as the key prefix a range
* delete names, or the connection's name, with every target the statement reaches listed beside the field.
*
* Answered by a confirmation-gate vocabulary's `typedConfirmation` (`src/lib/db/destructive-commands.ts`) and drawn
* by `QuerySafetyDialog`. Not published: `src/exports/types.ts` does not name it.
*/
export type TypedConfirmationAsk =
| { readonly type: "text"; readonly text: string }
| { readonly type: "connection-name"; readonly targets: readonly string[] };
/**
* Whether column profiling may be offered for this engine: the one gate both row menus
* (`src/components/object-tree/row-actions.ts` and the mobile
* `src/components/schema-explorer/TableItem.tsx`) and `POST /api/db/profile` ask, so that no
* menu offers an action the route refuses.
*
* The route writes exactly two statement shapes: SQL aggregates, and a MongoDB `aggregate`
* document with a `$sample` stage. So profiling is offered for `"sql"`, and for `"json"` only
* when no `queryDialect` says the JSON is some other grammar. Several engines, among them Redis, LibreDB, Kafka
* and etcd, declare
* `"json"` with a dialect of their own, a Kafka read request being JSON of this product's own
* schema (#1088) and an etcd command a line of etcdctl's (#1089), and `"promql"` is not JSON at all;
* before this gate the route sent Redis,
* LibreDB and Prometheus the MongoDB document, which only MongoDB reads (#1085).
*
* A dialect's answer is its record's `offersColumnProfiling` in `QUERY_DIALECTS`
* (`src/lib/db/query-dialects.ts`), false for every dialect there. It is read beside `"json"` only: the language is
* first, so a dialect declared beside `"sql"` is offered profiling, while `offersCountQuery` below withholds its
* count. The asymmetry is kept on purpose, because changing either answer would change what a menu offers. A
* declared dialect with no record, which only a host's own declaration can name, is refused, as it always was.
*
* An engine declaring `supportsColumnProfiling: false` is refused before the language rule, whatever it speaks.
*
* Unknown capabilities are not a permission, for the reason `maintenanceControl` gives:
* `/api/db/provider-meta` answers with nothing both while it is in flight and when it failed.
*/
export function offersColumnProfiling(capabilities: ProviderCapabilities | undefined): boolean {
if (capabilities === undefined) return false;
if (capabilities.supportsColumnProfiling === false) return false;
if (capabilities.queryLanguage === "sql") return true;
const profilesDialect = dialectSpec(capabilities)?.offersColumnProfiling ?? !declaresDialect(capabilities);
return capabilities.queryLanguage === "json" && profilesDialect;
}
/**
* Whether the code generator may be offered for this engine, asked by both row menus.
*
* `src/components/CodeGenerator.tsx` maps an object's columns onto a TypeScript interface, a Zod
* schema, a Prisma model, a Go struct, a Python dataclass and a Java POJO: the models an
* application writes over a table or a document collection. It is offered for `"sql"` and
* `"json"`, dialects included, because it names the row rather than addressing it, which is why
* Redis and LibreDB keep it (#427). A PromQL metric's columns are its label names, and a record
* type over them models nothing an application stores, so `"promql"` is not offered it (#1085).
* The two languages are named rather than `"promql"` excluded, so a language added later is not
* offered the generator until somebody decides that it should be.
*
* A dialect refuses it through its record's `offersCodeGeneration: false` in `QUERY_DIALECTS`
* (`src/lib/db/query-dialects.ts`), and a record that says `true` leaves the answer to the language.
*
* The `"kafka"` dialect's record refuses it, for the reason PromQL is (#1088): a topic's
* columns are the fixed shape of a read result, not a record an application stores, and the models
* written over them reject the rows a read returns, a `Date` for a timestamp that arrives as an ISO
* string and a record type for a value that arrives as text, base64 or a Confluent schema label.
*
* The `"etcd"` dialect's record refuses it too (#1089, section 3.3): a key-prefix group's columns are
* the fixed shape of a `get` row, and a model written over them is not a record an application stores in
* a key-value store.
*
* Unknown capabilities are not a permission, as for `offersColumnProfiling`.
*/
export function offersCodeGeneration(capabilities: ProviderCapabilities | undefined): boolean {
if (dialectSpec(capabilities)?.offersCodeGeneration === false) return false;
return capabilities?.queryLanguage === "sql" || capabilities?.queryLanguage === "json";
}
/**
* Whether "Generate Count Query" may be offered for this engine, asked by both row menus and by
* `generateCountQuery` itself (#702).
*
* The generator writes two shapes: a SQL `SELECT COUNT(*)`, and a MongoDB `count` document. So
* it is offered for `"sql"` and `"json"`, and only when no `queryDialect` says the language is
* some other grammar. The dialect is read for both languages rather than for JSON alone, as
* `offersColumnProfiling` does, so that a dialect declared on a SQL engine later refuses the
* action until somebody writes its count. Redis and LibreDB have no count
* statement in their command grammars, Kafka's read request reads a topic's messages and counts
* none (#1088), etcd declares a dialect and derived groupings both, either of which withholds it
* (#1089), and `"promql"` is not offered it either: `count()` in
* PromQL counts series at an instant, which is not the row count this action promises.
*
* A declared dialect is offered it only where its record's `offersCountQuery` in `QUERY_DIALECTS`
* (`src/lib/db/query-dialects.ts`) says so, which Firestore's alone does, and a declared dialect with no record is refused.
*
* A derived grouping is refused on top of the language, because a Redis `user:*` row is a
* summary this server built and there is no object to address (#427).
*
* Unknown capabilities are not a permission, as for `offersColumnProfiling`.
*/
export function offersCountQuery(capabilities: ProviderCapabilities | undefined): boolean {
if (capabilities === undefined) return false;
if (capabilities.tablesAreDerivedGroupings === true) return false;
if (declaresDialect(capabilities) && dialectSpec(capabilities)?.offersCountQuery !== true) return false;
return capabilities.queryLanguage === "sql" || capabilities.queryLanguage === "json";
}
/**
* Whether the result export menu offers SQL `INSERT` and `CREATE TABLE` DDL, asked by `BottomPanel` for both its
* Export and its Copy items (BACKLOG U69).
*
* Both formats write a table of rows for a SQL database. A result whose records are not rows of such a table
* cannot become one: a vector cell is written as quoted text into a `TEXT` column, a file that loads nowhere as a
* vector. A dialect declines them through its record's `offersSqlExport: false` in `QUERY_DIALECTS`
* (`src/lib/db/query-dialects.ts`).
*
* Everything else keeps them, unlike the row-menu gates above: an absent field, no dialect, a dialect with no
* record, and capabilities that have not arrived. The menu offered both to every engine before the field existed,
* so each of those answers is the menu as it was, and an export the user asks for writes a file from rows already
* on screen, which is no permission this gate guards.
*/
export function offersSqlExport(capabilities: ProviderCapabilities | undefined): boolean {
return dialectSpec(capabilities)?.offersSqlExport !== false;
}
/**
* Whether the schema diagram (ERD) may be offered for this engine, asked by every entry point that opens
* it: the sidebar button and the command palette item, in both shells.
*
* The diagram draws objects as tables and infers an edge from a column named like another table's
* `_id`. A graph's relationship types are not tables and its edges are data, not columns, so on a
* `"cypher"` connection the heuristic would invent edges and draw none of the real ones (Neo4j spec, SR20).
* Every other language keeps the diagram it always had, and so does a connection whose capabilities have
* not answered yet: the entry point was offered before this gate existed, and only a declared Cypher is
* known not to fit it.
*/
export function offersSchemaDiagram(capabilities: ProviderCapabilities | undefined): boolean {
// InfluxDB spec 6.3: a measurement's columns are the union of the tag and field keys its points happened to carry,
// read with two SHOW statements per measurement, so a diagram would cost two requests per box to draw a schema the
// engine never declares, with no relation between boxes.
if (capabilities?.queryLanguage === "influxql") return false;
return capabilities?.queryLanguage !== "cypher";
}
// ============================================================================
// Provider Capabilities & Labels
// ============================================================================
/**
* Dialect discriminant for client-side EXPLAIN handling (issue #194).
* Each id selects one strategy module in `src/lib/explain`. Extended per
* provider as explain support lands.
*/
export type ExplainFormat =
| "postgres-json"
| "postgres-text"
| "postgres-text-analyze"
| "mysql-json"
| "mysql-text"
| "sqlite-queryplan"
| "couchbase-json"
| "clickhouse-json"
| "druid-native"
| "trino-json"
| "duckdb-json"
| "databend-text"
| "bigquery-dryrun"
| "snowflake-json"
| "firestore-explain"
| "spark-text";
/**
* How deep an engine's container chain is, in the TYPE rather than only in a derivation.
*
* Phase 1's tree models zero, one or two levels and `containerDepth()` answers `0 | 1 | 2`, so a
* provider declaring a third level used to have it silently clamped away: the level would exist in
* the declaration, `ContainerLevelSpec.level` indices would still point at it, and nothing would
* read it. Spelling the ceiling as a tuple union makes that declaration a compile error at the
* provider that writes it, which is where the person who meant to add it is standing. No engine in
* this repository declares three; the first one that needs to is a Phase 2 question about the tree,
* not a number to widen here (#789).
*/
export type ContainerLevels =
| readonly []
| readonly [ContainerLevelSpec]
| readonly [ContainerLevelSpec, ContainerLevelSpec];
/**
* How an engine lists its key space ONE LEVEL AT A TIME under `separator`, for an engine whose server
* groups keys into folder prefixes (S3's `delimiter` and `CommonPrefixes`).
* Absent: the walk pages keys only, which is every walk declared before this field existed.
*/
export interface KeyScanLevels {
/**
* The object kind whose rows name the key space's FIRST SEGMENT, when one does: a row of this kind
* named `sales` is the folder `sales<separator>` of the walk. The row menu offers Browse Keys on it.
* Only for an engine with no container level: a key space per container has no first segment
* that a row could name.
* Absent: no tree row stands for a folder of the walk.
*/
readonly rootKind?: string;
}
/**
* Whether an engine can page a resumable walk of its own KEY SPACE, and the batch sizes it
* will accept.
*
* THE ENGINES THIS EXISTS FOR ARE THE ONES WITH NO CATALOG. Most engines answer
* `listObjects` from a stored definition — a table, a collection, an index — and a stored
* definition is enumerable in full. A Redis key is not: there is no prefix index and no
* directory, so the only way to learn what exists is `SCAN`, and `SCAN` is a CURSOR rather
* than a listing. A caller who stops at one batch holds a SAMPLE; a caller who wants the
* whole key space has to come back with the cursor it was handed.
*
* That difference is why this is a capability and not an object kind. `listObjects` answers
* "what is here" in one call and is allowed to be finite. This answers "here is the next
* batch and the cursor after it", which is a different contract, and expressing it as an
* object kind would have to lie about either the bound or the completeness.
*
* The declaration is STATIC, like `objectKinds` and for a related reason: it states what the
* PROVIDER can do, not what the connected server answered. A Redis-wire relative that
* refuses `SCAN` would be a different provider, not a different capability.
*
* THE WALK'S SHAPE IS DECLARED TOO, in four optional fields read only through `keyScanShape` below
* (spec 3.4): which string separates a key's segments, how the cursor is spelled, what the pattern
* is, and what the total counts. An absent field is Redis's walk, the one this interface described
* before the fields existed, so a declaration written then still means what it meant. etcd declares
* all four: a `/` convention, a cursor only it can read, a literal prefix rather than a glob, and a
* total that counts the range the walk covers.
*/
export interface KeyScanCapability {
/** Batch size used when the caller names none. */
readonly defaultCount: number;
/**
* Largest batch this provider forwards. A caller asking for more is REFUSED rather than
* clamped: a silent clamp answers a request for 10,000 with a batch of 1,000 and says
* nothing, which is a wrong answer about what a batch is. The caller can ask again.
*/
readonly maxCount: number;
/**
* What separates one segment of a key from the next in the panel's tree, and in the prefix a
* folder stands for. Absent reads `":"`, Redis's convention; etcd's is `"/"`.
*/
readonly separator?: string;
/**
* How the cursor is spelled. Absent reads `"decimal"`, a run of digits the route shape-checks, as a
* Redis cursor is; `"opaque"` is a string only the provider that wrote it can read, which the route
* passes through untouched. `"0"` starts and ends a walk under both.
*/
readonly cursor?: "decimal" | "opaque";
/**
* What `KeyScanOptions.pattern` is. Absent reads `"glob"`, a `MATCH` pattern whose prefix half the
* panel escapes; `"prefix"` is the literal bytes every walked key begins with, unescaped, untrimmed
* and with no `*`.
*/
readonly pattern?: "glob" | "prefix";
/**
* What `KeyScanPage.total` counts. Absent reads `"database"`, the engine's own count of the database
* walked; `"walk"` is the exact count of the keys this walk covers at its pinned revision. `"none"` is
* an engine that publishes no count and pins no revision: `total` is not read, and a provider answers 0.
*/
readonly totalScope?: "database" | "walk" | "none";
/**
* Present iff `scanKeysPage` also answers a LEVEL page: asked with `KeyScanOptions.level`, it returns
* the keys directly under the pattern and the folder prefixes one separator deeper, each folder once,
* at most `count` entries in all, and the cursor of that level. Requires `pattern: "prefix"`,
* `totalScope: "none"` and a separator one UTF-16 code unit long, such as `/`.
* A provider that declares it still answers a walk without `level` exactly as before.
*
* THE LEVEL-PROVIDER CONTRACT (Keys panel levels, spec 3.3). A level page never holds more than `count`
* entries, keys and prefixes together, whatever the server returns in one response, and no prefix or
* key twice. A provider whose server answers a level unpaged removes repeated entries, sorts the full answer
* by UTF-8 byte order, returns the first `count` entries after the cursor, and spells its cursor as the
* last entry returned (start-after semantics), so a list that changes order between calls neither skips
* nor repeats an entry. A level cursor is valid in every process that serves the connection: a provider
* may bind it to the request's scope but not to its own instance. The route enforces the count and the
* no-repeat rule; the ordering and cursor rules are the provider's to meet.
*/
readonly levels?: KeyScanLevels;
}
/**
* A key-space walk's shape with every field present: what `keyScanShape` answers for a declaration.
*
* A TYPE OF ITS OWN SO A READER HOLDS THE ANSWER AND NOT THE QUESTION. A panel that read
* `capability.separator ?? ":"` in one place and `capability.separator` in another would draw one key
* space two ways; a reader handed this has no default left to forget.
*/
export interface KeyScanShape {
readonly separator: string;
readonly cursor: "decimal" | "opaque";
readonly pattern: "glob" | "prefix";
readonly totalScope: "database" | "walk" | "none";
}
/**
* The one reader of `KeyScanCapability`'s four optional fields (spec 3.4).
*
* An absent field is today's walk, `":"`, `"decimal"`, `"glob"` and `"database"`: the compatibility
* rule `maintenanceControl` states for the optional `maintenanceOperationSpecs`, which is why Redis's
* declaration, naming none of the four, keeps its panel as it was.
*/
export function keyScanShape(capability: KeyScanCapability): KeyScanShape {
return {
separator: capability.separator ?? ":",
cursor: capability.cursor ?? "decimal",
pattern: capability.pattern ?? "glob",
totalScope: capability.totalScope ?? "database",
};
}
export interface KeyScanOptions {
/** The cursor the previous page answered with, in the declaration's `cursor` spelling; `"0"` starts a walk. */
readonly cursor: string;
/**
* The walk's pattern in the declaration's `pattern` shape, or omitted for every key: a `MATCH` glob
* under `"glob"`, and under `"prefix"` the literal prefix every walked key begins with.
*/
readonly pattern?: string;
/** Batch size, within `[1, maxCount]`. */
readonly count: number;
/**
* Which numbered database to walk, for an engine that declares a container level to name. An engine
* that declares none walks one key space, and both key routes refuse the field for it (spec 3.4).
*/
readonly database?: number;
/**
* List one level: the keys whose text after `pattern` holds no separator, and the folder prefixes
* that end at the first separator after `pattern`. Sent only to an engine that declares
* `KeyScanCapability.levels`; absent is the key walk every engine answers.
*/
readonly level?: true;
}
export interface KeyScanPage {
/** The keys this batch returned. NOT deduplicated: `SCAN` may repeat a key. */
readonly keys: readonly string[];
/** The cursor for the next batch. `"0"` means this walk reached the end. */
readonly cursor: string;
/**
* Each key's value type, by key name.
*
* IT TRAVELS WITH THE PAGE RATHER THAN BEING ASKED FOR SEPARATELY, because the type is what makes
* a key row readable and a panel that fetched it afterwards would draw a list without it and then
* fill it in. `TYPE` is a single-key command — Redis publishes no batch form — so the provider
* pipelines one per key in the page: the cost is ONE extra round trip per page whatever the page
* holds, not one per key.
*
* A KEY ABSENT FROM THIS MAP IS ONE WHOSE TYPE COULD NOT BE READ, and the honest thing to draw is
* nothing. A key that vanished between the walk and this read is present, with the server's own
* answer for it (`"none"`): a row the sample says is there, beside a type that says it is not, is
* a true pair and a reader can act on it.
*
* WHAT IT DESCRIBES IS THE MOMENT IT WAS READ. The walk is a sample and so is this: a key whose
* type changed between two pages is described by the earlier page's answer for as long as that
* answer is what the caller holds.
*
* An engine that declares a key-browser kind opens a key's Source tab rather than a typed read, so the
* value it carries here is not read as a type; such an engine may carry a short descriptor of the key
* instead, which the panel draws in the same cell.
*/
readonly types: Readonly<Record<string, string>>;
/**
* What a progress indicator divides by, in the scope the declaration's `totalScope` names (spec 4.6).
*
* UNDER `"database"`, THE ABSENT DEFAULT, IT IS SERVER-WIDE AND NOT THE WALK'S OWN TOTAL, which is
* why it looks like a naive field. A `SCAN` cursor says nothing about how much is left, so no
* denominator can be computed from the batches a caller has already seen; this is the one number the
* engine publishes (`DBSIZE`, which is O(1)). On a clustered deployment it is the LOCAL node's key
* count, because `DBSIZE` has no cluster-wide form and `SCAN` walks one node's slots: a panel drawing
* 2000/6355 there is showing a fraction of one node and not of the cluster.
*
* UNDER `"walk"` IT IS THE EXACT COUNT OF THE KEYS THIS WALK COVERS at the revision its pages are
* pinned to: the pattern's prefix range, or the whole key space, and for a caller whose grants are
* narrower, the keys of the ranges it may read (spec 4.7). Every page answers it, and a panel replaces
* its total with each page's.
*
* UNDER `"none"` IT IS NOT READ. `"none"` is an engine that publishes no count and pins no revision:
* `total` is not read, and a provider answers 0.
*/
readonly total: number;
/**
* Whether this answer is about ONE NODE of a clustered deployment.
*
* `SCAN` and `DBSIZE` are per node and neither has a cluster-wide form, so on a cluster the keys
* and the count describe the node that answered and nothing else. Absent means the deployment
* does not say it is clustered, which is the ordinary server.
*/
readonly clustered?: boolean;
/**
* Keys this page read and left out, and why: present only when it left any out (spec 4.6).
*
* A KEY IS LEFT OUT WHEN NO NAME A ROW COULD CARRY ADDRESSES IT. etcd keys are bytes, and one that
* is not UTF-8 text, decoded with replacement characters, would name a different key, so it is
* counted here rather than listed; a typed read shows it in base64. `reason` is the provider's own
* words. Redis's pages never carry it.
*/
readonly skipped?: { readonly count: number; readonly reason: string };
/**
* What the provider can and cannot establish about this read's completeness, for an engine where that is
* not a settled question (#1635).
*
* ABSENT IS THE ORDINARY CASE AND MEANS NOTHING IS CLAIMED EITHER WAY. An engine that walks a key space it
* can see the end of says nothing here, exactly as it always did; the panel draws no line for it.
*
* `scope` has two values and `complete` is not one of them. An engine declares `incomplete` only where it
* can PROVE the read is short - a refused subscription, a server that closed the connection, a message
* skipped for its size - and `unknown` everywhere else. A provider that inferred incompleteness from
* timing would be guessing: measured on MQTT, the quiet gap before a lossless read and before a lossy one
* do not separate and their ordering is inverted.
*
* `endReason` is why the read stopped, in the provider's own words, and is never empty. `provenLoss` lists
* what the provider can show was lost, each entry its own sentence; an empty list means nothing was proved
* lost, never that nothing was.
*/
readonly readOutcome?: {
readonly scope: "incomplete" | "unknown";
readonly endReason: string;
readonly provenLoss?: readonly string[];
};
/**
* The folder prefixes of a LEVEL page, each the full prefix ending in the separator, each one
* separator deeper than the pattern asked about, none twice. Present only on an answer to `level`;
* a folder is complete for its level, so a reader lists it by asking for its own level and never
* infers it from keys. Counted with `keys` against the page's `count`.
*/
readonly prefixes?: readonly string[];
}
/**
* One action a row of a NON-RELATION kind offers, declared by the provider that writes its statement.
*
* The row menu offers it on every object of a kind named in `kinds`, and choosing it opens a new tab
* that runs what the dialect's `rowAction` generator writes for that object (`src/lib/query-generators.ts`).
* `id` is what the generator is asked for and stays stable; `label` is what the menu and the tab read.
*
* A row action reads and never writes: it runs on click, with no role asked and no editing step, so
* one that wrote would put a write behind an ungated menu item. Its statement honours
* `GeneratorScope.readOnly` like every other generated read.
*/
export interface KindRowAction {
readonly id: string;
readonly label: string;
/** The `ObjectKindSpec.id`s whose rows offer this action. */
readonly kinds: readonly string[];
/**
* Activating a row of these kinds (a click, Enter or Space in the object tree) runs this action
* instead of opening the kind's source, the way activating a relation previews its rows.
*
* At most one action may declare it for a kind: one gesture has one behaviour per row, so a second
* is a declaration error that `activationRowActionForKind` raises. A relation kind's data preview
* still wins over it. Absent means the action runs from the row menu only.
*/
readonly activates?: true;
}
export interface ProviderCapabilities {
/**
* The language this engine's statements are written in: what its editor tabs are typed and
* highlighted as (`src/lib/editor/tab-language.ts`), and the arm the query generators take for a
* tree click and for "Generate Query" (`src/lib/query-generators.ts`).
*
* A CLOSED union, and a member added to it is not neutral: a reader written `=== "json"` sends
* the new member into its SQL branch, and one written `!== "sql"` sends it into its JSON
* (MongoDB) branch. So a new member lands with an explicit arm in every reader, or with a test
* pinning that the branch it falls into is right for it. `"promql"` is the Prometheus provider's
* (#1085), and it declares no `queryDialect`, because PromQL is not a kind of JSON.
* `"cypher"` is the graph providers' (Neo4j spec 6.5), declared with no `queryDialect` for the
* same reason: Cypher is neither JSON nor SQL. Its tabs render in the `graph-cypher` language, a
* tree click writes a bounded Cypher read, and the count, profiling and code-generation gates
* below refuse it by naming the languages they serve.
* `"influxql"` is the InfluxDB provider's (InfluxDB spec I12), declared with no `queryDialect`
* because InfluxQL is neither JSON nor SQL. Its tabs render in the `influxql` language over the
* provider's own lexer, a tree click writes a time-windowed newest-first read, and the count,
* profiling and code-generation gates refuse it by naming the languages they serve.
*
* Published through `src/exports/types.ts`, so widening it breaks a consumer's exhaustive
* switch over it; that ships with a release note, not a compatibility layer.
*/
queryLanguage: "sql" | "json" | "promql" | "cypher" | "influxql";
/**
* Optional client-side query dialect, declared only beside `queryLanguage: "json"`, where it
* names the grammar the editor text really is: JSON of this product's own schema (Kafka) or a
* command line (Redis, LibreDB, etcd), for which `"json"` means only "not SQL". `queryLanguage`
* says SQL, JSON, PromQL, Cypher or InfluxQL; for a `"json"` provider the query generators otherwise
* assume MongoDB syntax.
* A provider sets `queryDialect` to opt its tables into a custom client-side
* generator (see `query-generators.ts`), and it is checked BEFORE
* `queryLanguage` everywhere. Left undefined by SQL and MongoDB, so their
* generation is unchanged; Redis declares `"redis"` because it too says
* `queryLanguage: "json"` while speaking neither MongoDB JSON nor SQL, and
* silently got MongoDB commands its own driver rejected (#427).
*
* `"kafka"` is the Kafka provider's (#1088): its editor text is JSON, a read request of this
* product's own schema, so it declares `"json"` with this dialect rather than a language of its
* own. A reader keyed on `"json"` alone treats that text as MongoDB, which is the #427 class, so
* the member lands with an explicit arm in every reader of either field, or with a test pinning
* that the branch it falls into is right for Kafka. Widening this published union breaks a
* consumer's exhaustive switch over it, which ships with a release note, as `queryLanguage`'s did.
*
* `"etcd"` is the etcd provider's (#1089): its editor text is a subset of etcdctl's command line,
* read by the provider's own parser. It landed the way Kafka's did: an explicit arm in every reader
* of either field, or a test pinning that the branch it falls into is right for etcd.
*
* `"milvus"` is the Milvus provider's (vector-family spec 5.7): its editor text is one `POST /v2/vectordb/<route>`
* line and one JSON body, the closed console the provider lowers to typed gRPC calls. It landed through one record
* in each registry and no arm anywhere else.
*
* `"qdrant"` is the Qdrant provider's (vector-family spec 6.4): its editor text is one `METHOD /path` request
* line and one JSON body, the closed console the provider re-serialises from its own parse. It landed through
* one record in each registry and no arm anywhere else.
*
* `"weaviate"` is the Weaviate provider's (#1566 spec 5.1): its editor text is one `METHOD /v1/path` request line
* and one JSON body in Weaviate's REST shape, the closed console the provider runs over REST or GraphQL as the
* server's version decides. It landed through one record in each registry and no arm anywhere else.
*
* `"oxia"` is the Oxia provider's: one `oxia client` read command per run (O10).
*
* `"s3"` is the S3 provider's: one AWS CLI read command per run, `aws s3 ls`, the `aws s3api` reads its table names,
* or Studio's own `preview`.
*
* `"kubectl"` is the Kubernetes provider's: one statement of a read-only kubectl subset per run. It landed through one
* record in each registry and no arm anywhere else.
*
* `"firestore"` is the Firestore provider's: one statement of a read-only subset of the Admin SDK's query chain per
* run, `db.collection(...)` and the rest of its chain.
*
* `"mtconnect"` is the MTConnect provider's: one request line in the agent's own URL grammar per run, a route from
* a closed table of nine with GET implied, and no body on any of them.