@telorun/sql 0.21.3 → 0.22.1

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.
Files changed (45) hide show
  1. package/dist/index.d.ts +18 -2
  2. package/dist/index.js +9 -2
  3. package/dist/schema/declaration-snapshot.d.ts +21 -0
  4. package/dist/schema/declaration-snapshot.js +46 -0
  5. package/dist/schema/declared-schema.d.ts +64 -0
  6. package/dist/schema/declared-schema.js +16 -0
  7. package/dist/schema/migration-runner.d.ts +30 -0
  8. package/dist/schema/migration-runner.js +38 -0
  9. package/dist/schema/normalize-table.d.ts +57 -0
  10. package/dist/schema/normalize-table.js +125 -0
  11. package/dist/schema/reclaim-policy.d.ts +34 -0
  12. package/dist/schema/reclaim-policy.js +40 -0
  13. package/dist/schema/schema-driver.d.ts +184 -0
  14. package/dist/schema/schema-driver.js +1 -0
  15. package/dist/schema/schema-ledger.d.ts +119 -0
  16. package/dist/schema/schema-ledger.js +231 -0
  17. package/dist/schema/schema-reconciler.d.ts +45 -0
  18. package/dist/schema/schema-reconciler.js +276 -0
  19. package/dist/schema/schema-run.d.ts +50 -0
  20. package/dist/schema/schema-run.js +318 -0
  21. package/dist/schema/table-reference.d.ts +25 -0
  22. package/dist/schema/table-reference.js +61 -0
  23. package/dist/sql-connection-base.d.ts +21 -0
  24. package/dist/sql-connection-base.js +30 -4
  25. package/dist/sql-connection.d.ts +19 -0
  26. package/package.json +5 -3
  27. package/src/index.ts +43 -2
  28. package/src/schema/declaration-snapshot.ts +71 -0
  29. package/src/schema/declared-schema.ts +73 -0
  30. package/src/schema/migration-runner.ts +69 -0
  31. package/src/schema/normalize-table.ts +224 -0
  32. package/src/schema/reclaim-policy.ts +73 -0
  33. package/src/schema/schema-driver.ts +207 -0
  34. package/src/schema/schema-ledger.ts +309 -0
  35. package/src/schema/schema-reconciler.ts +372 -0
  36. package/src/schema/schema-run.ts +441 -0
  37. package/src/schema/table-reference.ts +78 -0
  38. package/src/sql-connection-base.ts +35 -4
  39. package/src/sql-connection.ts +21 -0
  40. package/dist/sql-migration-controller.d.ts +0 -16
  41. package/dist/sql-migration-controller.js +0 -13
  42. package/dist/sql-migrations-controller.d.ts +0 -23
  43. package/dist/sql-migrations-controller.js +0 -98
  44. package/src/sql-migration-controller.ts +0 -20
  45. package/src/sql-migrations-controller.ts +0 -143
