@yeyuan98/opencode-bioresearcher-plugin 1.4.1 → 1.5.0-alpha.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 +35 -20
- package/dist/db-tools/backends/index.d.ts +11 -0
- package/dist/db-tools/backends/index.js +48 -0
- package/dist/db-tools/backends/mongodb/backend.d.ts +15 -0
- package/dist/db-tools/backends/mongodb/backend.js +76 -0
- package/dist/db-tools/backends/mongodb/connection.d.ts +27 -0
- package/dist/db-tools/backends/mongodb/connection.js +107 -0
- package/dist/db-tools/backends/mongodb/index.d.ts +4 -0
- package/dist/db-tools/backends/mongodb/index.js +3 -0
- package/dist/db-tools/backends/mongodb/translator.d.ts +30 -0
- package/dist/db-tools/backends/mongodb/translator.js +407 -0
- package/dist/db-tools/backends/mysql/backend.d.ts +15 -0
- package/dist/db-tools/backends/mysql/backend.js +57 -0
- package/dist/db-tools/backends/mysql/connection.d.ts +25 -0
- package/dist/db-tools/backends/mysql/connection.js +83 -0
- package/dist/db-tools/backends/mysql/index.d.ts +3 -0
- package/dist/db-tools/backends/mysql/index.js +2 -0
- package/dist/db-tools/backends/mysql/translator.d.ts +7 -0
- package/dist/db-tools/backends/mysql/translator.js +67 -0
- package/dist/db-tools/core/base.d.ts +17 -0
- package/dist/db-tools/core/base.js +51 -0
- package/dist/db-tools/core/config-loader.d.ts +3 -0
- package/dist/db-tools/core/config-loader.js +46 -0
- package/dist/db-tools/core/index.d.ts +2 -0
- package/dist/db-tools/core/index.js +2 -0
- package/dist/db-tools/core/jsonc-parser.d.ts +2 -0
- package/dist/db-tools/core/jsonc-parser.js +77 -0
- package/dist/db-tools/core/validator.d.ts +16 -0
- package/dist/db-tools/core/validator.js +118 -0
- package/dist/db-tools/executor.d.ts +13 -0
- package/dist/db-tools/executor.js +54 -0
- package/dist/db-tools/index.d.ts +51 -0
- package/dist/db-tools/index.js +27 -0
- package/dist/db-tools/interface/backend.d.ts +24 -0
- package/dist/db-tools/interface/backend.js +1 -0
- package/dist/db-tools/interface/connection.d.ts +21 -0
- package/dist/db-tools/interface/connection.js +11 -0
- package/dist/db-tools/interface/index.d.ts +4 -0
- package/dist/db-tools/interface/index.js +4 -0
- package/dist/db-tools/interface/query.d.ts +60 -0
- package/dist/db-tools/interface/query.js +1 -0
- package/dist/db-tools/interface/schema.d.ts +22 -0
- package/dist/db-tools/interface/schema.js +1 -0
- package/dist/db-tools/pool.d.ts +8 -0
- package/dist/db-tools/pool.js +49 -0
- package/dist/db-tools/tools/index.d.ts +27 -0
- package/dist/db-tools/tools/index.js +191 -0
- package/dist/db-tools/tools.d.ts +27 -0
- package/dist/db-tools/tools.js +111 -0
- package/dist/db-tools/types.d.ts +94 -0
- package/dist/db-tools/types.js +40 -0
- package/dist/db-tools/utils.d.ts +33 -0
- package/dist/db-tools/utils.js +94 -0
- package/dist/index.js +2 -0
- package/dist/skills/env-jsonc-setup/SKILL.md +206 -0
- package/dist/skills/long-table-summary/SKILL.md +437 -374
- package/dist/skills/long-table-summary/combine_outputs.py +5 -14
- package/dist/skills/long-table-summary/generate_prompts.py +211 -0
- package/dist/skills/long-table-summary/pyproject.toml +8 -11
- package/package.json +3 -1
- package/dist/skills/long-table-summary/__init__.py +0 -3
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
export class SQLParseError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(`SQL Parse Error: ${message}`);
|
|
4
|
+
this.name = 'SQLParseError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function parseSelectQuery(sql) {
|
|
8
|
+
const normalized = sql.trim().replace(/\s+/g, ' ');
|
|
9
|
+
if (!normalized.toUpperCase().startsWith('SELECT')) {
|
|
10
|
+
throw new SQLParseError('Only SELECT queries are supported');
|
|
11
|
+
}
|
|
12
|
+
const result = {
|
|
13
|
+
collection: '',
|
|
14
|
+
fields: null,
|
|
15
|
+
where: null,
|
|
16
|
+
orderBy: null,
|
|
17
|
+
limit: null,
|
|
18
|
+
offset: null,
|
|
19
|
+
groupBy: null,
|
|
20
|
+
having: null,
|
|
21
|
+
aggregations: null,
|
|
22
|
+
};
|
|
23
|
+
const selectMatch = normalized.match(/^SELECT\s+(.+?)\s+FROM\s+(\w+)/i);
|
|
24
|
+
if (!selectMatch) {
|
|
25
|
+
throw new SQLParseError('Invalid SELECT syntax: expected SELECT ... FROM table');
|
|
26
|
+
}
|
|
27
|
+
const fieldsStr = selectMatch[1].trim();
|
|
28
|
+
const tableName = selectMatch[2].trim();
|
|
29
|
+
result.collection = tableName;
|
|
30
|
+
if (fieldsStr === '*') {
|
|
31
|
+
result.fields = null;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
result.fields = fieldsStr.split(',').map(f => f.trim());
|
|
35
|
+
}
|
|
36
|
+
result.aggregations = parseAggregations(fieldsStr);
|
|
37
|
+
let remaining = normalized.substring(selectMatch[0].length).trim();
|
|
38
|
+
const whereMatch = remaining.match(/^WHERE\s+(.+?)(?=\s+(?:ORDER|GROUP|LIMIT|OFFSET|$))/i);
|
|
39
|
+
if (whereMatch) {
|
|
40
|
+
result.where = parseWhereClause(whereMatch[1].trim());
|
|
41
|
+
remaining = remaining.substring(whereMatch[0].length).trim();
|
|
42
|
+
}
|
|
43
|
+
const groupByMatch = remaining.match(/^GROUP\s+BY\s+(.+?)(?=\s+(?:HAVING|ORDER|LIMIT|OFFSET|$))/i);
|
|
44
|
+
if (groupByMatch) {
|
|
45
|
+
result.groupBy = groupByMatch[1].trim().split(',').map(f => f.trim());
|
|
46
|
+
remaining = remaining.substring(groupByMatch[0].length).trim();
|
|
47
|
+
}
|
|
48
|
+
const havingMatch = remaining.match(/^HAVING\s+(.+?)(?=\s+(?:ORDER|LIMIT|OFFSET|$))/i);
|
|
49
|
+
if (havingMatch) {
|
|
50
|
+
result.having = parseWhereClause(havingMatch[1].trim());
|
|
51
|
+
remaining = remaining.substring(havingMatch[0].length).trim();
|
|
52
|
+
}
|
|
53
|
+
const orderByMatch = remaining.match(/^ORDER\s+BY\s+(.+?)(?=\s+(?:LIMIT|OFFSET|$))/i);
|
|
54
|
+
if (orderByMatch) {
|
|
55
|
+
result.orderBy = parseOrderBy(orderByMatch[1].trim());
|
|
56
|
+
remaining = remaining.substring(orderByMatch[0].length).trim();
|
|
57
|
+
}
|
|
58
|
+
const limitMatch = remaining.match(/^LIMIT\s+(\d+)/i);
|
|
59
|
+
if (limitMatch) {
|
|
60
|
+
result.limit = parseInt(limitMatch[1], 10);
|
|
61
|
+
remaining = remaining.substring(limitMatch[0].length).trim();
|
|
62
|
+
}
|
|
63
|
+
const offsetMatch = remaining.match(/^OFFSET\s+(\d+)/i);
|
|
64
|
+
if (offsetMatch) {
|
|
65
|
+
result.offset = parseInt(offsetMatch[1], 10);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function parseAggregations(fieldsStr) {
|
|
70
|
+
const aggRegex = /(COUNT|SUM|AVG|MIN|MAX)\s*\(\s*(\*|\w+)\s*\)(?:\s+(?:AS\s+)?(\w+))?/gi;
|
|
71
|
+
const aggregations = [];
|
|
72
|
+
let match;
|
|
73
|
+
while ((match = aggRegex.exec(fieldsStr)) !== null) {
|
|
74
|
+
aggregations.push({
|
|
75
|
+
type: match[1].toUpperCase(),
|
|
76
|
+
field: match[2] === '*' ? '_all' : match[2],
|
|
77
|
+
alias: match[3],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return aggregations.length > 0 ? aggregations : null;
|
|
81
|
+
}
|
|
82
|
+
function parseWhereClause(whereStr) {
|
|
83
|
+
const conditions = [];
|
|
84
|
+
const andParts = whereStr.split(/\s+AND\s+/i);
|
|
85
|
+
for (const part of andParts) {
|
|
86
|
+
const orConditions = [];
|
|
87
|
+
const orParts = part.split(/\s+OR\s+/i);
|
|
88
|
+
for (const orPart of orParts) {
|
|
89
|
+
const condition = parseCondition(orPart.trim());
|
|
90
|
+
if (condition) {
|
|
91
|
+
orConditions.push(condition);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (orConditions.length === 1) {
|
|
95
|
+
conditions.push(orConditions[0]);
|
|
96
|
+
}
|
|
97
|
+
else if (orConditions.length > 1) {
|
|
98
|
+
conditions.push({ $or: orConditions });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (conditions.length === 0) {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
else if (conditions.length === 1) {
|
|
105
|
+
return conditions[0];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
return { $and: conditions };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function parseCondition(conditionStr) {
|
|
112
|
+
const comparisonMatch = conditionStr.match(/^(\w+)\s*(=|!=|<>|>=|<=|>|<|LIKE|NOT\s+LIKE)\s*['"]?([^'"]+)['"]?$/i);
|
|
113
|
+
if (!comparisonMatch) {
|
|
114
|
+
const inMatch = conditionStr.match(/^(\w+)\s+(NOT\s+)?IN\s*\((.+)\)$/i);
|
|
115
|
+
if (inMatch) {
|
|
116
|
+
const field = inMatch[1];
|
|
117
|
+
const isNot = !!inMatch[2];
|
|
118
|
+
const values = inMatch[3].split(',').map(v => {
|
|
119
|
+
const trimmed = v.trim();
|
|
120
|
+
if (trimmed.startsWith("'") || trimmed.startsWith('"')) {
|
|
121
|
+
return trimmed.slice(1, -1);
|
|
122
|
+
}
|
|
123
|
+
const num = Number(trimmed);
|
|
124
|
+
return isNaN(num) ? trimmed : num;
|
|
125
|
+
});
|
|
126
|
+
if (isNot) {
|
|
127
|
+
return { [field]: { $nin: values } };
|
|
128
|
+
}
|
|
129
|
+
return { [field]: { $in: values } };
|
|
130
|
+
}
|
|
131
|
+
const isNullMatch = conditionStr.match(/^(\w+)\s+IS\s+(NOT\s+)?NULL$/i);
|
|
132
|
+
if (isNullMatch) {
|
|
133
|
+
const field = isNullMatch[1];
|
|
134
|
+
const isNot = !!isNullMatch[2];
|
|
135
|
+
return { [field]: { $exists: isNot } };
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const field = comparisonMatch[1];
|
|
140
|
+
const operator = comparisonMatch[2].toUpperCase().replace(/\s+/g, ' ');
|
|
141
|
+
let value = comparisonMatch[3];
|
|
142
|
+
if (typeof value === 'string') {
|
|
143
|
+
if (value.toLowerCase() === 'true')
|
|
144
|
+
value = true;
|
|
145
|
+
else if (value.toLowerCase() === 'false')
|
|
146
|
+
value = false;
|
|
147
|
+
else {
|
|
148
|
+
const num = Number(value);
|
|
149
|
+
if (!isNaN(num))
|
|
150
|
+
value = num;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
switch (operator) {
|
|
154
|
+
case '=':
|
|
155
|
+
return { [field]: value };
|
|
156
|
+
case '!=':
|
|
157
|
+
case '<>':
|
|
158
|
+
return { [field]: { $ne: value } };
|
|
159
|
+
case '>':
|
|
160
|
+
return { [field]: { $gt: value } };
|
|
161
|
+
case '>=':
|
|
162
|
+
return { [field]: { $gte: value } };
|
|
163
|
+
case '<':
|
|
164
|
+
return { [field]: { $lt: value } };
|
|
165
|
+
case '<=':
|
|
166
|
+
return { [field]: { $lte: value } };
|
|
167
|
+
case 'LIKE':
|
|
168
|
+
return { [field]: { $regex: sqlLikeToRegex(value) } };
|
|
169
|
+
case 'NOT LIKE':
|
|
170
|
+
return { [field]: { $not: { $regex: sqlLikeToRegex(value) } } };
|
|
171
|
+
default:
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function sqlLikeToRegex(pattern) {
|
|
176
|
+
return pattern
|
|
177
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
178
|
+
.replace(/%/g, '.*')
|
|
179
|
+
.replace(/_/g, '.');
|
|
180
|
+
}
|
|
181
|
+
function parseOrderBy(orderByStr) {
|
|
182
|
+
const sort = {};
|
|
183
|
+
const parts = orderByStr.split(',').map(p => p.trim());
|
|
184
|
+
for (const part of parts) {
|
|
185
|
+
const match = part.match(/^(\w+)(?:\s+(ASC|DESC))?$/i);
|
|
186
|
+
if (match) {
|
|
187
|
+
const field = match[1];
|
|
188
|
+
const direction = (match[2] || 'ASC').toUpperCase();
|
|
189
|
+
sort[field] = direction === 'DESC' ? -1 : 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return sort;
|
|
193
|
+
}
|
|
194
|
+
export async function executeParsedQuery(collection, parsed, params) {
|
|
195
|
+
if (parsed.groupBy || parsed.aggregations) {
|
|
196
|
+
return executeAggregationQuery(collection, parsed);
|
|
197
|
+
}
|
|
198
|
+
let filter = (parsed.where || {});
|
|
199
|
+
if (params && !Array.isArray(params)) {
|
|
200
|
+
filter = replaceParams(filter, params);
|
|
201
|
+
}
|
|
202
|
+
const options = {};
|
|
203
|
+
if (parsed.fields && parsed.fields.length > 0) {
|
|
204
|
+
const projection = {};
|
|
205
|
+
for (const field of parsed.fields) {
|
|
206
|
+
projection[field] = 1;
|
|
207
|
+
}
|
|
208
|
+
options.projection = projection;
|
|
209
|
+
}
|
|
210
|
+
if (parsed.limit !== null) {
|
|
211
|
+
options.limit = parsed.limit;
|
|
212
|
+
}
|
|
213
|
+
if (parsed.offset !== null) {
|
|
214
|
+
options.skip = parsed.offset;
|
|
215
|
+
}
|
|
216
|
+
if (parsed.orderBy) {
|
|
217
|
+
options.sort = parsed.orderBy;
|
|
218
|
+
}
|
|
219
|
+
const cursor = collection.find(filter, options);
|
|
220
|
+
const docs = await cursor.toArray();
|
|
221
|
+
return docs.map(doc => {
|
|
222
|
+
const row = {};
|
|
223
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
224
|
+
if (key === '_id') {
|
|
225
|
+
row[key] = value.toString();
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
row[key] = value;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return row;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function executeAggregationQuery(collection, parsed) {
|
|
235
|
+
const pipeline = [];
|
|
236
|
+
if (parsed.where) {
|
|
237
|
+
pipeline.push({ $match: parsed.where });
|
|
238
|
+
}
|
|
239
|
+
if (parsed.groupBy && parsed.aggregations) {
|
|
240
|
+
const groupId = {};
|
|
241
|
+
for (const field of parsed.groupBy) {
|
|
242
|
+
groupId[field] = `$${field}`;
|
|
243
|
+
}
|
|
244
|
+
const groupAccumulators = {};
|
|
245
|
+
for (const agg of parsed.aggregations) {
|
|
246
|
+
const alias = agg.alias || `${agg.type.toLowerCase()}_${agg.field}`;
|
|
247
|
+
switch (agg.type) {
|
|
248
|
+
case 'COUNT':
|
|
249
|
+
groupAccumulators[alias] = agg.field === '_all' ? { $sum: 1 } : { $sum: { $cond: [`$${agg.field}`, 1, 0] } };
|
|
250
|
+
break;
|
|
251
|
+
case 'SUM':
|
|
252
|
+
groupAccumulators[alias] = { $sum: `$${agg.field}` };
|
|
253
|
+
break;
|
|
254
|
+
case 'AVG':
|
|
255
|
+
groupAccumulators[alias] = { $avg: `$${agg.field}` };
|
|
256
|
+
break;
|
|
257
|
+
case 'MIN':
|
|
258
|
+
groupAccumulators[alias] = { $min: `$${agg.field}` };
|
|
259
|
+
break;
|
|
260
|
+
case 'MAX':
|
|
261
|
+
groupAccumulators[alias] = { $max: `$${agg.field}` };
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
pipeline.push({
|
|
266
|
+
$group: {
|
|
267
|
+
_id: groupId,
|
|
268
|
+
...groupAccumulators,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
else if (parsed.aggregations) {
|
|
273
|
+
const groupAccumulators = {};
|
|
274
|
+
for (const agg of parsed.aggregations) {
|
|
275
|
+
const alias = agg.alias || `${agg.type.toLowerCase()}_${agg.field}`;
|
|
276
|
+
switch (agg.type) {
|
|
277
|
+
case 'COUNT':
|
|
278
|
+
groupAccumulators[alias] = agg.field === '_all' ? { $sum: 1 } : { $sum: { $cond: [`$${agg.field}`, 1, 0] } };
|
|
279
|
+
break;
|
|
280
|
+
case 'SUM':
|
|
281
|
+
groupAccumulators[alias] = { $sum: `$${agg.field}` };
|
|
282
|
+
break;
|
|
283
|
+
case 'AVG':
|
|
284
|
+
groupAccumulators[alias] = { $avg: `$${agg.field}` };
|
|
285
|
+
break;
|
|
286
|
+
case 'MIN':
|
|
287
|
+
groupAccumulators[alias] = { $min: `$${agg.field}` };
|
|
288
|
+
break;
|
|
289
|
+
case 'MAX':
|
|
290
|
+
groupAccumulators[alias] = { $max: `$${agg.field}` };
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
pipeline.push({
|
|
295
|
+
$group: {
|
|
296
|
+
_id: null,
|
|
297
|
+
...groupAccumulators,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (parsed.having) {
|
|
302
|
+
pipeline.push({ $match: parsed.having });
|
|
303
|
+
}
|
|
304
|
+
if (parsed.orderBy) {
|
|
305
|
+
pipeline.push({ $sort: parsed.orderBy });
|
|
306
|
+
}
|
|
307
|
+
if (parsed.limit !== null) {
|
|
308
|
+
pipeline.push({ $limit: parsed.limit });
|
|
309
|
+
}
|
|
310
|
+
if (parsed.offset !== null) {
|
|
311
|
+
pipeline.push({ $skip: parsed.offset });
|
|
312
|
+
}
|
|
313
|
+
const results = await collection.aggregate(pipeline).toArray();
|
|
314
|
+
return results.map(doc => {
|
|
315
|
+
const row = {};
|
|
316
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
317
|
+
if (key === '_id' && typeof value === 'object' && value !== null) {
|
|
318
|
+
Object.assign(row, value);
|
|
319
|
+
}
|
|
320
|
+
else if (key !== '_id') {
|
|
321
|
+
row[key] = value;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return row;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function replaceParams(obj, params) {
|
|
328
|
+
if (typeof obj === 'string') {
|
|
329
|
+
let result = obj;
|
|
330
|
+
for (const [key, value] of Object.entries(params)) {
|
|
331
|
+
const placeholder = `:${key}`;
|
|
332
|
+
if (result.includes(placeholder)) {
|
|
333
|
+
result = result.replace(placeholder, String(value));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
if (Array.isArray(obj)) {
|
|
339
|
+
return obj.map(item => replaceParams(item, params));
|
|
340
|
+
}
|
|
341
|
+
if (obj && typeof obj === 'object') {
|
|
342
|
+
const result = {};
|
|
343
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
344
|
+
result[key] = replaceParams(value, params);
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
return obj;
|
|
349
|
+
}
|
|
350
|
+
export async function listMongoDBCollections(db) {
|
|
351
|
+
const collections = await db.listCollections().toArray();
|
|
352
|
+
return collections.map(col => ({
|
|
353
|
+
name: col.name,
|
|
354
|
+
type: col.type === 'view' ? 'view' : 'collection',
|
|
355
|
+
engine: null,
|
|
356
|
+
rowCount: null,
|
|
357
|
+
createdAt: null,
|
|
358
|
+
comment: null,
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
export async function describeMongoDBCollection(db, collectionName) {
|
|
362
|
+
const collections = await db.listCollections({ name: collectionName }, { nameOnly: false }).toArray();
|
|
363
|
+
if (collections.length === 0) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
const collectionInfo = collections[0];
|
|
367
|
+
const columns = [];
|
|
368
|
+
const options = collectionInfo.options;
|
|
369
|
+
if (options?.validator?.$jsonSchema?.properties) {
|
|
370
|
+
const props = options.validator.$jsonSchema.properties;
|
|
371
|
+
const required = options.validator.$jsonSchema.required || [];
|
|
372
|
+
for (const [fieldName, fieldDef] of Object.entries(props)) {
|
|
373
|
+
const def = fieldDef;
|
|
374
|
+
columns.push({
|
|
375
|
+
field: fieldName,
|
|
376
|
+
type: def.bsonType || def.type || 'unknown',
|
|
377
|
+
nullable: !required.includes(fieldName),
|
|
378
|
+
key: fieldName === '_id' ? 'PRI' : null,
|
|
379
|
+
defaultValue: null,
|
|
380
|
+
extra: null,
|
|
381
|
+
comment: def.description || null,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (columns.length === 0) {
|
|
386
|
+
columns.push({
|
|
387
|
+
field: '_id',
|
|
388
|
+
type: 'ObjectId',
|
|
389
|
+
nullable: false,
|
|
390
|
+
key: 'PRI',
|
|
391
|
+
defaultValue: null,
|
|
392
|
+
extra: null,
|
|
393
|
+
comment: null,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return columns;
|
|
397
|
+
}
|
|
398
|
+
export function createFieldInfoFromRow(row) {
|
|
399
|
+
return Object.keys(row).map(key => ({
|
|
400
|
+
name: key,
|
|
401
|
+
type: typeof row[key] === 'number' ? 'number' : typeof row[key] === 'boolean' ? 'boolean' : 'string',
|
|
402
|
+
nullable: true,
|
|
403
|
+
key: key === '_id' ? 'PRI' : null,
|
|
404
|
+
default: null,
|
|
405
|
+
extra: null,
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IConnectionConfig, IQuery, IQueryResult, ICollectionInfo, IColumnSchema, IConnectionPool } from '../../interface';
|
|
2
|
+
import { BaseBackend } from '../../core/base';
|
|
3
|
+
import { MySQLConnectionConfig } from './connection';
|
|
4
|
+
export declare class MySQLBackend extends BaseBackend {
|
|
5
|
+
readonly type = "mysql";
|
|
6
|
+
readonly config: MySQLConnectionConfig;
|
|
7
|
+
constructor(config: IConnectionConfig);
|
|
8
|
+
connect(): Promise<void>;
|
|
9
|
+
disconnect(): Promise<void>;
|
|
10
|
+
getPool(): IConnectionPool | null;
|
|
11
|
+
executeQuery(query: IQuery): Promise<IQueryResult>;
|
|
12
|
+
listCollections(): Promise<ICollectionInfo[]>;
|
|
13
|
+
describeCollection(name: string): Promise<IColumnSchema[]>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createMySQLBackend(config: IConnectionConfig): MySQLBackend;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BaseBackend } from '../../core/base';
|
|
2
|
+
import { getPool, closePool, validateMySQLConfig, MySQLConnectionPool, } from './connection';
|
|
3
|
+
import { extractFieldInfo, mapMySQLTablesToCollections, mapMySQLColumnsToSchema, LIST_TABLES_SQL, DESCRIBE_TABLE_SQL, } from './translator';
|
|
4
|
+
export class MySQLBackend extends BaseBackend {
|
|
5
|
+
type = 'mysql';
|
|
6
|
+
config;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super();
|
|
9
|
+
this.config = {
|
|
10
|
+
...config,
|
|
11
|
+
charset: config.options?.charset || 'utf8mb4',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async connect() {
|
|
15
|
+
validateMySQLConfig(this.config);
|
|
16
|
+
getPool(this.config);
|
|
17
|
+
this._connected = true;
|
|
18
|
+
}
|
|
19
|
+
async disconnect() {
|
|
20
|
+
await closePool();
|
|
21
|
+
this._connected = false;
|
|
22
|
+
}
|
|
23
|
+
getPool() {
|
|
24
|
+
const mysqlPool = getPool(this.config);
|
|
25
|
+
return new MySQLConnectionPool(mysqlPool);
|
|
26
|
+
}
|
|
27
|
+
async executeQuery(query) {
|
|
28
|
+
const startTime = Date.now();
|
|
29
|
+
try {
|
|
30
|
+
if (!this._connected) {
|
|
31
|
+
await this.connect();
|
|
32
|
+
}
|
|
33
|
+
const pool = getPool(this.config);
|
|
34
|
+
const params = query.params ?? {};
|
|
35
|
+
const [rows, fields] = await pool.query(query.sql, params);
|
|
36
|
+
const executionTimeMs = Date.now() - startTime;
|
|
37
|
+
return this.createSuccessResult(rows, extractFieldInfo(fields), executionTimeMs);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const executionTimeMs = Date.now() - startTime;
|
|
41
|
+
return this.createErrorResult(error, executionTimeMs);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async listCollections() {
|
|
45
|
+
const pool = getPool(this.config);
|
|
46
|
+
const [rows] = await pool.query(LIST_TABLES_SQL);
|
|
47
|
+
return mapMySQLTablesToCollections(rows);
|
|
48
|
+
}
|
|
49
|
+
async describeCollection(name) {
|
|
50
|
+
const pool = getPool(this.config);
|
|
51
|
+
const [rows] = await pool.query(DESCRIBE_TABLE_SQL, [name]);
|
|
52
|
+
return mapMySQLColumnsToSchema(rows);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function createMySQLBackend(config) {
|
|
56
|
+
return new MySQLBackend(config);
|
|
57
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as mysql from 'mysql2/promise';
|
|
2
|
+
import type { IConnectionConfig, IConnectionPool, IConnection } from '../../interface';
|
|
3
|
+
export interface MySQLConnectionConfig extends IConnectionConfig {
|
|
4
|
+
charset?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MySQLPoolOptions extends mysql.PoolOptions {
|
|
7
|
+
namedPlaceholders: boolean;
|
|
8
|
+
}
|
|
9
|
+
declare let pool: mysql.Pool | null;
|
|
10
|
+
export declare function createPoolConfig(config: MySQLConnectionConfig): MySQLPoolOptions;
|
|
11
|
+
export declare function getPool(config: MySQLConnectionConfig): mysql.Pool;
|
|
12
|
+
export declare function closePool(): Promise<void>;
|
|
13
|
+
export declare function getPoolStatus(): {
|
|
14
|
+
initialized: boolean;
|
|
15
|
+
hasConfig: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function validateMySQLConfig(config: MySQLConnectionConfig): void;
|
|
18
|
+
export declare class MySQLConnectionPool implements IConnectionPool {
|
|
19
|
+
private pool;
|
|
20
|
+
constructor(pool: mysql.Pool);
|
|
21
|
+
getConnection(): Promise<IConnection>;
|
|
22
|
+
end(): Promise<void>;
|
|
23
|
+
isHealthy(): boolean;
|
|
24
|
+
}
|
|
25
|
+
export { pool as _internalPool };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as mysql from 'mysql2/promise';
|
|
2
|
+
let pool = null;
|
|
3
|
+
let currentConfig = null;
|
|
4
|
+
export function createPoolConfig(config) {
|
|
5
|
+
return {
|
|
6
|
+
host: config.host,
|
|
7
|
+
port: config.port,
|
|
8
|
+
user: config.username,
|
|
9
|
+
password: config.password,
|
|
10
|
+
database: config.database,
|
|
11
|
+
charset: config.charset || 'utf8mb4',
|
|
12
|
+
connectTimeout: config.connectionTimeout || 10000,
|
|
13
|
+
waitForConnections: true,
|
|
14
|
+
connectionLimit: 10,
|
|
15
|
+
queueLimit: 0,
|
|
16
|
+
namedPlaceholders: true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function getPool(config) {
|
|
20
|
+
if (pool && currentConfig && isSameConfig(currentConfig, config)) {
|
|
21
|
+
return pool;
|
|
22
|
+
}
|
|
23
|
+
if (pool) {
|
|
24
|
+
pool.end().catch(err => console.error('[mysql-backend] Previous pool cleanup failed:', err.message));
|
|
25
|
+
}
|
|
26
|
+
pool = mysql.createPool(createPoolConfig(config));
|
|
27
|
+
currentConfig = config;
|
|
28
|
+
return pool;
|
|
29
|
+
}
|
|
30
|
+
function isSameConfig(a, b) {
|
|
31
|
+
return (a.host === b.host &&
|
|
32
|
+
a.port === b.port &&
|
|
33
|
+
a.username === b.username &&
|
|
34
|
+
a.password === b.password &&
|
|
35
|
+
a.database === b.database);
|
|
36
|
+
}
|
|
37
|
+
export async function closePool() {
|
|
38
|
+
if (pool) {
|
|
39
|
+
await pool.end().catch(err => console.error('[mysql-backend] Pool close failed:', err.message));
|
|
40
|
+
pool = null;
|
|
41
|
+
currentConfig = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function getPoolStatus() {
|
|
45
|
+
return {
|
|
46
|
+
initialized: pool !== null,
|
|
47
|
+
hasConfig: currentConfig !== null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function validateMySQLConfig(config) {
|
|
51
|
+
const missing = [];
|
|
52
|
+
if (!config.username)
|
|
53
|
+
missing.push('DB_USER (or DB_USERNAME)');
|
|
54
|
+
if (!config.database)
|
|
55
|
+
missing.push('DB_DATABASE');
|
|
56
|
+
if (missing.length > 0) {
|
|
57
|
+
throw new Error(`Missing required MySQL configuration: ${missing.join(', ')}.\n` +
|
|
58
|
+
`Set these environment variables or provide in config.`);
|
|
59
|
+
}
|
|
60
|
+
if (config.port < 1 || config.port > 65535) {
|
|
61
|
+
throw new Error(`Invalid MySQL port: ${config.port}. Must be between 1 and 65535.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export class MySQLConnectionPool {
|
|
65
|
+
pool;
|
|
66
|
+
constructor(pool) {
|
|
67
|
+
this.pool = pool;
|
|
68
|
+
}
|
|
69
|
+
async getConnection() {
|
|
70
|
+
const conn = await this.pool.getConnection();
|
|
71
|
+
return {
|
|
72
|
+
isConnected: () => true,
|
|
73
|
+
close: async () => conn.release(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async end() {
|
|
77
|
+
await this.pool.end();
|
|
78
|
+
}
|
|
79
|
+
isHealthy() {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export { pool as _internalPool };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FieldPacket, RowDataPacket } from 'mysql2/promise';
|
|
2
|
+
import type { IFieldInfo, IColumnSchema, ICollectionInfo } from '../../interface';
|
|
3
|
+
export declare function extractFieldInfo(fieldPackets: FieldPacket[] | null | undefined): IFieldInfo[];
|
|
4
|
+
export declare function mapMySQLTablesToCollections(rows: RowDataPacket[]): ICollectionInfo[];
|
|
5
|
+
export declare function mapMySQLColumnsToSchema(rows: RowDataPacket[]): IColumnSchema[];
|
|
6
|
+
export declare const LIST_TABLES_SQL = "\n SELECT \n TABLE_NAME,\n TABLE_TYPE,\n ENGINE,\n TABLE_ROWS,\n CREATE_TIME,\n TABLE_COMMENT\n FROM information_schema.TABLES \n WHERE TABLE_SCHEMA = DATABASE()\n ORDER BY TABLE_NAME\n";
|
|
7
|
+
export declare const DESCRIBE_TABLE_SQL = "\n SELECT \n COLUMN_NAME as Field,\n COLUMN_TYPE as Type,\n IS_NULLABLE as `Null`,\n COLUMN_KEY as `Key`,\n COLUMN_DEFAULT as `Default`,\n EXTRA as Extra,\n COLUMN_COMMENT as Comment\n FROM information_schema.COLUMNS \n WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?\n ORDER BY ORDINAL_POSITION\n";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function extractFieldInfo(fieldPackets) {
|
|
2
|
+
if (!fieldPackets || !Array.isArray(fieldPackets)) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
return fieldPackets.map((field) => ({
|
|
6
|
+
name: field.name ?? 'unknown',
|
|
7
|
+
type: String(field.type ?? 'unknown'),
|
|
8
|
+
nullable: true,
|
|
9
|
+
key: null,
|
|
10
|
+
default: null,
|
|
11
|
+
extra: null,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
export function mapMySQLTablesToCollections(rows) {
|
|
15
|
+
return rows.map((row) => ({
|
|
16
|
+
name: row.TABLE_NAME || row.table_name || row.name || String(row[0] ?? 'unknown'),
|
|
17
|
+
type: mapTableType(row.TABLE_TYPE || row.table_type || row.type),
|
|
18
|
+
engine: row.ENGINE || row.engine || null,
|
|
19
|
+
rowCount: row.TABLE_ROWS ?? row.table_rows ?? row.rows ?? null,
|
|
20
|
+
createdAt: row.CREATE_TIME instanceof Date
|
|
21
|
+
? row.CREATE_TIME.toISOString()
|
|
22
|
+
: (row.create_time || null),
|
|
23
|
+
comment: row.TABLE_COMMENT || row.table_comment || row.comment || null,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
function mapTableType(mysqlType) {
|
|
27
|
+
const upper = (mysqlType || '').toUpperCase();
|
|
28
|
+
if (upper.includes('VIEW'))
|
|
29
|
+
return 'view';
|
|
30
|
+
return 'table';
|
|
31
|
+
}
|
|
32
|
+
export function mapMySQLColumnsToSchema(rows) {
|
|
33
|
+
return rows.map((row) => ({
|
|
34
|
+
field: row.Field || row.COLUMN_NAME || row.field || row.column_name || 'unknown',
|
|
35
|
+
type: row.Type || row.COLUMN_TYPE || row.type || row.column_type || 'unknown',
|
|
36
|
+
nullable: (row.Null || row.IS_NULLABLE || row.nullable) === 'YES',
|
|
37
|
+
key: row.Key || row.COLUMN_KEY || row.key || null,
|
|
38
|
+
defaultValue: row.Default ?? row.COLUMN_DEFAULT ?? row.default ?? null,
|
|
39
|
+
extra: row.Extra || row.EXTRA || row.extra || null,
|
|
40
|
+
comment: row.Comment || row.COLUMN_COMMENT || row.comment || null,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
export const LIST_TABLES_SQL = `
|
|
44
|
+
SELECT
|
|
45
|
+
TABLE_NAME,
|
|
46
|
+
TABLE_TYPE,
|
|
47
|
+
ENGINE,
|
|
48
|
+
TABLE_ROWS,
|
|
49
|
+
CREATE_TIME,
|
|
50
|
+
TABLE_COMMENT
|
|
51
|
+
FROM information_schema.TABLES
|
|
52
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
53
|
+
ORDER BY TABLE_NAME
|
|
54
|
+
`;
|
|
55
|
+
export const DESCRIBE_TABLE_SQL = `
|
|
56
|
+
SELECT
|
|
57
|
+
COLUMN_NAME as Field,
|
|
58
|
+
COLUMN_TYPE as Type,
|
|
59
|
+
IS_NULLABLE as \`Null\`,
|
|
60
|
+
COLUMN_KEY as \`Key\`,
|
|
61
|
+
COLUMN_DEFAULT as \`Default\`,
|
|
62
|
+
EXTRA as Extra,
|
|
63
|
+
COLUMN_COMMENT as Comment
|
|
64
|
+
FROM information_schema.COLUMNS
|
|
65
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
|
66
|
+
ORDER BY ORDINAL_POSITION
|
|
67
|
+
`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IDatabaseBackend, IConnectionConfig, IConnectionPool, IQuery, IQueryResult, ICollectionInfo, IColumnSchema, IFieldInfo, IRow } from '../interface';
|
|
2
|
+
export declare abstract class BaseBackend implements IDatabaseBackend {
|
|
3
|
+
abstract readonly type: string;
|
|
4
|
+
abstract readonly config: IConnectionConfig;
|
|
5
|
+
protected _connected: boolean;
|
|
6
|
+
abstract connect(): Promise<void>;
|
|
7
|
+
abstract disconnect(): Promise<void>;
|
|
8
|
+
abstract getPool(): IConnectionPool | null;
|
|
9
|
+
isConnected(): boolean;
|
|
10
|
+
abstract executeQuery(query: IQuery): Promise<IQueryResult>;
|
|
11
|
+
abstract listCollections(): Promise<ICollectionInfo[]>;
|
|
12
|
+
abstract describeCollection(name: string): Promise<IColumnSchema[]>;
|
|
13
|
+
protected createSuccessResult(rows: IRow[], fields: IFieldInfo[], executionTimeMs: number): IQueryResult;
|
|
14
|
+
protected createErrorResult(error: unknown, executionTimeMs: number, additionalHints?: string[]): IQueryResult;
|
|
15
|
+
protected createFieldInfo(name: string, type?: string, nullable?: boolean): IFieldInfo;
|
|
16
|
+
}
|
|
17
|
+
export { loadDbConfig as getConfigFromEnv } from './config-loader';
|