@hyqf98/easy_db_mcp_server 1.0.0 → 2.0.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.
@@ -1,11 +1,25 @@
1
1
  import Database from 'better-sqlite3';
2
+ import fs from 'fs';
2
3
  import type {
3
4
  DatabaseAdapter,
4
5
  TableInfo,
5
6
  TableColumn,
6
7
  QueryResult,
8
+ TransactionResult,
9
+ BatchInsertResult,
10
+ BatchUpdateResult,
11
+ BatchUpdateOptions,
12
+ ExportResult,
13
+ ExportOptions,
14
+ CreateTableResult,
15
+ DropTableResult,
16
+ TableColumnDef,
17
+ TableStatsResult,
18
+ PreviewDataResult,
19
+ SampleDataResult,
7
20
  } from './base.js';
8
21
  import type { DatabaseConfig } from '../config.js';
22
+ import { expandTilde, formatFileSize, ensureDirectoryExists, getDefaultFilePath } from '../utils/file.js';
9
23
 
10
24
  export class SQLiteAdapter implements DatabaseAdapter {
11
25
  private db?: Database.Database;
@@ -90,4 +104,272 @@ export class SQLiteAdapter implements DatabaseAdapter {
90
104
  this.db = undefined;
91
105
  }
92
106
  }
107
+
108
+ async executeTransaction(queries: string[]): Promise<TransactionResult> {
109
+ if (!this.db) throw new Error('Not connected');
110
+
111
+ const results: unknown[] = [];
112
+ let totalAffectedRows = 0;
113
+
114
+ this.db.exec('BEGIN TRANSACTION');
115
+
116
+ try {
117
+ for (const query of queries) {
118
+ const trimmed = query.trim().toUpperCase();
119
+
120
+ if (trimmed.startsWith('SELECT')) {
121
+ const rows = this.db.prepare(query).all() as Record<string, unknown>[];
122
+ results.push({ rows, rowCount: rows.length });
123
+ } else {
124
+ const result = this.db.prepare(query).run();
125
+ totalAffectedRows += result.changes;
126
+ results.push({ affectedRows: result.changes });
127
+ }
128
+ }
129
+
130
+ this.db.exec('COMMIT');
131
+
132
+ return {
133
+ success: true,
134
+ affectedRows: totalAffectedRows,
135
+ results,
136
+ };
137
+ } catch (error) {
138
+ this.db.exec('ROLLBACK');
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ async batchInsert(
144
+ table: string,
145
+ data: Record<string, unknown>[]
146
+ ): Promise<BatchInsertResult> {
147
+ if (!this.db) throw new Error('Not connected');
148
+
149
+ if (data.length === 0) {
150
+ return { insertedRows: 0, duplicateRows: 0 };
151
+ }
152
+
153
+ const columns = Object.keys(data[0]);
154
+ const columnList = columns.join(', ');
155
+ const placeholders = columns.map(() => '?').join(', ');
156
+
157
+ const query = `INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`;
158
+ const stmt = this.db.prepare(query);
159
+
160
+ let insertedRows = 0;
161
+ let duplicateRows = 0;
162
+
163
+ this.db.exec('BEGIN TRANSACTION');
164
+
165
+ try {
166
+ for (const row of data) {
167
+ try {
168
+ const values = columns.map((col) => row[col]);
169
+ stmt.run(values);
170
+ insertedRows++;
171
+ } catch (error: any) {
172
+ if (error.code === 'SQLITE_CONSTRAINT') {
173
+ duplicateRows++;
174
+ } else {
175
+ throw error;
176
+ }
177
+ }
178
+ }
179
+
180
+ this.db.exec('COMMIT');
181
+
182
+ return { insertedRows, duplicateRows };
183
+ } catch (error) {
184
+ this.db.exec('ROLLBACK');
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ async batchUpdate(
190
+ table: string,
191
+ updates: BatchUpdateOptions
192
+ ): Promise<BatchUpdateResult> {
193
+ if (!this.db) throw new Error('Not connected');
194
+
195
+ const setClause = Object.entries(updates.set)
196
+ .map(([key, value]) => `${key} = ${this.escapeValue(value)}`)
197
+ .join(', ');
198
+
199
+ const query = `UPDATE ${table} SET ${setClause} WHERE ${updates.where}`;
200
+ const result = this.db.prepare(query).run();
201
+
202
+ return { affectedRows: result.changes };
203
+ }
204
+
205
+ private escapeValue(value: unknown): string {
206
+ if (value === null) return 'NULL';
207
+ if (typeof value === 'number') return String(value);
208
+ if (typeof value === 'boolean') return value ? '1' : '0';
209
+ return `'${String(value).replace(/'/g, "''")}'`;
210
+ }
211
+
212
+ async exportData(
213
+ table: string,
214
+ format: 'json' | 'csv',
215
+ filePath?: string,
216
+ options?: ExportOptions
217
+ ): Promise<ExportResult> {
218
+ if (!this.db) throw new Error('Not connected');
219
+
220
+ let query = `SELECT * FROM ${table}`;
221
+ if (options?.where) {
222
+ query += ` WHERE ${options.where}`;
223
+ }
224
+ if (options?.limit) {
225
+ query += ` LIMIT ${options.limit}`;
226
+ }
227
+
228
+ const data = this.db.prepare(query).all() as Record<string, unknown>[];
229
+
230
+ const targetPath = filePath
231
+ ? expandTilde(filePath)
232
+ : getDefaultFilePath(table, format);
233
+
234
+ ensureDirectoryExists(targetPath);
235
+
236
+ let content: string;
237
+ if (format === 'json') {
238
+ content = JSON.stringify(data, null, 2);
239
+ } else {
240
+ if (data.length === 0) {
241
+ content = '';
242
+ } else {
243
+ const headers = Object.keys(data[0]);
244
+ const csvRows = [
245
+ headers.join(','),
246
+ ...data.map((row) =>
247
+ headers.map((h) => {
248
+ const val = row[h];
249
+ if (val === null) return '';
250
+ if (typeof val === 'string') return '"' + val.replace(/"/g, '""') + '"';
251
+ return String(val);
252
+ }).join(',')
253
+ ),
254
+ ];
255
+ content = csvRows.join('\n');
256
+ }
257
+ }
258
+
259
+ fs.writeFileSync(targetPath, content, 'utf-8');
260
+ const stats = fs.statSync(targetPath);
261
+
262
+ return {
263
+ success: true,
264
+ filePath: targetPath,
265
+ rowCount: data.length,
266
+ fileSize: formatFileSize(stats.size),
267
+ };
268
+ }
269
+
270
+ async createTable(table: string, columns: TableColumnDef[]): Promise<CreateTableResult> {
271
+ if (!this.db) throw new Error('Not connected');
272
+
273
+ const columnDefs = columns.map((col) => {
274
+ let def = `${col.name} ${col.type}`;
275
+ if (col.nullable === false) def += ' NOT NULL';
276
+ if (col.primaryKey) def += ' PRIMARY KEY';
277
+ if (col.defaultValue !== undefined) {
278
+ def += ` DEFAULT ${this.escapeValue(col.defaultValue)}`;
279
+ }
280
+ return def;
281
+ });
282
+
283
+ const query = `CREATE TABLE ${table} (${columnDefs.join(', ')})`;
284
+ this.db.exec(query);
285
+
286
+ return { success: true, tableName: table };
287
+ }
288
+
289
+ async dropTable(table: string, ifExists = false): Promise<DropTableResult> {
290
+ if (!this.db) throw new Error('Not connected');
291
+
292
+ const query = ifExists
293
+ ? `DROP TABLE IF EXISTS ${table}`
294
+ : `DROP TABLE ${table}`;
295
+
296
+ this.db.exec(query);
297
+
298
+ return { success: true, tableName: table };
299
+ }
300
+
301
+ async getTableStats(table: string): Promise<TableStatsResult> {
302
+ if (!this.db) throw new Error('Not connected');
303
+
304
+ // Get row count
305
+ const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get() as any;
306
+ const rowCount = countResult.count;
307
+
308
+ // Get column count
309
+ const colResult = this.db.prepare(`PRAGMA table_info(${table})`).all();
310
+ const columns = colResult.length;
311
+
312
+ // Get indexes
313
+ const indexResult = this.db.prepare(`PRAGMA index_list(${table})`).all() as any[];
314
+ const indexes = indexResult.map((row) => row.name);
315
+
316
+ // Get table size (SQLite doesn't have a direct way, estimate from page count)
317
+ const pragmaResult = this.db.prepare(`PRAGMA page_count`).get() as any;
318
+ const pageSizeResult = this.db.prepare(`PRAGMA page_size`).get() as any;
319
+ const sizeBytes = pragmaResult.page_count * pageSizeResult.page_size;
320
+ const size = formatFileSize(sizeBytes);
321
+
322
+ return {
323
+ tableName: table,
324
+ rowCount,
325
+ columns,
326
+ indexes,
327
+ size,
328
+ };
329
+ }
330
+
331
+ async previewData(
332
+ table: string,
333
+ page = 1,
334
+ pageSize = 50,
335
+ orderBy?: string
336
+ ): Promise<PreviewDataResult> {
337
+ if (!this.db) throw new Error('Not connected');
338
+
339
+ // Get total row count
340
+ const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get() as any;
341
+ const totalRows = countResult.count;
342
+
343
+ const totalPages = Math.ceil(totalRows / pageSize);
344
+ const offset = (page - 1) * pageSize;
345
+
346
+ // Build query
347
+ let query = `SELECT * FROM ${table}`;
348
+ if (orderBy) {
349
+ query += ` ORDER BY ${orderBy}`;
350
+ }
351
+ query += ` LIMIT ${pageSize} OFFSET ${offset}`;
352
+
353
+ const rows = this.db.prepare(query).all() as Record<string, unknown>[];
354
+
355
+ return {
356
+ rows,
357
+ currentPage: page,
358
+ totalPages,
359
+ totalRows,
360
+ };
361
+ }
362
+
363
+ async sampleData(table: string, count = 10): Promise<SampleDataResult> {
364
+ if (!this.db) throw new Error('Not connected');
365
+
366
+ const rows = this.db
367
+ .prepare(`SELECT * FROM ${table} ORDER BY RANDOM() LIMIT ${count}`)
368
+ .all() as Record<string, unknown>[];
369
+
370
+ return {
371
+ rows,
372
+ sampleCount: rows.length,
373
+ };
374
+ }
93
375
  }