@@ -0,0 +1,224 @@
1
+ import type {
2
+ DeclaredColumn,
3
+ DeclaredForeignKey,
4
+ DeclaredIndex,
5
+ DeclaredTable,
6
+ } from "./declared-schema.js";
7
+
8
+ /**
9
+ * The manifest shape a backend's `Table` kind declares, reduced to the
10
+ * normalized model.
11
+ *
12
+ * The STRUCTURE is shared — named columns, named indexes, named foreign keys —
13
+ * while the type vocabulary is not: `type` is carried through opaquely and
14
+ * every field the shared model does not name becomes a type parameter, so a
15
+ * backend adds `length`, `precision` or `collation` to its schema and nothing
16
+ * here changes. Types are structured rather than spelled into a scalar
17
+ * (`type: varchar` with `length: 64`, never `varchar(64)`), so nothing has to
18
+ * parse a type back apart.
19
+ */
20
+ export interface RawColumn {
21
+ readonly type: string;
22
+ readonly nullable?: boolean;
23
+ readonly array?: boolean;
24
+ readonly primaryKey?: boolean;
25
+ readonly unique?: boolean;
26
+ readonly default?: unknown;
27
+ readonly defaultExpression?: string;
28
+ readonly identity?: string;
29
+ readonly renamedFrom?: string;
30
+ readonly [param: string]: unknown;
31
+ }
32
+
33
+ export interface RawIndex {
34
+ readonly columns: readonly string[];
35
+ readonly unique?: boolean;
36
+ readonly [option: string]: unknown;
37
+ }
38
+
39
+ export interface RawForeignKey {
40
+ readonly columns: readonly string[];
41
+ readonly references: { readonly table: unknown; readonly columns: readonly string[] };
42
+ readonly onDelete?: string;
43
+ readonly onUpdate?: string;
44
+ }
45
+
46
+ export interface RawTable {
47
+ readonly table: string;
48
+ readonly columns?: Record<string, RawColumn>;
49
+ readonly indexes?: Record<string, RawIndex>;
50
+ readonly foreignKeys?: Record<string, RawForeignKey>;
51
+ }
52
+
53
+ const COLUMN_KEYS = new Set([
54
+ "type",
55
+ "nullable",
56
+ "array",
57
+ "primaryKey",
58
+ "unique",
59
+ "default",
60
+ "defaultExpression",
61
+ "identity",
62
+ "renamedFrom",
63
+ ]);
64
+
65
+ const INDEX_KEYS = new Set(["columns", "unique"]);
66
+
67
+ function params(raw: Record<string, unknown>, known: ReadonlySet<string>): Record<string, unknown> {
68
+ return Object.fromEntries(
69
+ Object.entries(raw).filter(([key, value]) => !known.has(key) && value !== undefined),
70
+ );
71
+ }
72
+
73
+ function normalizeColumn(name: string, raw: RawColumn): DeclaredColumn {
74
+ if (raw.default !== undefined && raw.defaultExpression !== undefined) {
75
+ throw new Error(
76
+ `column '${name}' declares both 'default' and 'defaultExpression' — a typed literal and ` +
77
+ `a backend SQL expression are separate fields and exactly one may be set`,
78
+ );
79
+ }
80
+
81
+ // A primary key and an identity column cannot hold NULL, and the engine
82
+ // enforces that whether or not the declaration says so. Left at the `nullable`
83
+ // default of true, the column would read back NOT NULL on the next boot, the
84
+ // pass would see a difference it could "fix", and every boot from then on
85
+ // would try to DROP NOT NULL on a primary key and fail. So the implication is
86
+ // applied here, once, where both the DDL and the comparison read it — and a
87
+ // declaration that states the opposite is refused rather than quietly
88
+ // overruled.
89
+ const impliesNotNull = raw.primaryKey === true || raw.identity !== undefined;
90
+ if (impliesNotNull && raw.nullable === true) {
91
+ throw new Error(
92
+ `column '${name}' is declared nullable and ${raw.primaryKey ? "a primary key" : "an identity column"}, ` +
93
+ `which cannot hold NULL. Remove 'nullable: true'.`,
94
+ );
95
+ }
96
+
97
+ return {
98
+ name,
99
+ type: raw.type,
100
+ params: params(raw as Record<string, unknown>, COLUMN_KEYS),
101
+ nullable: impliesNotNull ? false : (raw.nullable ?? true),
102
+ array: raw.array ?? false,
103
+ primaryKey: raw.primaryKey ?? false,
104
+ unique: raw.unique ?? false,
105
+ default: raw.default,
106
+ defaultExpression: raw.defaultExpression,
107
+ identity: raw.identity,
108
+ renamedFrom: raw.renamedFrom,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Turns whatever sits at a `references.table` slot into the referenced table's
114
+ * physical name.
115
+ *
116
+ * REQUIRED rather than optional: a `!ref` is not resolved when a controller is
117
+ * constructed — Phase-5 injection runs after `create()` returns — so a caller
118
+ * that omitted one would read the sentinel and reproduce, silently, the exact
119
+ * defect this parameter exists to fix. `tableReferenceResolver` is the one every
120
+ * backend uses.
121
+ */
122
+ export type TableReferenceResolver = (value: unknown, fk: string) => string;
123
+
124
+ /**
125
+ * Structural checks over one declaration, at resource creation — before any
126
+ * connection is opened, let alone any DDL planned.
127
+ *
128
+ * Each of these would otherwise reach the engine as raw SQL and come back as a
129
+ * driver error naming a statement the author never wrote. They are decidable
130
+ * from the declaration alone, so they are decided here and reported against the
131
+ * field that is wrong.
132
+ */
133
+ function validateTable(table: DeclaredTable): void {
134
+ const where = `${table.name}`;
135
+ if (table.columns.length === 0) {
136
+ throw new Error(`table '${where}' declares no columns — a table needs at least one.`);
137
+ }
138
+
139
+ const names = new Set(table.columns.map((c) => c.name));
140
+
141
+ const primaryKeys = table.columns.filter((c) => c.primaryKey).map((c) => c.name);
142
+ if (primaryKeys.length > 1) {
143
+ throw new Error(
144
+ `table '${where}' marks ${primaryKeys.map((n) => `'${n}'`).join(" and ")} as primaryKey. ` +
145
+ `A composite primary key is not expressible as a per-column flag — declare one column ` +
146
+ `as the key, or create the constraint in a 'migrations:' entry.`,
147
+ );
148
+ }
149
+
150
+ for (const column of table.columns) {
151
+ if (!column.renamedFrom) continue;
152
+ if (column.renamedFrom === column.name) {
153
+ throw new Error(
154
+ `column '${where}.${column.name}' declares renamedFrom itself, which describes no rename.`,
155
+ );
156
+ }
157
+ if (names.has(column.renamedFrom)) {
158
+ throw new Error(
159
+ `column '${where}.${column.name}' renames from '${column.renamedFrom}', which this table ` +
160
+ `also declares. A rename's source is the column being retired, so declaring both would ` +
161
+ `copy one live column into another and retire neither.`,
162
+ );
163
+ }
164
+ }
165
+
166
+ // An index or foreign key over a column the table does not declare cannot be
167
+ // created, and the engine's complaint would name a generated statement.
168
+ for (const index of table.indexes) {
169
+ for (const column of index.columns) {
170
+ if (!names.has(column)) {
171
+ throw new Error(
172
+ `index '${where}.${index.name}' names column '${column}', which this table does not ` +
173
+ `declare.`,
174
+ );
175
+ }
176
+ }
177
+ }
178
+ for (const fk of table.foreignKeys) {
179
+ for (const column of fk.columns) {
180
+ if (!names.has(column)) {
181
+ throw new Error(
182
+ `foreign key '${where}.${fk.name}' names column '${column}', which this table does not ` +
183
+ `declare.`,
184
+ );
185
+ }
186
+ }
187
+ if (fk.references.columns.length !== fk.columns.length) {
188
+ throw new Error(
189
+ `foreign key '${where}.${fk.name}' has ${fk.columns.length} column(s) but references ` +
190
+ `${fk.references.columns.length} — a foreign key maps its columns one for one.`,
191
+ );
192
+ }
193
+ }
194
+ }
195
+
196
+ export function normalizeTable(
197
+ raw: RawTable,
198
+ resolveReference: TableReferenceResolver,
199
+ ): DeclaredTable {
200
+ const columns = Object.entries(raw.columns ?? {}).map(([name, column]) =>
201
+ normalizeColumn(name, column),
202
+ );
203
+ const indexes: DeclaredIndex[] = Object.entries(raw.indexes ?? {}).map(([name, index]) => ({
204
+ name,
205
+ columns: [...index.columns],
206
+ unique: index.unique ?? false,
207
+ options: params(index as Record<string, unknown>, INDEX_KEYS),
208
+ }));
209
+ const foreignKeys: DeclaredForeignKey[] = Object.entries(raw.foreignKeys ?? {}).map(
210
+ ([name, fk]) => ({
211
+ name,
212
+ columns: [...fk.columns],
213
+ references: {
214
+ table: resolveReference(fk.references.table, name),
215
+ columns: [...fk.references.columns],
216
+ },
217
+ onDelete: fk.onDelete,
218
+ onUpdate: fk.onUpdate,
219
+ }),
220
+ );
221
+ const table: DeclaredTable = { name: raw.table, columns, indexes, foreignKeys };
222
+ validateTable(table);
223
+ return table;
224
+ }
@@ -0,0 +1,73 @@
1
+ import { parseDurationMs } from "@telorun/sdk";
2
+ import type { TombstoneRecord, VersionRecord } from "./schema-ledger.js";
3
+
4
+ /**
5
+ * When a tombstone may be reclaimed. Eligibility is a CONJUNCTION: N released
6
+ * versions must have been observed since the object went missing, AND T must
7
+ * have elapsed. Version is the primary signal — it is what proves older code is
8
+ * no longer live — and time is the backstop, because N versions can land in an
9
+ * afternoon.
10
+ *
11
+ * Declaring no policy means nothing is ever dropped, so reclamation is opt-in
12
+ * by declaration rather than by invocation.
13
+ */
14
+ export interface ReclaimPolicy {
15
+ readonly afterVersions: number;
16
+ readonly afterDuration: string;
17
+ }
18
+
19
+ export interface Eligibility {
20
+ readonly eligible: boolean;
21
+ /** Versions observed since the tombstone, and how many are still needed. */
22
+ readonly versionsObserved: number;
23
+ readonly versionsRemaining: number;
24
+ readonly msElapsed: number;
25
+ readonly msRemaining: number;
26
+ }
27
+
28
+ /**
29
+ * A rollback resets progress rather than merely pausing it.
30
+ *
31
+ * Going backwards proves older code is live, so a boot at a version that was
32
+ * already observed at or before the tombstone is not one more release past the
33
+ * removal — it is evidence the removal is not yet safe. Both counters restart
34
+ * from that observation: the version count, and the elapsed-time baseline. This
35
+ * is answerable only because the ledger records the observed *sequence* rather
36
+ * than a counter.
37
+ */
38
+ export function assessTombstone(
39
+ tombstone: TombstoneRecord,
40
+ history: readonly VersionRecord[],
41
+ policy: ReclaimPolicy,
42
+ nowMs: number,
43
+ ): Eligibility {
44
+ const priorVersions = new Set(
45
+ history
46
+ .filter((entry) => entry.sequence <= tombstone.missingSinceSequence)
47
+ .map((entry) => entry.version),
48
+ );
49
+ let counted = new Set<string>();
50
+ let baselineAt = tombstone.missingSinceAt;
51
+ for (const entry of history) {
52
+ if (entry.sequence <= tombstone.missingSinceSequence) continue;
53
+ if (priorVersions.has(entry.version)) {
54
+ counted = new Set();
55
+ baselineAt = entry.firstSeenAt;
56
+ continue;
57
+ }
58
+ counted.add(entry.version);
59
+ }
60
+
61
+ const versionsObserved = counted.size;
62
+ const msElapsed = Math.max(0, nowMs - Date.parse(baselineAt));
63
+ const requiredMs = parseDurationMs(policy.afterDuration);
64
+ const versionsRemaining = Math.max(0, policy.afterVersions - versionsObserved);
65
+ const msRemaining = Math.max(0, requiredMs - msElapsed);
66
+ return {
67
+ eligible: versionsRemaining === 0 && msRemaining === 0,
68
+ versionsObserved,
69
+ versionsRemaining,
70
+ msElapsed,
71
+ msRemaining,
72
+ };
73
+ }
@@ -0,0 +1,207 @@
1
+ import type { SqlConnection } from "../sql-connection.js";
2
+ import type { LedgerTables } from "./schema-ledger.js";
3
+ import type {
4
+ SchemaObjectId,
5
+ DeclaredColumn,
6
+ DeclaredForeignKey,
7
+ DeclaredIndex,
8
+ DeclaredTable,
9
+ } from "./declared-schema.js";
10
+
11
+ /** The live shape of one table, as the driver reads it back from the database. */
12
+ export interface LiveColumn {
13
+ readonly name: string;
14
+ /**
15
+ * The driver's canonical rendering of the column's type, compared verbatim
16
+ * against what {@link SchemaDriver.typeSignature} produces for a declaration.
17
+ * Comparing signatures rather than parsed parts is what keeps every type rule
18
+ * inside the driver.
19
+ */
20
+ readonly typeSignature: string;
21
+ readonly nullable: boolean;
22
+ readonly hasDefault: boolean;
23
+ /** Whether the column is part of the table's primary key. */
24
+ readonly primaryKey: boolean;
25
+ /** Whether a single-column uniqueness constraint covers it. */
26
+ readonly unique: boolean;
27
+ }
28
+
29
+ /**
30
+ * An index as the database has it, not merely its name.
31
+ *
32
+ * Compared by name alone, changing which columns an index covers — or making it
33
+ * unique — emitted nothing and reported nothing, while the ledger recorded the
34
+ * new definition as owned. The declaration then asserted an index the database
35
+ * was not providing.
36
+ */
37
+ export interface LiveIndex {
38
+ readonly name: string;
39
+ readonly columns: readonly string[];
40
+ readonly unique: boolean;
41
+ }
42
+
43
+ /** A foreign key as the database has it. Same reasoning as {@link LiveIndex}:
44
+ * changing `onDelete` is a change to what the constraint DOES. */
45
+ export interface LiveForeignKey {
46
+ readonly name: string;
47
+ readonly columns: readonly string[];
48
+ readonly references: { readonly table: string; readonly columns: readonly string[] };
49
+ readonly onDelete?: string;
50
+ readonly onUpdate?: string;
51
+ }
52
+
53
+ export interface LiveTable {
54
+ readonly name: string;
55
+ readonly columns: readonly LiveColumn[];
56
+ readonly indexes: readonly LiveIndex[];
57
+ readonly foreignKeys: readonly LiveForeignKey[];
58
+ }
59
+
60
+ /** How a declared change relates to the data already in the column. */
61
+ export type ChangeSafety =
62
+ | { readonly safe: true }
63
+ | { readonly safe: false; readonly reason: string };
64
+
65
+ /**
66
+ * Everything reconciliation needs that only the engine knows: how a type is
67
+ * spelled, how it is read back, what a lock is, and which alterations the
68
+ * engine can perform at all. The diff, the ledger, the tombstones and the
69
+ * ordering live in the shared library and are identical for every backend.
70
+ *
71
+ * Statement renderers return SQL text; the runner executes and logs it. They
72
+ * never touch the connection themselves, so a plan can be built, reported and
73
+ * refused without anything having run.
74
+ */
75
+ export interface SchemaDriver {
76
+ readonly connection: SqlConnection;
77
+
78
+ /** Quote an identifier. */
79
+ quote(name: string): string;
80
+ /** Qualify a table with the namespace this schema resource owns. */
81
+ qualify(schema: string, table: string): string;
82
+
83
+ /**
84
+ * Hold the engine's cross-process schema lock for the duration of `body`.
85
+ * Every replica of an app boots the same pass, so this is what makes
86
+ * "reconcile once" true rather than hoped for.
87
+ */
88
+ withLock<T>(schema: string, body: () => Promise<T>): Promise<T>;
89
+
90
+ /** Create the namespace when absent. Purely additive. */
91
+ ensureNamespaceStatements(schema: string): string[];
92
+
93
+ /** The three ledger tables, `CREATE TABLE IF NOT EXISTS`. Named by the caller:
94
+ * which ledger a schema resource writes to is its own declaration. */
95
+ ledgerStatements(schema: string, tables: LedgerTables): string[];
96
+
97
+ /**
98
+ * Run `statements` atomically where the engine allows DDL inside a
99
+ * transaction, and sequentially where it does not. Which of the two an engine
100
+ * offers is the driver's to know; the runner only needs the strongest
101
+ * grouping available, and relies on the pass being idempotent for the rest.
102
+ *
103
+ * A migration's ledger row is the LAST statement of its group, so on an engine
104
+ * with transactional DDL "applied" and "recorded as applied" are one commit.
105
+ * An implementation whose engine cannot do that MUST say so here, because the
106
+ * consequence is specific: a crash mid-group can re-run a migration that is
107
+ * not idempotent.
108
+ */
109
+ runAtomically(statements: readonly string[]): Promise<void>;
110
+
111
+ /**
112
+ * The current time, as ISO-8601 UTC text, **from the database**.
113
+ *
114
+ * Not the application's clock: the grace window gates an irreversible drop,
115
+ * and one replica with a skewed clock would satisfy `afterDuration`
116
+ * instantly. The ledger's history already treats the database as the
117
+ * authority; the clock measured against it has to come from there too, so
118
+ * every replica measures the same elapsed time.
119
+ */
120
+ now(): Promise<string>;
121
+
122
+ /** Read back the live shape of the named tables. Absent tables are omitted. */
123
+ introspect(schema: string, tables: readonly string[]): Promise<LiveTable[]>;
124
+
125
+ /** The canonical signature of a declared column's type, for comparison with {@link LiveColumn.typeSignature}. */
126
+ typeSignature(column: DeclaredColumn): string;
127
+
128
+ /**
129
+ * Whether altering a live column to the declared one can be applied without
130
+ * risking the data already in it. Type widening is safe, narrowing is not,
131
+ * and only the engine knows which is which.
132
+ */
133
+ classifyAlter(live: LiveColumn, declared: DeclaredColumn): ChangeSafety;
134
+
135
+ /**
136
+ * Whether the values in a live column can be COPIED into a newly added one —
137
+ * the expand-contract half of a rename.
138
+ *
139
+ * A different question from {@link classifyAlter}, which asks whether a column
140
+ * can be changed in place. Here the target is brand new, so its nullability
141
+ * and default are already what the declaration says and only the VALUES have
142
+ * to survive the move. An engine that can alter nothing in place can still
143
+ * copy freely between compatible types, and one that copies between anything
144
+ * may still lose data doing it.
145
+ */
146
+ classifyCopy(live: LiveColumn, target: DeclaredColumn): ChangeSafety;
147
+
148
+ /**
149
+ * Whether an index can be brought to its declaration in place. Most engines
150
+ * cannot alter one, so the honest answer is usually to drop and recreate —
151
+ * which this returns as statements, or refuses when the index is not this
152
+ * schema's to rebuild.
153
+ */
154
+ classifyIndexChange(live: LiveIndex, declared: DeclaredIndex): ChangeSafety;
155
+
156
+ /** Whether a foreign key can be brought to its declaration in place. */
157
+ classifyForeignKeyChange(live: LiveForeignKey, declared: DeclaredForeignKey): ChangeSafety;
158
+
159
+ /**
160
+ * Whether `createTable` already carries the table's foreign keys, so the
161
+ * reconciler must not also plan an `addForeignKey` for a table it just made.
162
+ *
163
+ * REQUIRED, like every other member here, because the wrong answer is silent:
164
+ * a driver that omitted it would get name matching by default, and if its
165
+ * engine also emits keys inside `CREATE TABLE` it would get exactly the
166
+ * unrestartable application this member exists to prevent — with no compile
167
+ * error and no failing test. Stating an answer is the point.
168
+ */
169
+ readonly foreignKeysInCreateTable: boolean;
170
+
171
+ /**
172
+ * Whether the engine reports a foreign key back under the name the
173
+ * declaration gave it.
174
+ *
175
+ * Separate from `foreignKeysInCreateTable` because they are separate facts and
176
+ * an engine can hold one without the other: MySQL emits keys inside `CREATE
177
+ * TABLE` and names them. Where this is false a declaration is matched to a
178
+ * live key by its columns, target and referential actions, since there is no
179
+ * name to match on and matching by one reads a table's own key as missing on
180
+ * every boot after the one that created it.
181
+ */
182
+ readonly namesForeignKeys: boolean;
183
+
184
+ createTable(schema: string, table: DeclaredTable): string[];
185
+ addColumn(schema: string, table: string, column: DeclaredColumn): string[];
186
+ alterColumn(schema: string, table: string, live: LiveColumn, column: DeclaredColumn): string[];
187
+ copyColumn(schema: string, table: string, from: string, to: string): string[];
188
+ createIndex(schema: string, table: string, index: DeclaredIndex): string[];
189
+ dropIndex(schema: string, table: string, index: string): string[];
190
+ addForeignKey(schema: string, table: string, fk: DeclaredForeignKey): string[];
191
+ dropForeignKey(schema: string, table: string, name: string): string[];
192
+
193
+ /**
194
+ * Whether this engine can reclaim an object of this kind at all.
195
+ *
196
+ * Asked BEFORE the drop is attempted, because a tombstone the engine cannot
197
+ * act on is otherwise permanent breakage rather than a one-off failure: it
198
+ * stays eligible, so every boot from then on fails in the same place, and the
199
+ * application never starts again. Reported instead, with the reason, and the
200
+ * tombstone is left standing.
201
+ */
202
+ canReclaim(id: SchemaObjectId): ChangeSafety;
203
+
204
+ /** Reclamation. Separated from the rest because these are the only destructive statements. */
205
+ dropColumn(schema: string, table: string, column: string): string[];
206
+ dropTable(schema: string, table: string): string[];
207
+ }