taurusdb-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/auth/secret-resolver.d.ts +16 -0
- package/dist/auth/secret-resolver.js +64 -0
- package/dist/auth/sql-profile-loader/env-source.d.ts +3 -0
- package/dist/auth/sql-profile-loader/env-source.js +94 -0
- package/dist/auth/sql-profile-loader/file-source.d.ts +6 -0
- package/dist/auth/sql-profile-loader/file-source.js +40 -0
- package/dist/auth/sql-profile-loader/loader.d.ts +16 -0
- package/dist/auth/sql-profile-loader/loader.js +81 -0
- package/dist/auth/sql-profile-loader/parsing.d.ts +14 -0
- package/dist/auth/sql-profile-loader/parsing.js +216 -0
- package/dist/auth/sql-profile-loader/runtime-override.d.ts +14 -0
- package/dist/auth/sql-profile-loader/runtime-override.js +52 -0
- package/dist/auth/sql-profile-loader/types.d.ts +64 -0
- package/dist/auth/sql-profile-loader/types.js +1 -0
- package/dist/auth/sql-profile-loader.d.ts +4 -0
- package/dist/auth/sql-profile-loader.js +3 -0
- package/dist/capability/feature-matrix.d.ts +5 -0
- package/dist/capability/feature-matrix.js +237 -0
- package/dist/capability/probe.d.ts +19 -0
- package/dist/capability/probe.js +139 -0
- package/dist/capability/types.d.ts +49 -0
- package/dist/capability/types.js +16 -0
- package/dist/capability/version.d.ts +3 -0
- package/dist/capability/version.js +47 -0
- package/dist/cloud/auth.d.ts +26 -0
- package/dist/cloud/auth.js +198 -0
- package/dist/cloud/instances.d.ts +46 -0
- package/dist/cloud/instances.js +224 -0
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.js +194 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +21 -0
- package/dist/config/redaction.d.ts +2 -0
- package/dist/config/redaction.js +19 -0
- package/dist/config/schema.d.ts +417 -0
- package/dist/config/schema.js +100 -0
- package/dist/context/datasource-resolver.d.ts +19 -0
- package/dist/context/datasource-resolver.js +71 -0
- package/dist/context/session-context.d.ts +26 -0
- package/dist/context/session-context.js +1 -0
- package/dist/diagnostics/metrics-source.d.ts +65 -0
- package/dist/diagnostics/metrics-source.js +280 -0
- package/dist/diagnostics/slow-sql-source/das-source.d.ts +43 -0
- package/dist/diagnostics/slow-sql-source/das-source.js +170 -0
- package/dist/diagnostics/slow-sql-source/factory.d.ts +5 -0
- package/dist/diagnostics/slow-sql-source/factory.js +87 -0
- package/dist/diagnostics/slow-sql-source/parsers.d.ts +7 -0
- package/dist/diagnostics/slow-sql-source/parsers.js +125 -0
- package/dist/diagnostics/slow-sql-source/taurus-api-source.d.ts +42 -0
- package/dist/diagnostics/slow-sql-source/taurus-api-source.js +149 -0
- package/dist/diagnostics/slow-sql-source/types.d.ts +40 -0
- package/dist/diagnostics/slow-sql-source/types.js +1 -0
- package/dist/diagnostics/slow-sql-source/utils.d.ts +20 -0
- package/dist/diagnostics/slow-sql-source/utils.js +170 -0
- package/dist/diagnostics/slow-sql-source.d.ts +4 -0
- package/dist/diagnostics/slow-sql-source.js +3 -0
- package/dist/diagnostics/types.d.ts +189 -0
- package/dist/diagnostics/types.js +39 -0
- package/dist/engine/data-access/locks.d.ts +8 -0
- package/dist/engine/data-access/locks.js +146 -0
- package/dist/engine/data-access/processlist.d.ts +4 -0
- package/dist/engine/data-access/processlist.js +56 -0
- package/dist/engine/data-access/statements.d.ts +10 -0
- package/dist/engine/data-access/statements.js +203 -0
- package/dist/engine/data-access/storage.d.ts +6 -0
- package/dist/engine/data-access/storage.js +96 -0
- package/dist/engine/data-access.d.ts +4 -0
- package/dist/engine/data-access.js +4 -0
- package/dist/engine/diagnostics.d.ts +7 -0
- package/dist/engine/diagnostics.js +7 -0
- package/dist/engine/helper-modules/diagnostics.d.ts +57 -0
- package/dist/engine/helper-modules/diagnostics.js +322 -0
- package/dist/engine/helper-modules/parsers.d.ts +13 -0
- package/dist/engine/helper-modules/parsers.js +283 -0
- package/dist/engine/helper-modules/sql.d.ts +12 -0
- package/dist/engine/helper-modules/sql.js +119 -0
- package/dist/engine/helper-modules/types.d.ts +103 -0
- package/dist/engine/helper-modules/types.js +1 -0
- package/dist/engine/helpers.d.ts +4 -0
- package/dist/engine/helpers.js +4 -0
- package/dist/engine/runtime.d.ts +20 -0
- package/dist/engine/runtime.js +385 -0
- package/dist/engine/types.d.ts +125 -0
- package/dist/engine/types.js +1 -0
- package/dist/engine/workflows/connection-spike.d.ts +4 -0
- package/dist/engine/workflows/connection-spike.js +316 -0
- package/dist/engine/workflows/db-hotspot.d.ts +4 -0
- package/dist/engine/workflows/db-hotspot.js +182 -0
- package/dist/engine/workflows/lock-contention-helpers/entities.d.ts +9 -0
- package/dist/engine/workflows/lock-contention-helpers/entities.js +58 -0
- package/dist/engine/workflows/lock-contention-helpers/no-match.d.ts +3 -0
- package/dist/engine/workflows/lock-contention-helpers/no-match.js +65 -0
- package/dist/engine/workflows/lock-contention-helpers/report.d.ts +21 -0
- package/dist/engine/workflows/lock-contention-helpers/report.js +104 -0
- package/dist/engine/workflows/lock-contention-helpers/root-cause.d.ts +4 -0
- package/dist/engine/workflows/lock-contention-helpers/root-cause.js +79 -0
- package/dist/engine/workflows/lock-contention-helpers/signals.d.ts +22 -0
- package/dist/engine/workflows/lock-contention-helpers/signals.js +34 -0
- package/dist/engine/workflows/lock-contention-helpers.d.ts +5 -0
- package/dist/engine/workflows/lock-contention-helpers.js +5 -0
- package/dist/engine/workflows/lock-contention.d.ts +4 -0
- package/dist/engine/workflows/lock-contention.js +67 -0
- package/dist/engine/workflows/service-latency.d.ts +4 -0
- package/dist/engine/workflows/service-latency.js +262 -0
- package/dist/engine/workflows/slow-query-helpers.d.ts +41 -0
- package/dist/engine/workflows/slow-query-helpers.js +253 -0
- package/dist/engine/workflows/slow-query.d.ts +4 -0
- package/dist/engine/workflows/slow-query.js +156 -0
- package/dist/engine/workflows/storage-pressure-helpers.d.ts +12 -0
- package/dist/engine/workflows/storage-pressure-helpers.js +281 -0
- package/dist/engine/workflows/storage-pressure.d.ts +4 -0
- package/dist/engine/workflows/storage-pressure.js +27 -0
- package/dist/engine/workflows/top-slow-sql.d.ts +4 -0
- package/dist/engine/workflows/top-slow-sql.js +222 -0
- package/dist/engine.d.ts +77 -0
- package/dist/engine.js +240 -0
- package/dist/executor/adapters/mysql.d.ts +2 -0
- package/dist/executor/adapters/mysql.js +114 -0
- package/dist/executor/connection-pool.d.ts +105 -0
- package/dist/executor/connection-pool.js +236 -0
- package/dist/executor/explain.d.ts +5 -0
- package/dist/executor/explain.js +119 -0
- package/dist/executor/query-tracker.d.ts +45 -0
- package/dist/executor/query-tracker.js +83 -0
- package/dist/executor/result-normalizer.d.ts +6 -0
- package/dist/executor/result-normalizer.js +47 -0
- package/dist/executor/sql-executor.d.ts +32 -0
- package/dist/executor/sql-executor.js +250 -0
- package/dist/executor/types.d.ts +70 -0
- package/dist/executor/types.js +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +21 -0
- package/dist/safety/confirmation-store.d.ts +44 -0
- package/dist/safety/confirmation-store.js +130 -0
- package/dist/safety/guardrail.d.ts +39 -0
- package/dist/safety/guardrail.js +99 -0
- package/dist/safety/parser/adapter.d.ts +10 -0
- package/dist/safety/parser/adapter.js +72 -0
- package/dist/safety/parser/ast-utils.d.ts +10 -0
- package/dist/safety/parser/ast-utils.js +167 -0
- package/dist/safety/parser/features.d.ts +12 -0
- package/dist/safety/parser/features.js +113 -0
- package/dist/safety/parser/index.d.ts +2 -0
- package/dist/safety/parser/index.js +1 -0
- package/dist/safety/parser/types.d.ts +76 -0
- package/dist/safety/parser/types.js +1 -0
- package/dist/safety/redaction.d.ts +34 -0
- package/dist/safety/redaction.js +186 -0
- package/dist/safety/sql-classifier.d.ts +19 -0
- package/dist/safety/sql-classifier.js +43 -0
- package/dist/safety/sql-validator.d.ts +19 -0
- package/dist/safety/sql-validator.js +143 -0
- package/dist/schema/adapters/mysql.d.ts +16 -0
- package/dist/schema/adapters/mysql.js +287 -0
- package/dist/schema/introspector.d.ts +70 -0
- package/dist/schema/introspector.js +40 -0
- package/dist/taurus/flashback.d.ts +36 -0
- package/dist/taurus/flashback.js +149 -0
- package/dist/taurus/recycle-bin.d.ts +14 -0
- package/dist/taurus/recycle-bin.js +61 -0
- package/dist/utils/formatter.d.ts +70 -0
- package/dist/utils/formatter.js +60 -0
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +247 -0
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.js +11 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +39 -0
- package/package.json +46 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
const SENSITIVE_PATTERNS = [
|
|
2
|
+
/phone|mobile|tel/i,
|
|
3
|
+
/id_?card|passport|ssn/i,
|
|
4
|
+
/email/i,
|
|
5
|
+
/password|passwd|secret/i,
|
|
6
|
+
/token|api_?key/i,
|
|
7
|
+
/bank|card_?no|account/i,
|
|
8
|
+
];
|
|
9
|
+
const TIME_COLUMN_PATTERNS = [
|
|
10
|
+
/created(_at|_time)?$/i,
|
|
11
|
+
/updated(_at|_time)?$/i,
|
|
12
|
+
/modified(_at|_time)?$/i,
|
|
13
|
+
/event(_at|_time)?$/i,
|
|
14
|
+
/timestamp/i,
|
|
15
|
+
/date$/i,
|
|
16
|
+
/time$/i,
|
|
17
|
+
];
|
|
18
|
+
const TIME_DATA_TYPES = new Set(["date", "datetime", "timestamp", "time", "year"]);
|
|
19
|
+
function quoteLiteral(value) {
|
|
20
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
|
|
21
|
+
}
|
|
22
|
+
function normalizeType(value) {
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
return value.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
if (value === null || value === undefined) {
|
|
27
|
+
return "unknown";
|
|
28
|
+
}
|
|
29
|
+
return String(value).toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
function normalizeNumber(value) {
|
|
32
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
36
|
+
const parsed = Number.parseFloat(value);
|
|
37
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
function normalizeBoolean(value) {
|
|
42
|
+
if (typeof value === "boolean") {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "number") {
|
|
46
|
+
return value !== 0;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
const normalized = value.trim().toLowerCase();
|
|
50
|
+
if (["1", "true", "yes", "y"].includes(normalized)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (["0", "false", "no", "n"].includes(normalized)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function readField(row, candidates) {
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
if (Object.hasOwn(row, candidate)) {
|
|
62
|
+
return row[candidate];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const lowerMap = new Map();
|
|
66
|
+
for (const [key, value] of Object.entries(row)) {
|
|
67
|
+
lowerMap.set(key.toLowerCase(), value);
|
|
68
|
+
}
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
const match = lowerMap.get(candidate.toLowerCase());
|
|
71
|
+
if (match !== undefined) {
|
|
72
|
+
return match;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function rowsToObjects(result) {
|
|
78
|
+
const rawRows = result.rows;
|
|
79
|
+
if (!Array.isArray(rawRows) || rawRows.length === 0) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const firstRow = rawRows[0];
|
|
83
|
+
if (!Array.isArray(firstRow) && firstRow !== null && typeof firstRow === "object") {
|
|
84
|
+
return rawRows;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(firstRow) && Array.isArray(result.fields)) {
|
|
87
|
+
return rawRows.map((row) => {
|
|
88
|
+
const mapped = {};
|
|
89
|
+
result.fields?.forEach((field, index) => {
|
|
90
|
+
mapped[field.name] = row[index];
|
|
91
|
+
});
|
|
92
|
+
return mapped;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
function mapTableType(value) {
|
|
98
|
+
const normalized = typeof value === "string" ? value.toLowerCase() : "";
|
|
99
|
+
if (normalized === "base table" || normalized === "table") {
|
|
100
|
+
return "table";
|
|
101
|
+
}
|
|
102
|
+
if (normalized === "view") {
|
|
103
|
+
return "view";
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
function buildEngineHints(columns, indexes, primaryKey) {
|
|
108
|
+
const indexedColumns = new Set();
|
|
109
|
+
for (const index of indexes) {
|
|
110
|
+
for (const column of index.columns) {
|
|
111
|
+
indexedColumns.add(column);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const column of primaryKey ?? []) {
|
|
115
|
+
indexedColumns.add(column);
|
|
116
|
+
}
|
|
117
|
+
const likelyTimeColumns = columns
|
|
118
|
+
.filter((column) => {
|
|
119
|
+
const byType = TIME_DATA_TYPES.has(column.dataType.toLowerCase());
|
|
120
|
+
const byName = TIME_COLUMN_PATTERNS.some((pattern) => pattern.test(column.name));
|
|
121
|
+
return byType || byName;
|
|
122
|
+
})
|
|
123
|
+
.map((column) => column.name);
|
|
124
|
+
const likelyFilterColumns = columns
|
|
125
|
+
.filter((column) => indexedColumns.has(column.name))
|
|
126
|
+
.map((column) => column.name);
|
|
127
|
+
const sensitiveColumns = columns
|
|
128
|
+
.filter((column) => SENSITIVE_PATTERNS.some((pattern) => pattern.test(column.name)))
|
|
129
|
+
.map((column) => column.name);
|
|
130
|
+
return {
|
|
131
|
+
likelyTimeColumns,
|
|
132
|
+
likelyFilterColumns,
|
|
133
|
+
sensitiveColumns,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export class MySqlSchemaAdapter {
|
|
137
|
+
connectionPool;
|
|
138
|
+
constructor(options) {
|
|
139
|
+
this.connectionPool = options.connectionPool;
|
|
140
|
+
}
|
|
141
|
+
async listDatabases(ctx) {
|
|
142
|
+
const sql = `
|
|
143
|
+
SELECT SCHEMA_NAME AS schema_name
|
|
144
|
+
FROM information_schema.SCHEMATA
|
|
145
|
+
ORDER BY SCHEMA_NAME
|
|
146
|
+
`;
|
|
147
|
+
const rows = await this.queryObjects(ctx, sql);
|
|
148
|
+
return rows.map((row) => ({
|
|
149
|
+
name: String(readField(row, ["schema_name", "SCHEMA_NAME"])),
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
async listTables(ctx, database) {
|
|
153
|
+
const sql = `
|
|
154
|
+
SELECT TABLE_NAME AS table_name,
|
|
155
|
+
TABLE_TYPE AS table_type,
|
|
156
|
+
TABLE_COMMENT AS table_comment,
|
|
157
|
+
TABLE_ROWS AS table_rows
|
|
158
|
+
FROM information_schema.TABLES
|
|
159
|
+
WHERE TABLE_SCHEMA = ${quoteLiteral(database)}
|
|
160
|
+
ORDER BY TABLE_NAME
|
|
161
|
+
`;
|
|
162
|
+
const rows = await this.queryObjects(ctx, sql);
|
|
163
|
+
return rows.map((row) => ({
|
|
164
|
+
database,
|
|
165
|
+
name: String(readField(row, ["table_name", "TABLE_NAME"])),
|
|
166
|
+
type: mapTableType(readField(row, ["table_type", "TABLE_TYPE"])),
|
|
167
|
+
comment: readField(row, ["table_comment", "TABLE_COMMENT"]) || undefined,
|
|
168
|
+
rowCountEstimate: normalizeNumber(readField(row, ["table_rows", "TABLE_ROWS"])),
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
async describeTable(ctx, database, table) {
|
|
172
|
+
const columnsSql = `
|
|
173
|
+
SELECT COLUMN_NAME AS column_name,
|
|
174
|
+
DATA_TYPE AS data_type,
|
|
175
|
+
IS_NULLABLE AS is_nullable,
|
|
176
|
+
COLUMN_DEFAULT AS column_default,
|
|
177
|
+
COLUMN_KEY AS column_key,
|
|
178
|
+
EXTRA AS extra,
|
|
179
|
+
COLUMN_COMMENT AS column_comment,
|
|
180
|
+
CHARACTER_MAXIMUM_LENGTH AS character_maximum_length
|
|
181
|
+
FROM information_schema.COLUMNS
|
|
182
|
+
WHERE TABLE_SCHEMA = ${quoteLiteral(database)}
|
|
183
|
+
AND TABLE_NAME = ${quoteLiteral(table)}
|
|
184
|
+
ORDER BY ORDINAL_POSITION
|
|
185
|
+
`;
|
|
186
|
+
const indexesSql = `
|
|
187
|
+
SELECT INDEX_NAME AS index_name,
|
|
188
|
+
COLUMN_NAME AS column_name,
|
|
189
|
+
NON_UNIQUE AS non_unique,
|
|
190
|
+
SEQ_IN_INDEX AS seq_in_index,
|
|
191
|
+
INDEX_TYPE AS index_type
|
|
192
|
+
FROM information_schema.STATISTICS
|
|
193
|
+
WHERE TABLE_SCHEMA = ${quoteLiteral(database)}
|
|
194
|
+
AND TABLE_NAME = ${quoteLiteral(table)}
|
|
195
|
+
ORDER BY INDEX_NAME, SEQ_IN_INDEX
|
|
196
|
+
`;
|
|
197
|
+
const tableMetaSql = `
|
|
198
|
+
SELECT TABLE_ROWS AS table_rows,
|
|
199
|
+
TABLE_COMMENT AS table_comment
|
|
200
|
+
FROM information_schema.TABLES
|
|
201
|
+
WHERE TABLE_SCHEMA = ${quoteLiteral(database)}
|
|
202
|
+
AND TABLE_NAME = ${quoteLiteral(table)}
|
|
203
|
+
LIMIT 1
|
|
204
|
+
`;
|
|
205
|
+
const [columnRows, indexRows, tableMetaRows] = await Promise.all([
|
|
206
|
+
this.queryObjects(ctx, columnsSql),
|
|
207
|
+
this.queryObjects(ctx, indexesSql),
|
|
208
|
+
this.queryObjects(ctx, tableMetaSql),
|
|
209
|
+
]);
|
|
210
|
+
const indexes = this.mapIndexes(indexRows);
|
|
211
|
+
const primaryKey = indexes.find((index) => index.name === "PRIMARY")?.columns;
|
|
212
|
+
const indexedColumnSet = new Set(indexes.flatMap((index) => index.columns));
|
|
213
|
+
const columns = columnRows.map((row) => {
|
|
214
|
+
const name = String(readField(row, ["column_name", "COLUMN_NAME"]));
|
|
215
|
+
const dataType = normalizeType(readField(row, ["data_type", "DATA_TYPE"]));
|
|
216
|
+
const nullable = normalizeBoolean(readField(row, ["is_nullable", "IS_NULLABLE"])) ?? true;
|
|
217
|
+
return {
|
|
218
|
+
name,
|
|
219
|
+
dataType,
|
|
220
|
+
nullable,
|
|
221
|
+
defaultValue: readField(row, ["column_default", "COLUMN_DEFAULT"]),
|
|
222
|
+
maxLength: normalizeNumber(readField(row, ["character_maximum_length", "CHARACTER_MAXIMUM_LENGTH"])),
|
|
223
|
+
isPrimaryKey: readField(row, ["column_key", "COLUMN_KEY"])?.toUpperCase() === "PRI",
|
|
224
|
+
isIndexed: indexedColumnSet.has(name),
|
|
225
|
+
comment: readField(row, ["column_comment", "COLUMN_COMMENT"]) || undefined,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
const meta = tableMetaRows[0];
|
|
229
|
+
const rowCountEstimate = meta
|
|
230
|
+
? normalizeNumber(readField(meta, ["table_rows", "TABLE_ROWS"]))
|
|
231
|
+
: undefined;
|
|
232
|
+
const comment = meta
|
|
233
|
+
? (readField(meta, ["table_comment", "TABLE_COMMENT"]) || undefined)
|
|
234
|
+
: undefined;
|
|
235
|
+
const schema = {
|
|
236
|
+
database,
|
|
237
|
+
table,
|
|
238
|
+
columns,
|
|
239
|
+
indexes,
|
|
240
|
+
primaryKey,
|
|
241
|
+
engineHints: buildEngineHints(columns, indexes, primaryKey),
|
|
242
|
+
comment,
|
|
243
|
+
rowCountEstimate,
|
|
244
|
+
};
|
|
245
|
+
return schema;
|
|
246
|
+
}
|
|
247
|
+
mapIndexes(rows) {
|
|
248
|
+
const byName = new Map();
|
|
249
|
+
for (const row of rows) {
|
|
250
|
+
const name = String(readField(row, ["index_name", "INDEX_NAME"]));
|
|
251
|
+
const columnName = String(readField(row, ["column_name", "COLUMN_NAME"]));
|
|
252
|
+
const nonUnique = normalizeNumber(readField(row, ["non_unique", "NON_UNIQUE"])) ?? 1;
|
|
253
|
+
const indexType = readField(row, ["index_type", "INDEX_TYPE"]);
|
|
254
|
+
const seqInIndex = normalizeNumber(readField(row, ["seq_in_index", "SEQ_IN_INDEX"])) ?? 0;
|
|
255
|
+
const current = byName.get(name) ?? {
|
|
256
|
+
name,
|
|
257
|
+
columns: [],
|
|
258
|
+
unique: nonUnique === 0,
|
|
259
|
+
type: typeof indexType === "string" ? indexType : undefined,
|
|
260
|
+
};
|
|
261
|
+
if (seqInIndex > current.columns.length) {
|
|
262
|
+
current.columns[seqInIndex - 1] = columnName;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
current.columns.push(columnName);
|
|
266
|
+
}
|
|
267
|
+
byName.set(name, current);
|
|
268
|
+
}
|
|
269
|
+
return [...byName.values()].map((index) => ({
|
|
270
|
+
...index,
|
|
271
|
+
columns: index.columns.filter((column) => typeof column === "string"),
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
async queryObjects(ctx, sql) {
|
|
275
|
+
const session = await this.connectionPool.acquire(ctx.datasource, "ro");
|
|
276
|
+
try {
|
|
277
|
+
const result = await session.execute(sql, { timeoutMs: ctx.limits.timeoutMs });
|
|
278
|
+
return rowsToObjects(result);
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await this.connectionPool.release(session);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export function createMySqlSchemaAdapter(options) {
|
|
286
|
+
return new MySqlSchemaAdapter(options);
|
|
287
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { DatabaseEngine } from "../auth/sql-profile-loader.js";
|
|
2
|
+
import type { SessionContext } from "../context/session-context.js";
|
|
3
|
+
export interface DatabaseInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
owner?: string;
|
|
6
|
+
comment?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TableInfo {
|
|
9
|
+
database: string;
|
|
10
|
+
name: string;
|
|
11
|
+
type?: "table" | "view" | "materialized_view";
|
|
12
|
+
comment?: string;
|
|
13
|
+
rowCountEstimate?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ColumnInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
dataType: string;
|
|
18
|
+
nullable: boolean;
|
|
19
|
+
defaultValue?: unknown;
|
|
20
|
+
maxLength?: number;
|
|
21
|
+
isPrimaryKey?: boolean;
|
|
22
|
+
isIndexed?: boolean;
|
|
23
|
+
comment?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface IndexInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
columns: string[];
|
|
28
|
+
unique: boolean;
|
|
29
|
+
type?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface TableSchema {
|
|
32
|
+
database: string;
|
|
33
|
+
table: string;
|
|
34
|
+
columns: ColumnInfo[];
|
|
35
|
+
indexes: IndexInfo[];
|
|
36
|
+
primaryKey?: string[];
|
|
37
|
+
engineHints?: {
|
|
38
|
+
likelyTimeColumns: string[];
|
|
39
|
+
likelyFilterColumns: string[];
|
|
40
|
+
sensitiveColumns: string[];
|
|
41
|
+
};
|
|
42
|
+
comment?: string;
|
|
43
|
+
rowCountEstimate?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface SchemaAdapter {
|
|
46
|
+
listDatabases(ctx: SessionContext): Promise<DatabaseInfo[]>;
|
|
47
|
+
listTables(ctx: SessionContext, database: string): Promise<TableInfo[]>;
|
|
48
|
+
describeTable(ctx: SessionContext, database: string, table: string): Promise<TableSchema>;
|
|
49
|
+
}
|
|
50
|
+
export interface SchemaIntrospector {
|
|
51
|
+
listDatabases(ctx: SessionContext): Promise<DatabaseInfo[]>;
|
|
52
|
+
listTables(ctx: SessionContext, database: string): Promise<TableInfo[]>;
|
|
53
|
+
describeTable(ctx: SessionContext, database: string, table: string): Promise<TableSchema>;
|
|
54
|
+
}
|
|
55
|
+
export declare class SchemaIntrospectionError extends Error {
|
|
56
|
+
readonly code: "SCHEMA_ADAPTER_NOT_FOUND" | "INVALID_INTROSPECTION_INPUT";
|
|
57
|
+
constructor(code: "SCHEMA_ADAPTER_NOT_FOUND" | "INVALID_INTROSPECTION_INPUT", message: string);
|
|
58
|
+
}
|
|
59
|
+
export type SchemaIntrospectorOptions = {
|
|
60
|
+
adapters: Partial<Record<DatabaseEngine, SchemaAdapter>>;
|
|
61
|
+
};
|
|
62
|
+
export declare class AdapterSchemaIntrospector implements SchemaIntrospector {
|
|
63
|
+
private readonly adapters;
|
|
64
|
+
constructor(options: SchemaIntrospectorOptions);
|
|
65
|
+
listDatabases(ctx: SessionContext): Promise<DatabaseInfo[]>;
|
|
66
|
+
listTables(ctx: SessionContext, database: string): Promise<TableInfo[]>;
|
|
67
|
+
describeTable(ctx: SessionContext, database: string, table: string): Promise<TableSchema>;
|
|
68
|
+
private getAdapter;
|
|
69
|
+
}
|
|
70
|
+
export declare function createSchemaIntrospector(options: SchemaIntrospectorOptions): SchemaIntrospector;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class SchemaIntrospectionError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "SchemaIntrospectionError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function normalizeName(value, fieldName) {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
throw new SchemaIntrospectionError("INVALID_INTROSPECTION_INPUT", `Invalid ${fieldName}: value cannot be empty.`);
|
|
13
|
+
}
|
|
14
|
+
return trimmed;
|
|
15
|
+
}
|
|
16
|
+
export class AdapterSchemaIntrospector {
|
|
17
|
+
adapters;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.adapters = options.adapters;
|
|
20
|
+
}
|
|
21
|
+
async listDatabases(ctx) {
|
|
22
|
+
return this.getAdapter(ctx.engine).listDatabases(ctx);
|
|
23
|
+
}
|
|
24
|
+
async listTables(ctx, database) {
|
|
25
|
+
return this.getAdapter(ctx.engine).listTables(ctx, normalizeName(database, "database"));
|
|
26
|
+
}
|
|
27
|
+
async describeTable(ctx, database, table) {
|
|
28
|
+
return this.getAdapter(ctx.engine).describeTable(ctx, normalizeName(database, "database"), normalizeName(table, "table"));
|
|
29
|
+
}
|
|
30
|
+
getAdapter(engine) {
|
|
31
|
+
const adapter = this.adapters[engine];
|
|
32
|
+
if (!adapter) {
|
|
33
|
+
throw new SchemaIntrospectionError("SCHEMA_ADAPTER_NOT_FOUND", `Schema adapter not found for engine "${engine}".`);
|
|
34
|
+
}
|
|
35
|
+
return adapter;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createSchemaIntrospector(options) {
|
|
39
|
+
return new AdapterSchemaIntrospector(options);
|
|
40
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReadonlyOptions } from "../executor/sql-executor.js";
|
|
2
|
+
export interface FlashbackInput {
|
|
3
|
+
database?: string;
|
|
4
|
+
table: string;
|
|
5
|
+
asOf: {
|
|
6
|
+
timestamp: string;
|
|
7
|
+
relative?: never;
|
|
8
|
+
} | {
|
|
9
|
+
timestamp?: never;
|
|
10
|
+
relative: string;
|
|
11
|
+
};
|
|
12
|
+
where?: string;
|
|
13
|
+
columns?: string[];
|
|
14
|
+
limit?: number;
|
|
15
|
+
}
|
|
16
|
+
export type FlashbackNoViewDetails = {
|
|
17
|
+
database?: string;
|
|
18
|
+
table?: string;
|
|
19
|
+
where?: string;
|
|
20
|
+
requested_timestamp: string;
|
|
21
|
+
current_time?: string;
|
|
22
|
+
backquery_window_seconds?: number;
|
|
23
|
+
earliest_supported_timestamp_estimate?: string;
|
|
24
|
+
current_row_updated_at?: string;
|
|
25
|
+
recommended_timestamps?: string[];
|
|
26
|
+
guidance?: string[];
|
|
27
|
+
};
|
|
28
|
+
export declare class FlashbackNoViewError extends Error {
|
|
29
|
+
readonly details: FlashbackNoViewDetails;
|
|
30
|
+
constructor(message: string, details: FlashbackNoViewDetails);
|
|
31
|
+
}
|
|
32
|
+
export declare function formatTimestamp(date: Date): string;
|
|
33
|
+
export declare function resolveRelativeTimestampFromBase(relative: string, baseTimestamp: string): string;
|
|
34
|
+
export declare function resolveFlashbackTimestamp(asOf: FlashbackInput["asOf"], now?: () => number): string;
|
|
35
|
+
export declare function buildFlashbackSql(input: FlashbackInput, defaultDatabase: string, now?: () => number): string;
|
|
36
|
+
export declare function flashbackReadonlyOptions(limit: number | undefined): ReadonlyOptions | undefined;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
2
|
+
const SQL_TIMESTAMP_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.\d{1,6})?$/;
|
|
3
|
+
const RELATIVE_DURATION_PATTERN = /(\d+)\s*(ms|milliseconds?|s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?|d|days?)/gi;
|
|
4
|
+
const UNIT_TO_MS = {
|
|
5
|
+
ms: 1,
|
|
6
|
+
s: 1000,
|
|
7
|
+
m: 60_000,
|
|
8
|
+
h: 3_600_000,
|
|
9
|
+
d: 86_400_000,
|
|
10
|
+
};
|
|
11
|
+
function normalizeDurationUnit(unit) {
|
|
12
|
+
const normalized = unit.toLowerCase();
|
|
13
|
+
if (normalized.startsWith("ms")) {
|
|
14
|
+
return "ms";
|
|
15
|
+
}
|
|
16
|
+
if (normalized.startsWith("s")) {
|
|
17
|
+
return "s";
|
|
18
|
+
}
|
|
19
|
+
if (normalized.startsWith("m")) {
|
|
20
|
+
return "m";
|
|
21
|
+
}
|
|
22
|
+
if (normalized.startsWith("h")) {
|
|
23
|
+
return "h";
|
|
24
|
+
}
|
|
25
|
+
return "d";
|
|
26
|
+
}
|
|
27
|
+
function quoteIdentifier(identifier, fieldName) {
|
|
28
|
+
if (!IDENTIFIER_PATTERN.test(identifier)) {
|
|
29
|
+
throw new Error(`Invalid ${fieldName}: "${identifier}".`);
|
|
30
|
+
}
|
|
31
|
+
return `\`${identifier}\``;
|
|
32
|
+
}
|
|
33
|
+
export class FlashbackNoViewError extends Error {
|
|
34
|
+
details;
|
|
35
|
+
constructor(message, details) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "FlashbackNoViewError";
|
|
38
|
+
this.details = details;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function formatTimestamp(date) {
|
|
42
|
+
if (Number.isNaN(date.getTime())) {
|
|
43
|
+
throw new Error("Invalid flashback timestamp.");
|
|
44
|
+
}
|
|
45
|
+
const year = String(date.getFullYear()).padStart(4, "0");
|
|
46
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
47
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
48
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
49
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
50
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
51
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
52
|
+
}
|
|
53
|
+
function parseTimestampLiteral(timestamp) {
|
|
54
|
+
const match = timestamp.trim().match(SQL_TIMESTAMP_PATTERN);
|
|
55
|
+
if (!match) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return `${match[1]} ${match[2]}`;
|
|
59
|
+
}
|
|
60
|
+
function parseRelativeDurationMs(relative) {
|
|
61
|
+
const input = relative.trim();
|
|
62
|
+
if (!input) {
|
|
63
|
+
throw new Error("Flashback relative time cannot be empty.");
|
|
64
|
+
}
|
|
65
|
+
let consumed = "";
|
|
66
|
+
let offsetMs = 0;
|
|
67
|
+
for (const match of input.matchAll(RELATIVE_DURATION_PATTERN)) {
|
|
68
|
+
consumed += match[0];
|
|
69
|
+
const amount = Number.parseInt(match[1], 10);
|
|
70
|
+
const unit = normalizeDurationUnit(match[2]);
|
|
71
|
+
offsetMs += amount * UNIT_TO_MS[unit];
|
|
72
|
+
}
|
|
73
|
+
if (offsetMs <= 0 || consumed.replace(/\s+/g, "") !== input.replace(/\s+/g, "")) {
|
|
74
|
+
throw new Error(`Invalid flashback relative time: "${relative}". Expected values like 5m, 10min, 1h, or 2h30m.`);
|
|
75
|
+
}
|
|
76
|
+
return offsetMs;
|
|
77
|
+
}
|
|
78
|
+
function parseTimestampLiteralToDate(timestamp) {
|
|
79
|
+
const literal = parseTimestampLiteral(timestamp);
|
|
80
|
+
if (literal) {
|
|
81
|
+
const match = literal.match(SQL_TIMESTAMP_PATTERN);
|
|
82
|
+
if (!match) {
|
|
83
|
+
throw new Error("Invalid flashback timestamp.");
|
|
84
|
+
}
|
|
85
|
+
const [, datePart, timePart] = match;
|
|
86
|
+
const [year, month, day] = datePart.split("-").map(Number);
|
|
87
|
+
const [hours, minutes, seconds] = timePart.split(":").map(Number);
|
|
88
|
+
const date = new Date(year, month - 1, day, hours, minutes, seconds, 0);
|
|
89
|
+
if (Number.isNaN(date.getTime())) {
|
|
90
|
+
throw new Error("Invalid flashback timestamp.");
|
|
91
|
+
}
|
|
92
|
+
return date;
|
|
93
|
+
}
|
|
94
|
+
const parsed = new Date(timestamp);
|
|
95
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
96
|
+
throw new Error("Invalid flashback timestamp.");
|
|
97
|
+
}
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
export function resolveRelativeTimestampFromBase(relative, baseTimestamp) {
|
|
101
|
+
const offsetMs = parseRelativeDurationMs(relative);
|
|
102
|
+
const baseDate = parseTimestampLiteralToDate(baseTimestamp);
|
|
103
|
+
return formatTimestamp(new Date(baseDate.getTime() - offsetMs));
|
|
104
|
+
}
|
|
105
|
+
export function resolveFlashbackTimestamp(asOf, now = Date.now) {
|
|
106
|
+
if ("timestamp" in asOf && typeof asOf.timestamp === "string") {
|
|
107
|
+
const literal = parseTimestampLiteral(asOf.timestamp);
|
|
108
|
+
if (literal) {
|
|
109
|
+
return literal;
|
|
110
|
+
}
|
|
111
|
+
return formatTimestamp(new Date(asOf.timestamp));
|
|
112
|
+
}
|
|
113
|
+
if ("relative" in asOf && typeof asOf.relative === "string") {
|
|
114
|
+
const offsetMs = parseRelativeDurationMs(asOf.relative);
|
|
115
|
+
return formatTimestamp(new Date(now() - offsetMs));
|
|
116
|
+
}
|
|
117
|
+
throw new Error("Flashback query requires either as_of.timestamp or as_of.relative.");
|
|
118
|
+
}
|
|
119
|
+
export function buildFlashbackSql(input, defaultDatabase, now = Date.now) {
|
|
120
|
+
const database = quoteIdentifier(input.database ?? defaultDatabase, "database");
|
|
121
|
+
const table = quoteIdentifier(input.table, "table");
|
|
122
|
+
const columns = input.columns && input.columns.length > 0
|
|
123
|
+
? input.columns.map((column) => quoteIdentifier(column, "column")).join(", ")
|
|
124
|
+
: "*";
|
|
125
|
+
const timestamp = resolveFlashbackTimestamp(input.asOf, now);
|
|
126
|
+
const clauses = [
|
|
127
|
+
`SELECT ${columns}`,
|
|
128
|
+
`FROM ${database}.${table} AS OF TIMESTAMP '${timestamp}'`,
|
|
129
|
+
];
|
|
130
|
+
const whereClause = input.where?.trim();
|
|
131
|
+
if (whereClause) {
|
|
132
|
+
clauses.push(`WHERE (${whereClause})`);
|
|
133
|
+
}
|
|
134
|
+
if (input.limit !== undefined) {
|
|
135
|
+
if (!Number.isInteger(input.limit) || input.limit <= 0) {
|
|
136
|
+
throw new Error("Flashback query limit must be a positive integer.");
|
|
137
|
+
}
|
|
138
|
+
clauses.push(`LIMIT ${input.limit}`);
|
|
139
|
+
}
|
|
140
|
+
return clauses.join(" ");
|
|
141
|
+
}
|
|
142
|
+
export function flashbackReadonlyOptions(limit) {
|
|
143
|
+
if (limit === undefined) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
maxRows: limit,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MutationOptions, MutationResult, QueryResult, ReadonlyOptions } from "../executor/sql-executor.js";
|
|
2
|
+
export interface RestoreRecycleBinTableInput {
|
|
3
|
+
recycleTable: string;
|
|
4
|
+
method?: "native_restore" | "insert_select";
|
|
5
|
+
destinationDatabase?: string;
|
|
6
|
+
destinationTable?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const RECYCLE_BIN_DATABASE = "__recyclebin__";
|
|
9
|
+
export declare function buildListRecycleBinSql(): string;
|
|
10
|
+
export declare function buildRestoreRecycleBinTableSql(input: RestoreRecycleBinTableInput): string;
|
|
11
|
+
export declare function recycleBinReadonlyOptions(opts?: ReadonlyOptions): ReadonlyOptions;
|
|
12
|
+
export declare function recycleBinMutationOptions(opts?: MutationOptions): MutationOptions | undefined;
|
|
13
|
+
export type RecycleBinListResult = QueryResult;
|
|
14
|
+
export type RecycleBinRestoreResult = MutationResult;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const RECYCLE_BIN_DATABASE = "__recyclebin__";
|
|
2
|
+
const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
3
|
+
const RECYCLE_TABLE_PATTERN = /^[A-Za-z0-9_$@.-]+$/;
|
|
4
|
+
function quoteIdentifier(identifier, fieldName) {
|
|
5
|
+
if (!SIMPLE_IDENTIFIER_PATTERN.test(identifier)) {
|
|
6
|
+
throw new Error(`Invalid ${fieldName}: "${identifier}".`);
|
|
7
|
+
}
|
|
8
|
+
return `\`${identifier}\``;
|
|
9
|
+
}
|
|
10
|
+
function quoteRecycleTableName(table) {
|
|
11
|
+
if (!RECYCLE_TABLE_PATTERN.test(table)) {
|
|
12
|
+
throw new Error(`Invalid recycle_table: "${table}".`);
|
|
13
|
+
}
|
|
14
|
+
return `\`${table.replace(/`/g, "``")}\``;
|
|
15
|
+
}
|
|
16
|
+
function quoteStringLiteral(value, fieldName) {
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
throw new Error(`Invalid ${fieldName}: value cannot be empty.`);
|
|
20
|
+
}
|
|
21
|
+
if (trimmed !== value) {
|
|
22
|
+
throw new Error(`Invalid ${fieldName}: extra leading or trailing spaces are not allowed.`);
|
|
23
|
+
}
|
|
24
|
+
if (!RECYCLE_TABLE_PATTERN.test(trimmed) && fieldName === "recycle_table") {
|
|
25
|
+
throw new Error(`Invalid recycle_table: "${value}".`);
|
|
26
|
+
}
|
|
27
|
+
return `'${trimmed.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
28
|
+
}
|
|
29
|
+
export function buildListRecycleBinSql() {
|
|
30
|
+
return "call dbms_recyclebin.show_tables()";
|
|
31
|
+
}
|
|
32
|
+
export function buildRestoreRecycleBinTableSql(input) {
|
|
33
|
+
const method = input.method ?? "native_restore";
|
|
34
|
+
const recycleTable = quoteStringLiteral(input.recycleTable, "recycle_table");
|
|
35
|
+
if (method === "native_restore") {
|
|
36
|
+
if (input.destinationDatabase || input.destinationTable) {
|
|
37
|
+
if (!input.destinationDatabase || !input.destinationTable) {
|
|
38
|
+
throw new Error("native_restore requires destination_database and destination_table to be provided together.");
|
|
39
|
+
}
|
|
40
|
+
return `call dbms_recyclebin.restore_table(${recycleTable}, ${quoteStringLiteral(input.destinationDatabase, "destination_database")}, ${quoteStringLiteral(input.destinationTable, "destination_table")})`;
|
|
41
|
+
}
|
|
42
|
+
return `call dbms_recyclebin.restore_table(${recycleTable})`;
|
|
43
|
+
}
|
|
44
|
+
if (method === "insert_select") {
|
|
45
|
+
if (!input.destinationDatabase || !input.destinationTable) {
|
|
46
|
+
throw new Error("insert_select restore requires destination_database and destination_table. Create the destination table with a compatible structure before calling this tool.");
|
|
47
|
+
}
|
|
48
|
+
return `INSERT INTO ${quoteIdentifier(input.destinationDatabase, "destination_database")}.${quoteIdentifier(input.destinationTable, "destination_table")} SELECT * FROM \`${RECYCLE_BIN_DATABASE}\`.${quoteRecycleTableName(input.recycleTable)}`;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Unsupported restore method: ${method}.`);
|
|
51
|
+
}
|
|
52
|
+
export function recycleBinReadonlyOptions(opts) {
|
|
53
|
+
return {
|
|
54
|
+
maxRows: 100,
|
|
55
|
+
maxColumns: 20,
|
|
56
|
+
...opts,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function recycleBinMutationOptions(opts) {
|
|
60
|
+
return opts;
|
|
61
|
+
}
|