mysqlsyncr 1.1.12 → 1.2.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.
- package/README.md +59 -0
- package/__tests__/acceptCurrent.test.js +117 -0
- package/__tests__/columnAttributes.test.js +101 -0
- package/__tests__/options.test.js +259 -0
- package/__tests__/parser.test.js +167 -166
- package/__tests__/parser2.test.js +155 -152
- package/__tests__/paths.test.js +21 -0
- package/__tests__/prompt.test.js +106 -0
- package/__tests__/sync.test.js +348 -0
- package/__tests__/templateDrift.test.js +72 -0
- package/__tests__/versions.test.js +138 -0
- package/dist/index.cjs +4059 -1733
- package/dist/index.js +4101 -1777
- package/package.json +66 -59
- package/rollup.config.js +2 -1
- package/src/DBStructureChecker.js +47 -0
- package/src/acceptCurrent.js +225 -0
- package/src/apply.js +254 -136
- package/src/cli.js +61 -34
- package/src/compare.js +106 -92
- package/src/differences.js +258 -212
- package/src/dump.js +63 -54
- package/src/getters.js +151 -139
- package/src/index.js +223 -169
- package/src/loggers.js +156 -139
- package/src/normalizers.js +28 -6
- package/src/options.js +133 -0
- package/src/parsers.js +82 -36
- package/src/paths.js +41 -0
- package/src/prompt.js +109 -0
- package/src/rebuild.js +106 -0
- package/src/rows.js +46 -0
- package/src/socketServer.js +106 -24
- package/src/templateDrift.js +93 -0
- package/src/versions.js +112 -0
- package/ui/README.md +55 -40
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-k1h7X_-h.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-B7du-70m.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CoAZ_DKt.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-D0406B4n.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-CnAg2DeQ.woff +0 -0
- package/ui/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-BG9VWE5v.woff +0 -0
- package/ui/dist/spa/assets/MainLayout-DLH1Kfg-.js +1 -0
- package/ui/dist/spa/assets/axios-2hQ_j_Tz.js +1 -0
- package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-OUIwM9U8.woff +0 -0
- package/ui/dist/spa/assets/index-Cqrg985P.js +23 -0
- package/ui/dist/spa/assets/index-pOWtXlXQ.css +1 -0
- package/ui/dist/spa/index.html +2 -2
- package/ui/package.json +43 -42
- package/ui/src/components/DefinitionDiff.vue +169 -0
- package/ui/src/pages/MainComponent.vue +704 -78
- package/ui/dist/spa/assets/KFOkCnqEu92Fr1MmgVxIIzQ-Y2mpUUkw.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmEU9fBBc--ViM7Ag4Q.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmSU5fBBc--CrFzpgl6.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmWUlfBBc--CW3_XjG_.woff +0 -0
- package/ui/dist/spa/assets/KFOlCnqEu92Fr1MmYUtfBBc--Dr8fnyGc.woff +0 -0
- package/ui/dist/spa/assets/KFOmCnqEu92Fr1Mu4mxM-DAYh6T4l.woff +0 -0
- package/ui/dist/spa/assets/MainLayout-CvQD3xKr.js +0 -1
- package/ui/dist/spa/assets/axios-C0XcclIb.js +0 -1
- package/ui/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/ui/dist/spa/assets/index-CwWo8xmP.js +0 -40
- package/ui/dist/spa/assets/index-kXpA8n8r.css +0 -5
|
@@ -54,7 +54,39 @@
|
|
|
54
54
|
>
|
|
55
55
|
{{ totalIssueCount }}
|
|
56
56
|
</q-badge>
|
|
57
|
+
<q-chip
|
|
58
|
+
v-if="versionInfo"
|
|
59
|
+
dense
|
|
60
|
+
:color="versionInfo.notes.length ? 'warning' : 'white'"
|
|
61
|
+
:text-color="versionInfo.notes.length ? 'black' : 'primary'"
|
|
62
|
+
:icon="versionInfo.notes.length ? 'warning' : 'dns'"
|
|
63
|
+
class="q-ml-md"
|
|
64
|
+
>
|
|
65
|
+
Template MySQL {{ templateVersionText }} · Server MySQL {{ versionInfo.server || 'unknown' }}
|
|
66
|
+
<q-tooltip max-width="420px">
|
|
67
|
+
<div v-if="versionInfo.template?.inferred">
|
|
68
|
+
Template version inferred: it {{ versionInfo.template.reason }}. A new dump records it exactly.
|
|
69
|
+
</div>
|
|
70
|
+
<div v-for="note in versionInfo.notes" :key="note" class="q-mt-xs">{{ note }}</div>
|
|
71
|
+
<div v-if="!versionInfo.notes.length && !versionInfo.template?.inferred">
|
|
72
|
+
The template and server are the same MySQL version.
|
|
73
|
+
</div>
|
|
74
|
+
</q-tooltip>
|
|
75
|
+
</q-chip>
|
|
57
76
|
</div>
|
|
77
|
+
<q-space />
|
|
78
|
+
<q-btn
|
|
79
|
+
v-if="ignoredCount > 0"
|
|
80
|
+
flat
|
|
81
|
+
dense
|
|
82
|
+
no-caps
|
|
83
|
+
color="white"
|
|
84
|
+
icon="visibility"
|
|
85
|
+
:label="`${ignoredCount} ignored`"
|
|
86
|
+
@click="showAllIgnored"
|
|
87
|
+
>
|
|
88
|
+
<q-tooltip>Show all ignored differences again</q-tooltip>
|
|
89
|
+
</q-btn>
|
|
58
90
|
<q-btn
|
|
59
91
|
v-if="totalIssueCount > 0"
|
|
60
92
|
flat
|
|
@@ -75,8 +107,32 @@
|
|
|
75
107
|
</div>
|
|
76
108
|
</q-card-section>
|
|
77
109
|
|
|
110
|
+
<q-card-section v-if="templateDrift.length > 0" class="q-pb-none">
|
|
111
|
+
<q-banner dense rounded class="bg-orange-1 text-black">
|
|
112
|
+
<template v-slot:avatar>
|
|
113
|
+
<q-icon name="difference" color="warning" />
|
|
114
|
+
</template>
|
|
115
|
+
{{ templateDrift.length }} table(s) in the template have a tables/<table>.sql file that differs
|
|
116
|
+
from the createSQL in the database's JSON, which is what comparing uses. Pick which version to keep.
|
|
117
|
+
<div class="q-mt-xs">
|
|
118
|
+
<q-chip
|
|
119
|
+
v-for="drift in templateDrift"
|
|
120
|
+
:key="`${drift.database}.${drift.table}`"
|
|
121
|
+
dense
|
|
122
|
+
clickable
|
|
123
|
+
icon="rule"
|
|
124
|
+
@click="reviewDrift(drift)"
|
|
125
|
+
>
|
|
126
|
+
{{ drift.database }}.{{ drift.table }}
|
|
127
|
+
<q-tooltip>Review and pick a version</q-tooltip>
|
|
128
|
+
</q-chip>
|
|
129
|
+
</div>
|
|
130
|
+
</q-banner>
|
|
131
|
+
</q-card-section>
|
|
132
|
+
|
|
78
133
|
<q-card-section>
|
|
79
134
|
<q-tree
|
|
135
|
+
ref="tree"
|
|
80
136
|
:nodes="databaseTree"
|
|
81
137
|
node-key="id"
|
|
82
138
|
v-model:expanded="expandedNodes"
|
|
@@ -110,8 +166,34 @@
|
|
|
110
166
|
>
|
|
111
167
|
{{ prop.node.diffCount }}
|
|
112
168
|
</q-badge>
|
|
169
|
+
<q-badge
|
|
170
|
+
v-if="prop.node.hiddenCount"
|
|
171
|
+
color="grey"
|
|
172
|
+
outline
|
|
173
|
+
class="q-ml-sm"
|
|
174
|
+
>
|
|
175
|
+
{{ prop.node.hiddenCount }} ignored
|
|
176
|
+
</q-badge>
|
|
113
177
|
</div>
|
|
114
178
|
|
|
179
|
+
<q-btn
|
|
180
|
+
v-if="
|
|
181
|
+
['database', 'table'].includes(prop.node.type) &&
|
|
182
|
+
(prop.node.diffCount || isNodeIgnored(prop.node))
|
|
183
|
+
"
|
|
184
|
+
flat
|
|
185
|
+
dense
|
|
186
|
+
color="grey-7"
|
|
187
|
+
:icon="isNodeIgnored(prop.node) ? 'visibility' : 'visibility_off'"
|
|
188
|
+
size="sm"
|
|
189
|
+
class="q-ml-md"
|
|
190
|
+
@click.stop="toggleIgnored(prop.node)"
|
|
191
|
+
>
|
|
192
|
+
<q-tooltip>
|
|
193
|
+
{{ isNodeIgnored(prop.node) ? 'Show these differences again' : 'Ignore these differences for this session' }}
|
|
194
|
+
</q-tooltip>
|
|
195
|
+
</q-btn>
|
|
196
|
+
|
|
115
197
|
<q-btn
|
|
116
198
|
v-if="
|
|
117
199
|
['issue-group', 'issues', 'category', 'database'].includes(
|
|
@@ -281,65 +363,105 @@
|
|
|
281
363
|
<!-- Single Issue View -->
|
|
282
364
|
<template v-if="selectedIssues.length === 1">
|
|
283
365
|
<q-card-section class="q-pt-none">
|
|
366
|
+
<q-btn
|
|
367
|
+
v-if="issueListBeforeDetails"
|
|
368
|
+
flat
|
|
369
|
+
dense
|
|
370
|
+
icon="arrow_back"
|
|
371
|
+
label="Back to list"
|
|
372
|
+
color="primary"
|
|
373
|
+
class="q-mb-sm"
|
|
374
|
+
@click="backToIssueList"
|
|
375
|
+
/>
|
|
284
376
|
<div class="text-subtitle2 q-mb-sm">
|
|
285
377
|
Issue Type:
|
|
286
378
|
<q-badge color="negative">{{
|
|
287
379
|
selectedIssues[0].type?.replace(/_/g, ' ')
|
|
288
380
|
}}</q-badge>
|
|
289
381
|
</div>
|
|
290
|
-
<div class="text-subtitle2 q-mb-sm">Name: {{ selectedIssues[0].Name }}</div>
|
|
291
|
-
|
|
292
382
|
<div v-if="selectedIssues[0].tableName" class="q-mb-sm">
|
|
293
383
|
<strong>Table:</strong> {{ selectedIssues[0].tableName }}
|
|
294
384
|
</div>
|
|
385
|
+
<div v-if="issueName(selectedIssues[0])" class="q-mb-sm">
|
|
386
|
+
<strong>{{ issueNameLabel(selectedIssues[0]) }}:</strong> {{ issueName(selectedIssues[0]) }}
|
|
387
|
+
</div>
|
|
388
|
+
<div class="q-mb-sm">{{ issueSummary(selectedIssues[0]) }}</div>
|
|
389
|
+
|
|
390
|
+
<div v-if="selectedIssues[0].type === 'mismatched_index' && selectedIssues[0].index" class="q-mb-sm">
|
|
391
|
+
<strong>Columns:</strong>
|
|
392
|
+
{{ (selectedIssues[0].index.ColumnName || []).join(', ') }}
|
|
393
|
+
<q-badge v-if="selectedIssues[0].index.Primary" color="primary" class="q-ml-sm">
|
|
394
|
+
PRIMARY
|
|
395
|
+
</q-badge>
|
|
396
|
+
<q-badge
|
|
397
|
+
v-else-if="selectedIssues[0].index.Unique"
|
|
398
|
+
color="secondary"
|
|
399
|
+
class="q-ml-sm"
|
|
400
|
+
>
|
|
401
|
+
UNIQUE
|
|
402
|
+
</q-badge>
|
|
403
|
+
</div>
|
|
295
404
|
|
|
296
|
-
<div v-if="selectedIssues[0]
|
|
297
|
-
<div class="text-subtitle2 q-mb-sm">
|
|
298
|
-
<
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
</q-card>
|
|
405
|
+
<div v-if="definitionComparison(selectedIssues[0])" class="q-mt-md">
|
|
406
|
+
<div class="text-subtitle2 q-mb-sm">Definition differences:</div>
|
|
407
|
+
<DefinitionDiff
|
|
408
|
+
:expected="definitionComparison(selectedIssues[0]).expected"
|
|
409
|
+
:current="definitionComparison(selectedIssues[0]).current"
|
|
410
|
+
/>
|
|
303
411
|
</div>
|
|
304
412
|
|
|
305
|
-
<div v-if="selectedIssues[0]
|
|
306
|
-
<div class="text-subtitle2 q-mb-sm">
|
|
413
|
+
<div v-else-if="singleDefinition(selectedIssues[0])" class="q-mt-md">
|
|
414
|
+
<div class="text-subtitle2 q-mb-sm">{{ singleDefinition(selectedIssues[0]).label }}:</div>
|
|
307
415
|
<q-card class="bg-grey-2">
|
|
308
416
|
<q-card-section class="q-pa-md">
|
|
309
|
-
<pre class="sql-code">{{ selectedIssues[0].
|
|
417
|
+
<pre class="sql-code">{{ singleDefinition(selectedIssues[0]).sql }}</pre>
|
|
310
418
|
</q-card-section>
|
|
311
419
|
</q-card>
|
|
312
420
|
</div>
|
|
313
421
|
|
|
314
|
-
<div v-if="selectedIssues[0]
|
|
315
|
-
<div class="text-subtitle2 q-mb-sm">
|
|
316
|
-
<q-markup-table flat bordered>
|
|
422
|
+
<div v-if="issueComparison(selectedIssues[0])" class="q-mt-md">
|
|
423
|
+
<div class="text-subtitle2 q-mb-sm">Comparison:</div>
|
|
424
|
+
<q-markup-table flat bordered dense>
|
|
317
425
|
<thead>
|
|
318
426
|
<tr>
|
|
319
427
|
<th class="text-left">Property</th>
|
|
320
|
-
<th class="text-left">
|
|
321
|
-
<th class="text-left">
|
|
428
|
+
<th class="text-left">Database (current)</th>
|
|
429
|
+
<th class="text-left">Template (expected)</th>
|
|
322
430
|
</tr>
|
|
323
431
|
</thead>
|
|
324
432
|
<tbody>
|
|
325
|
-
<tr v-for="
|
|
326
|
-
<td>{{
|
|
327
|
-
<td
|
|
328
|
-
|
|
329
|
-
|
|
433
|
+
<tr v-for="row in issueComparison(selectedIssues[0])" :key="row.property">
|
|
434
|
+
<td>{{ row.property }}</td>
|
|
435
|
+
<td :class="{ 'text-grey': row.current === undefined }">
|
|
436
|
+
{{ formatIssueValue(row.current, row.currentPresent) }}
|
|
437
|
+
</td>
|
|
438
|
+
<td :class="{ 'text-negative': row.differs, 'text-grey': row.expected === undefined }">
|
|
439
|
+
{{ formatIssueValue(row.expected, row.expectedPresent) }}
|
|
330
440
|
</td>
|
|
331
441
|
</tr>
|
|
332
442
|
</tbody>
|
|
333
443
|
</q-markup-table>
|
|
334
444
|
</div>
|
|
335
445
|
|
|
336
|
-
<q-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
446
|
+
<div class="row q-gutter-sm q-mt-md">
|
|
447
|
+
<!-- Act on the issue shown, not on every issue under the selected tree node -->
|
|
448
|
+
<q-btn
|
|
449
|
+
@click="fixSingleIssue()"
|
|
450
|
+
:loading="isNodeFixing['single-issue']"
|
|
451
|
+
color="primary"
|
|
452
|
+
label="Fix Issue"
|
|
453
|
+
/>
|
|
454
|
+
<q-btn
|
|
455
|
+
v-if="canKeepCurrent(selectedIssues[0])"
|
|
456
|
+
@click="keepCurrentInTemplate({ type: 'issue', id: 'single-issue', dbName: selectedIssueDbName, issueData: selectedIssues[0] })"
|
|
457
|
+
:loading="isNodeKeeping['single-issue']"
|
|
458
|
+
color="secondary"
|
|
459
|
+
outline
|
|
460
|
+
label="Keep Current"
|
|
461
|
+
>
|
|
462
|
+
<q-tooltip>Overwrite the template with the database's current value</q-tooltip>
|
|
463
|
+
</q-btn>
|
|
464
|
+
</div>
|
|
343
465
|
</q-card-section>
|
|
344
466
|
</template>
|
|
345
467
|
|
|
@@ -354,7 +476,9 @@
|
|
|
354
476
|
label: 'Type',
|
|
355
477
|
field: (row) => row.type?.replace(/_/g, ' '),
|
|
356
478
|
},
|
|
479
|
+
{ name: 'name', label: 'Name', field: (row) => issueName(row) },
|
|
357
480
|
{ name: 'tableName', label: 'Table', field: 'tableName' },
|
|
481
|
+
{ name: 'item', label: 'Item', field: (row) => issueName(row) },
|
|
358
482
|
{ name: 'actions', label: 'Actions', field: 'actions' },
|
|
359
483
|
]"
|
|
360
484
|
row-key="id"
|
|
@@ -363,14 +487,16 @@
|
|
|
363
487
|
<template v-slot:body="props">
|
|
364
488
|
<q-tr :props="props">
|
|
365
489
|
<q-td key="type">{{ props.row.type?.replace(/_/g, ' ') }}</q-td>
|
|
490
|
+
<q-td key="name">{{ issueName(props.row) }}</q-td>
|
|
366
491
|
<q-td key="tableName">{{ props.row.tableName }}</q-td>
|
|
492
|
+
<q-td key="item">{{ issueName(props.row) }}</q-td>
|
|
367
493
|
<q-td key="actions">
|
|
368
494
|
<q-btn
|
|
369
495
|
flat
|
|
370
496
|
dense
|
|
371
497
|
color="primary"
|
|
372
498
|
icon="visibility"
|
|
373
|
-
@click="showSingleIssue(props.row)"
|
|
499
|
+
@click="showSingleIssue(props.row, selectedIssueDbName)"
|
|
374
500
|
>
|
|
375
501
|
<q-tooltip>View Details</q-tooltip>
|
|
376
502
|
</q-btn>
|
|
@@ -383,13 +509,32 @@
|
|
|
383
509
|
@click="
|
|
384
510
|
fixNodeIssues({
|
|
385
511
|
type: 'issue',
|
|
386
|
-
id:
|
|
512
|
+
id: `issue-${props.rowIndex}`,
|
|
513
|
+
dbName: selectedIssueDbName,
|
|
387
514
|
issueData: props.row,
|
|
388
515
|
})
|
|
389
516
|
"
|
|
390
517
|
>
|
|
391
518
|
<q-tooltip>Fix Issue</q-tooltip>
|
|
392
519
|
</q-btn>
|
|
520
|
+
<q-btn
|
|
521
|
+
v-if="canKeepCurrent(props.row)"
|
|
522
|
+
flat
|
|
523
|
+
dense
|
|
524
|
+
color="secondary"
|
|
525
|
+
icon="save_as"
|
|
526
|
+
:loading="isNodeKeeping[`issue-${props.rowIndex}`]"
|
|
527
|
+
@click="
|
|
528
|
+
keepCurrentInTemplate({
|
|
529
|
+
type: 'issue',
|
|
530
|
+
id: `issue-${props.rowIndex}`,
|
|
531
|
+
dbName: selectedIssueDbName,
|
|
532
|
+
issueData: props.row,
|
|
533
|
+
})
|
|
534
|
+
"
|
|
535
|
+
>
|
|
536
|
+
<q-tooltip>Keep Current: overwrite the template with the database's current value</q-tooltip>
|
|
537
|
+
</q-btn>
|
|
393
538
|
</q-td>
|
|
394
539
|
</q-tr>
|
|
395
540
|
</template>
|
|
@@ -397,7 +542,7 @@
|
|
|
397
542
|
|
|
398
543
|
<q-btn
|
|
399
544
|
@click="fixNodeIssues(this.selectedNode)"
|
|
400
|
-
:loading="isNodeFixing[
|
|
545
|
+
:loading="isNodeFixing[selectedNode?.id]"
|
|
401
546
|
color="primary"
|
|
402
547
|
class="q-mt-md"
|
|
403
548
|
label="Fix All Issues"
|
|
@@ -411,6 +556,50 @@
|
|
|
411
556
|
</q-card>
|
|
412
557
|
</q-dialog>
|
|
413
558
|
|
|
559
|
+
<q-dialog v-model="driftDialog">
|
|
560
|
+
<q-card v-if="selectedDrift" style="min-width: 800px; width: 90vw; max-width: 1200px">
|
|
561
|
+
<q-card-section>
|
|
562
|
+
<div class="text-h6">{{ selectedDrift.database }}.{{ selectedDrift.table }}</div>
|
|
563
|
+
<div class="text-body2 q-mt-sm">
|
|
564
|
+
The table's createSQL in {{ selectedDrift.database }}.json (used for comparing and for
|
|
565
|
+
creating the table) differs from {{ selectedDrift.file }}. Keeping one copies it over the other;
|
|
566
|
+
notes written after the statement in the .sql file are kept either way.
|
|
567
|
+
</div>
|
|
568
|
+
</q-card-section>
|
|
569
|
+
<q-card-section class="q-pt-none">
|
|
570
|
+
<DefinitionDiff
|
|
571
|
+
:expected="selectedDrift.json"
|
|
572
|
+
:current="selectedDrift.sql"
|
|
573
|
+
:expected-label="`${selectedDrift.database}.json (used for comparing)`"
|
|
574
|
+
:current-label="selectedDrift.file"
|
|
575
|
+
/>
|
|
576
|
+
</q-card-section>
|
|
577
|
+
<q-card-actions align="right">
|
|
578
|
+
<q-btn flat label="Cancel" color="primary" v-close-popup />
|
|
579
|
+
<q-btn
|
|
580
|
+
outline
|
|
581
|
+
color="primary"
|
|
582
|
+
:label="`Use ${selectedDrift.file}`"
|
|
583
|
+
:loading="resolvingDrift === 'sql'"
|
|
584
|
+
:disable="!!resolvingDrift"
|
|
585
|
+
@click="resolveDrift('sql')"
|
|
586
|
+
>
|
|
587
|
+
<q-tooltip>Copy the .sql file's statement into the JSON, so comparing uses it</q-tooltip>
|
|
588
|
+
</q-btn>
|
|
589
|
+
<q-btn
|
|
590
|
+
unelevated
|
|
591
|
+
color="primary"
|
|
592
|
+
:label="`Use ${selectedDrift.database}.json`"
|
|
593
|
+
:loading="resolvingDrift === 'json'"
|
|
594
|
+
:disable="!!resolvingDrift"
|
|
595
|
+
@click="resolveDrift('json')"
|
|
596
|
+
>
|
|
597
|
+
<q-tooltip>Copy the JSON's createSQL into the .sql file</q-tooltip>
|
|
598
|
+
</q-btn>
|
|
599
|
+
</q-card-actions>
|
|
600
|
+
</q-card>
|
|
601
|
+
</q-dialog>
|
|
602
|
+
|
|
414
603
|
<FolderBrowser v-if="showFolderBrowser"
|
|
415
604
|
v-model="showFolderBrowser"
|
|
416
605
|
:initial-path="dbFolder"
|
|
@@ -421,11 +610,30 @@
|
|
|
421
610
|
|
|
422
611
|
<script>
|
|
423
612
|
import FolderBrowser from '../components/FolderBrowser.vue'
|
|
613
|
+
import DefinitionDiff from '../components/DefinitionDiff.vue'
|
|
614
|
+
|
|
615
|
+
// Ignored databases/tables last for the browser tab, so they survive a reload but not a new session
|
|
616
|
+
const IGNORED_KEY = 'mysqlsyncr.ignored'
|
|
617
|
+
const loadIgnored = () => {
|
|
618
|
+
try {
|
|
619
|
+
return JSON.parse(sessionStorage.getItem(IGNORED_KEY)) || {}
|
|
620
|
+
} catch {
|
|
621
|
+
return {}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const saveIgnored = (ignoredByScope) => {
|
|
625
|
+
try {
|
|
626
|
+
sessionStorage.setItem(IGNORED_KEY, JSON.stringify(ignoredByScope))
|
|
627
|
+
} catch {
|
|
628
|
+
// Storage unavailable (private window etc.): ignoring still works until the page reloads
|
|
629
|
+
}
|
|
630
|
+
}
|
|
424
631
|
|
|
425
632
|
|
|
426
633
|
export default {
|
|
427
634
|
components: {
|
|
428
|
-
FolderBrowser
|
|
635
|
+
FolderBrowser,
|
|
636
|
+
DefinitionDiff
|
|
429
637
|
},
|
|
430
638
|
data() {
|
|
431
639
|
return {
|
|
@@ -452,13 +660,39 @@ export default {
|
|
|
452
660
|
issueDialog: false,
|
|
453
661
|
selectedIssues: [],
|
|
454
662
|
selectedNode: null,
|
|
663
|
+
selectedIssueDbName: null,
|
|
455
664
|
isNodeFixing: {}, // Tracks loading state for each node
|
|
665
|
+
isNodeKeeping: {}, // Tracks loading state for Keep Current
|
|
666
|
+
issueListBeforeDetails: null, // The issue list View Details was opened from
|
|
667
|
+
rawDifferences: {}, // differences as the server sent them; this.differences leaves out ignored ones
|
|
668
|
+
// { 'host:port|folder': { databases: [db], tables: ['db.table'] } }, kept for this browser session
|
|
669
|
+
ignoredByScope: loadIgnored(),
|
|
670
|
+
versionInfo: null, // { server, template: { version, inferred, reason }, notes } from /api/versions
|
|
671
|
+
templateDrift: [], // [{ database, table, file, json, sql }]: .sql files that differ from the JSON createSQL
|
|
672
|
+
driftDialog: false,
|
|
673
|
+
selectedDrift: null,
|
|
674
|
+
resolvingDrift: null, // 'json' or 'sql' while saving
|
|
456
675
|
showFolderBrowser: false,
|
|
457
676
|
isFixingAll: false,
|
|
458
677
|
currentFixProgress: 0,
|
|
459
678
|
}
|
|
460
679
|
},
|
|
461
680
|
computed: {
|
|
681
|
+
// What's ignored for the current server and template folder
|
|
682
|
+
ignored() {
|
|
683
|
+
return this.ignoredByScope[this.ignoredScope] || { databases: [], tables: [] }
|
|
684
|
+
},
|
|
685
|
+
ignoredScope() {
|
|
686
|
+
return `${this.host}:${this.port}|${this.dbFolder}`
|
|
687
|
+
},
|
|
688
|
+
templateVersionText() {
|
|
689
|
+
const template = this.versionInfo?.template
|
|
690
|
+
if (!template) return 'unknown'
|
|
691
|
+
return template.inferred ? `${template.version} (inferred)` : template.version
|
|
692
|
+
},
|
|
693
|
+
ignoredCount() {
|
|
694
|
+
return this.ignored.databases.length + this.ignored.tables.length
|
|
695
|
+
},
|
|
462
696
|
totalIssueCount() {
|
|
463
697
|
return Object.values(this.differences)
|
|
464
698
|
.reduce((total, dbIssues) => total + dbIssues.length, 0);
|
|
@@ -499,7 +733,7 @@ export default {
|
|
|
499
733
|
database: this.database,
|
|
500
734
|
},
|
|
501
735
|
})
|
|
502
|
-
this.
|
|
736
|
+
this.setDifferences(response.data)
|
|
503
737
|
} catch (error) {
|
|
504
738
|
console.error('Error fetching differences:', error)
|
|
505
739
|
}
|
|
@@ -521,6 +755,7 @@ export default {
|
|
|
521
755
|
if (response.data && typeof response.data === 'object') {
|
|
522
756
|
this.existingDatabases = response.data
|
|
523
757
|
this.buildDatabaseTree()
|
|
758
|
+
this.fetchTemplateDrift()
|
|
524
759
|
}
|
|
525
760
|
} catch (error) {
|
|
526
761
|
console.error('Error fetching databases from existing dumps:', error)
|
|
@@ -535,13 +770,15 @@ export default {
|
|
|
535
770
|
const dbDiffs = this.differences[dbName] || []
|
|
536
771
|
const totalIssues = dbDiffs.length
|
|
537
772
|
console.log(`Database ${dbName} differences ${totalIssues}:`, dbDiffs)
|
|
773
|
+
const ignored = this.isDatabaseIgnored(dbName)
|
|
538
774
|
return {
|
|
539
775
|
id: `db-${dbName}`,
|
|
540
776
|
label: dbName,
|
|
541
777
|
type: 'database',
|
|
542
778
|
lazy: true,
|
|
543
|
-
icon: totalIssues > 0 ? 'warning' : 'check_circle',
|
|
544
|
-
iconColor: totalIssues > 0 ? 'negative' : 'positive',
|
|
779
|
+
icon: ignored ? 'visibility_off' : totalIssues > 0 ? 'warning' : 'check_circle',
|
|
780
|
+
iconColor: ignored ? 'grey' : totalIssues > 0 ? 'negative' : 'positive',
|
|
781
|
+
hiddenCount: this.hiddenCount(dbName),
|
|
545
782
|
diffCount: totalIssues || null,
|
|
546
783
|
dbData: db,
|
|
547
784
|
dbName: dbName,
|
|
@@ -560,17 +797,12 @@ export default {
|
|
|
560
797
|
const dbDiffs = this.differences[node.dbName] || []
|
|
561
798
|
|
|
562
799
|
// Count issues by main category
|
|
563
|
-
const tableIssues = dbDiffs.filter(
|
|
564
|
-
(d) =>
|
|
565
|
-
d.type.includes('table') ||
|
|
566
|
-
d.type.includes('field') ||
|
|
567
|
-
d.type.includes('index') ||
|
|
568
|
-
d.type.includes('trigger'),
|
|
569
|
-
)
|
|
800
|
+
const tableIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'tables')
|
|
570
801
|
console.log('Table issues:', tableIssues)
|
|
571
802
|
|
|
572
|
-
const procedureIssues = dbDiffs.filter((d) =>
|
|
573
|
-
const viewIssues = dbDiffs.filter((d) =>
|
|
803
|
+
const procedureIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'procedures')
|
|
804
|
+
const viewIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'views')
|
|
805
|
+
const otherIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'other')
|
|
574
806
|
|
|
575
807
|
const children = [
|
|
576
808
|
{
|
|
@@ -591,7 +823,8 @@ export default {
|
|
|
591
823
|
iconColor: procedureIssues.length > 0 ? 'negative' : 'positive',
|
|
592
824
|
diffCount: procedureIssues.length || null,
|
|
593
825
|
},
|
|
594
|
-
|
|
826
|
+
// Shown for view differences too, e.g. a view only in the database
|
|
827
|
+
(db.views || viewIssues.length > 0) && {
|
|
595
828
|
id: `${node.dbName}-views`,
|
|
596
829
|
label: 'Views',
|
|
597
830
|
type: 'category',
|
|
@@ -600,6 +833,26 @@ export default {
|
|
|
600
833
|
iconColor: viewIssues.length > 0 ? 'negative' : 'positive',
|
|
601
834
|
diffCount: viewIssues.length || null,
|
|
602
835
|
},
|
|
836
|
+
// Differences that belong to no table, procedure or view, so every counted one is shown
|
|
837
|
+
otherIssues.length > 0 && {
|
|
838
|
+
id: `${node.dbName}-other`,
|
|
839
|
+
label: 'Other',
|
|
840
|
+
type: 'category',
|
|
841
|
+
lazy: false,
|
|
842
|
+
icon: 'warning',
|
|
843
|
+
iconColor: 'negative',
|
|
844
|
+
diffCount: otherIssues.length,
|
|
845
|
+
children: otherIssues.map((issue, i) => ({
|
|
846
|
+
id: `${node.dbName}-other-${i}`,
|
|
847
|
+
label: `${issue.type.replace(/_/g, ' ')}${this.issueName(issue) ? `: ${this.issueName(issue)}` : ''}`,
|
|
848
|
+
type: 'issue',
|
|
849
|
+
icon: 'warning',
|
|
850
|
+
iconColor: 'negative',
|
|
851
|
+
expandable: false,
|
|
852
|
+
diffCount: 1,
|
|
853
|
+
issueData: issue,
|
|
854
|
+
})),
|
|
855
|
+
},
|
|
603
856
|
].filter(Boolean)
|
|
604
857
|
|
|
605
858
|
console.log('Database children:', children)
|
|
@@ -610,13 +863,7 @@ export default {
|
|
|
610
863
|
const dbDiffs = this.differences[dbName] || []
|
|
611
864
|
|
|
612
865
|
if (node.id.includes('-tables')) {
|
|
613
|
-
const tableIssues = dbDiffs.filter(
|
|
614
|
-
(d) =>
|
|
615
|
-
d.type.includes('table') ||
|
|
616
|
-
d.type.includes('field') ||
|
|
617
|
-
d.type.includes('index') ||
|
|
618
|
-
d.type.includes('trigger'),
|
|
619
|
-
)
|
|
866
|
+
const tableIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'tables')
|
|
620
867
|
console.log('Table category issues:', tableIssues)
|
|
621
868
|
|
|
622
869
|
const children = []
|
|
@@ -634,8 +881,15 @@ export default {
|
|
|
634
881
|
// })
|
|
635
882
|
// }
|
|
636
883
|
|
|
637
|
-
// Add actual tables
|
|
638
|
-
const tables = this.existingDatabases[dbName].tables
|
|
884
|
+
// Add actual tables, plus any only in the database (extra_table) so their issues show
|
|
885
|
+
const tables = { ...this.existingDatabases[dbName].tables }
|
|
886
|
+
const onlyInDatabase = new Set()
|
|
887
|
+
for (const issue of [...tableIssues, ...(this.rawDifferences[dbName] || [])]) {
|
|
888
|
+
if (issue.tableName && !tables[issue.tableName] && this.issueCategory(issue) === 'tables') {
|
|
889
|
+
tables[issue.tableName] = null
|
|
890
|
+
onlyInDatabase.add(issue.tableName)
|
|
891
|
+
}
|
|
892
|
+
}
|
|
639
893
|
console.log('Tables:', tables)
|
|
640
894
|
|
|
641
895
|
children.push(
|
|
@@ -646,13 +900,18 @@ export default {
|
|
|
646
900
|
console.log(`Issues for table ${tableName}:`, specificTableIssues)
|
|
647
901
|
const hasIssues = specificTableIssues.length > 0
|
|
648
902
|
|
|
903
|
+
const ignored = this.isTableIgnored(dbName, tableName) || this.isDatabaseIgnored(dbName)
|
|
649
904
|
return {
|
|
650
905
|
id: `${dbName}-table-${tableName}`,
|
|
651
|
-
label: tableName,
|
|
906
|
+
label: onlyInDatabase.has(tableName) ? `${tableName} (not in template)` : tableName,
|
|
652
907
|
type: 'table',
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
908
|
+
dbName,
|
|
909
|
+
tableName,
|
|
910
|
+
// A table only in the database has no template structure to expand
|
|
911
|
+
lazy: table !== null,
|
|
912
|
+
icon: ignored ? 'visibility_off' : hasIssues ? 'warning' : 'check_circle',
|
|
913
|
+
iconColor: ignored ? 'grey' : hasIssues ? 'negative' : 'positive',
|
|
914
|
+
hiddenCount: this.hiddenCount(dbName, tableName),
|
|
656
915
|
diffCount: hasIssues ? specificTableIssues.length : null,
|
|
657
916
|
tableData: table,
|
|
658
917
|
issueData: specificTableIssues,
|
|
@@ -693,16 +952,32 @@ export default {
|
|
|
693
952
|
done(children)
|
|
694
953
|
} else if (node.id.includes('-views')) {
|
|
695
954
|
const children = []
|
|
696
|
-
const views = this.existingDatabases[dbName].views
|
|
955
|
+
const views = { ...(this.existingDatabases[dbName].views || {}) }
|
|
956
|
+
const viewIssues = dbDiffs.filter((d) => this.issueCategory(d) === 'views')
|
|
957
|
+
const onlyInDatabase = new Set()
|
|
958
|
+
for (const issue of viewIssues) {
|
|
959
|
+
if (!(issue.viewName in views)) {
|
|
960
|
+
views[issue.viewName] = null
|
|
961
|
+
onlyInDatabase.add(issue.viewName)
|
|
962
|
+
}
|
|
963
|
+
}
|
|
697
964
|
console.log('Views:', views)
|
|
698
965
|
children.push(
|
|
699
|
-
...Object.entries(views).map(([name, view]) =>
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
966
|
+
...Object.entries(views).map(([name, view]) => {
|
|
967
|
+
const specificViewIssues = viewIssues.filter((d) => d.viewName === name)
|
|
968
|
+
const hasIssues = specificViewIssues.length > 0
|
|
969
|
+
return {
|
|
970
|
+
id: `${dbName}-view-${name}`,
|
|
971
|
+
label: onlyInDatabase.has(name) ? `${name} (not in template)` : name,
|
|
972
|
+
type: 'view',
|
|
973
|
+
lazy: false,
|
|
974
|
+
view: view,
|
|
975
|
+
icon: hasIssues ? 'warning' : 'check_circle',
|
|
976
|
+
iconColor: hasIssues ? 'negative' : 'positive',
|
|
977
|
+
diffCount: hasIssues ? specificViewIssues.length : null,
|
|
978
|
+
issueData: specificViewIssues,
|
|
979
|
+
}
|
|
980
|
+
}),
|
|
706
981
|
)
|
|
707
982
|
done(children)
|
|
708
983
|
}
|
|
@@ -807,8 +1082,9 @@ export default {
|
|
|
807
1082
|
output: this.dbFolder,
|
|
808
1083
|
},
|
|
809
1084
|
})
|
|
810
|
-
this.
|
|
1085
|
+
this.setDifferences(response.data)
|
|
811
1086
|
console.log('Differences in fetchAllDifferences:', this.differences)
|
|
1087
|
+
this.fetchVersions()
|
|
812
1088
|
} catch (error) {
|
|
813
1089
|
console.error('Error fetching all differences:', error)
|
|
814
1090
|
this.$q.notify({
|
|
@@ -833,7 +1109,7 @@ export default {
|
|
|
833
1109
|
database: selectedDbName,
|
|
834
1110
|
},
|
|
835
1111
|
})
|
|
836
|
-
this.
|
|
1112
|
+
this.setDifferences(response.data)
|
|
837
1113
|
} catch (error) {
|
|
838
1114
|
console.error('Error fetching differences for selected db:', error)
|
|
839
1115
|
}
|
|
@@ -920,15 +1196,42 @@ export default {
|
|
|
920
1196
|
},
|
|
921
1197
|
showIssueDetails(issue, node) {
|
|
922
1198
|
console.log('Showing issue details:', issue, "from node:", node)
|
|
1199
|
+
this.issueListBeforeDetails = null
|
|
923
1200
|
this.selectedIssues = Array.isArray(issue) ? issue : [issue]
|
|
924
1201
|
this.selectedNode = node;
|
|
1202
|
+
this.selectedIssueDbName = this.getDbNameFromNode(node)
|
|
925
1203
|
this.issueDialog = true
|
|
926
1204
|
},
|
|
927
1205
|
showSingleIssue(issue, dbName) {
|
|
1206
|
+
// Remember the list so the details view can go back to it
|
|
1207
|
+
if (this.selectedIssues.length > 1) this.issueListBeforeDetails = this.selectedIssues
|
|
928
1208
|
this.selectedIssues = [issue]
|
|
929
|
-
this.selectedIssueDbName = dbName
|
|
1209
|
+
this.selectedIssueDbName = dbName || this.getDbNameFromNode(this.selectedNode)
|
|
930
1210
|
this.issueDialog = true
|
|
931
1211
|
},
|
|
1212
|
+
// Fixes only the issue currently shown in the single-issue view, rather
|
|
1213
|
+
// than everything under the node the dialog was opened from.
|
|
1214
|
+
async fixSingleIssue() {
|
|
1215
|
+
const issue = this.selectedIssues[0]
|
|
1216
|
+
if (!issue) return
|
|
1217
|
+
if (!this.selectedIssueDbName) {
|
|
1218
|
+
// Without the database the synthetic node id below would be
|
|
1219
|
+
// parsed as one, so refuse rather than apply to the wrong db.
|
|
1220
|
+
console.error('No database known for issue', issue)
|
|
1221
|
+
this.$q.notify({
|
|
1222
|
+
type: 'negative',
|
|
1223
|
+
message: 'Could not determine which database this issue belongs to',
|
|
1224
|
+
position: 'top',
|
|
1225
|
+
})
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
await this.fixNodeIssues({
|
|
1229
|
+
type: 'issue',
|
|
1230
|
+
id: 'single-issue',
|
|
1231
|
+
dbName: this.selectedIssueDbName,
|
|
1232
|
+
issueData: issue,
|
|
1233
|
+
})
|
|
1234
|
+
},
|
|
932
1235
|
async fixNodeIssues(node) {
|
|
933
1236
|
if (!node) return
|
|
934
1237
|
try {
|
|
@@ -943,7 +1246,7 @@ export default {
|
|
|
943
1246
|
}else{
|
|
944
1247
|
issues = await this.collectIssuesForNode(node)
|
|
945
1248
|
}
|
|
946
|
-
dbName = this.getDbNameFromNode(node)
|
|
1249
|
+
dbName = node.dbName || this.getDbNameFromNode(node)
|
|
947
1250
|
|
|
948
1251
|
if (!issues.length){
|
|
949
1252
|
console.error('No issues found to fix', node);
|
|
@@ -964,15 +1267,21 @@ export default {
|
|
|
964
1267
|
})
|
|
965
1268
|
|
|
966
1269
|
if (response.data.message) {
|
|
1270
|
+
// The server reports differences it could only apply with a change
|
|
1271
|
+
// (an unsupported collation dropped, say). Those are not clean
|
|
1272
|
+
// successes, so say what happened rather than a bare count.
|
|
1273
|
+
const warnings = response.data.warnings || []
|
|
967
1274
|
this.$q.notify({
|
|
968
|
-
type: 'positive',
|
|
969
|
-
message:
|
|
1275
|
+
type: warnings.length ? 'warning' : 'positive',
|
|
1276
|
+
message: warnings.length
|
|
1277
|
+
? response.data.message
|
|
1278
|
+
: `Fixed ${issues.length} issue(s) successfully`,
|
|
970
1279
|
position: 'top',
|
|
1280
|
+
timeout: warnings.length ? 10000 : undefined,
|
|
971
1281
|
})
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
await this.
|
|
975
|
-
await this.buildDatabaseTree()
|
|
1282
|
+
this.afterIssuesResolved(issues)
|
|
1283
|
+
// Refresh the differences, keeping the tree as it was
|
|
1284
|
+
await this.refreshKeepingExpanded()
|
|
976
1285
|
}
|
|
977
1286
|
} catch (error) {
|
|
978
1287
|
console.error('Error fixing issues:', error)
|
|
@@ -981,18 +1290,335 @@ export default {
|
|
|
981
1290
|
message: error.response?.data?.message || 'Error fixing issues',
|
|
982
1291
|
position: 'top',
|
|
983
1292
|
})
|
|
1293
|
+
// The server tries every difference and saves the ones that work, so refresh
|
|
1294
|
+
// to drop those that were applied; the dialog keeps the rest
|
|
1295
|
+
if (error.response) {
|
|
1296
|
+
await this.refreshKeepingExpanded().catch((refreshError) => console.error('Error refreshing:', refreshError))
|
|
1297
|
+
}
|
|
984
1298
|
} finally {
|
|
985
1299
|
this.isNodeFixing[node.id] = false
|
|
986
1300
|
this.isNodeFixing = { ...this.isNodeFixing } // Trigger reactivity
|
|
987
1301
|
}
|
|
988
1302
|
},
|
|
1303
|
+
// Tables whose .sql file disagrees with the JSON createSQL; reads only the template
|
|
1304
|
+
async fetchTemplateDrift() {
|
|
1305
|
+
try {
|
|
1306
|
+
const response = await this.$api.post('/api/templateDrift', { config: { output: this.dbFolder } })
|
|
1307
|
+
this.templateDrift = Array.isArray(response.data) ? response.data : []
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
console.error('Error checking the template for .sql files that differ:', error)
|
|
1310
|
+
this.templateDrift = []
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
reviewDrift(drift) {
|
|
1314
|
+
this.selectedDrift = drift
|
|
1315
|
+
this.driftDialog = true
|
|
1316
|
+
},
|
|
1317
|
+
async resolveDrift(keep) {
|
|
1318
|
+
const { database, table } = this.selectedDrift
|
|
1319
|
+
this.resolvingDrift = keep
|
|
1320
|
+
try {
|
|
1321
|
+
const response = await this.$api.post('/api/resolveTemplateDrift', {
|
|
1322
|
+
config: { output: this.dbFolder },
|
|
1323
|
+
database,
|
|
1324
|
+
table,
|
|
1325
|
+
keep,
|
|
1326
|
+
})
|
|
1327
|
+
this.$q.notify({ type: 'positive', message: response.data.message, position: 'top' })
|
|
1328
|
+
this.driftDialog = false
|
|
1329
|
+
await this.fetchTemplateDrift()
|
|
1330
|
+
// Keeping the .sql file changes the JSON, which is what comparing uses
|
|
1331
|
+
if (keep === 'sql') await this.refreshKeepingExpanded({ reloadTemplate: true })
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
this.$q.notify({
|
|
1334
|
+
type: 'negative',
|
|
1335
|
+
message: error.response?.data?.message || 'Error updating the template',
|
|
1336
|
+
position: 'top',
|
|
1337
|
+
})
|
|
1338
|
+
} finally {
|
|
1339
|
+
this.resolvingDrift = null
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
// Shown next to the differences; a failure just leaves the versions out
|
|
1343
|
+
async fetchVersions() {
|
|
1344
|
+
try {
|
|
1345
|
+
const response = await this.$api.post('/api/versions', {
|
|
1346
|
+
config: {
|
|
1347
|
+
host: this.host,
|
|
1348
|
+
port: this.port,
|
|
1349
|
+
user: this.user,
|
|
1350
|
+
password: this.password,
|
|
1351
|
+
output: this.dbFolder,
|
|
1352
|
+
},
|
|
1353
|
+
})
|
|
1354
|
+
this.versionInfo = response.data
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
console.error('Error fetching MySQL versions:', error)
|
|
1357
|
+
this.versionInfo = null
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
setDifferences(raw) {
|
|
1361
|
+
this.rawDifferences = raw || {}
|
|
1362
|
+
this.differences = this.withoutIgnored(this.rawDifferences)
|
|
1363
|
+
},
|
|
1364
|
+
withoutIgnored(raw) {
|
|
1365
|
+
return Object.fromEntries(
|
|
1366
|
+
Object.entries(raw).map(([dbName, diffs]) => [
|
|
1367
|
+
dbName,
|
|
1368
|
+
this.isDatabaseIgnored(dbName)
|
|
1369
|
+
? []
|
|
1370
|
+
: (Array.isArray(diffs) ? diffs : []).filter(
|
|
1371
|
+
(diff) => !(diff.tableName && this.isTableIgnored(dbName, diff.tableName)),
|
|
1372
|
+
),
|
|
1373
|
+
]),
|
|
1374
|
+
)
|
|
1375
|
+
},
|
|
1376
|
+
isDatabaseIgnored(dbName) {
|
|
1377
|
+
return this.ignored.databases.includes(dbName)
|
|
1378
|
+
},
|
|
1379
|
+
isTableIgnored(dbName, tableName) {
|
|
1380
|
+
return this.ignored.tables.includes(`${dbName}.${tableName}`)
|
|
1381
|
+
},
|
|
1382
|
+
// How many of a database's (or one table's) differences are hidden by ignoring
|
|
1383
|
+
hiddenCount(dbName, tableName) {
|
|
1384
|
+
const diffs = Array.isArray(this.rawDifferences[dbName]) ? this.rawDifferences[dbName] : []
|
|
1385
|
+
if (tableName) {
|
|
1386
|
+
const hidden = this.isTableIgnored(dbName, tableName) || this.isDatabaseIgnored(dbName)
|
|
1387
|
+
return hidden ? diffs.filter((d) => d.tableName === tableName).length : 0
|
|
1388
|
+
}
|
|
1389
|
+
return diffs.length - (this.differences[dbName]?.length ?? 0)
|
|
1390
|
+
},
|
|
1391
|
+
isNodeIgnored(node) {
|
|
1392
|
+
if (node.type === 'database') return this.isDatabaseIgnored(node.dbName)
|
|
1393
|
+
if (node.type === 'table') return this.isTableIgnored(node.dbName, node.tableName)
|
|
1394
|
+
return false
|
|
1395
|
+
},
|
|
1396
|
+
async setIgnored(ignored) {
|
|
1397
|
+
this.ignoredByScope = { ...this.ignoredByScope, [this.ignoredScope]: ignored }
|
|
1398
|
+
saveIgnored(this.ignoredByScope)
|
|
1399
|
+
this.differences = this.withoutIgnored(this.rawDifferences)
|
|
1400
|
+
await this.rebuildTreeKeepingExpanded()
|
|
1401
|
+
},
|
|
1402
|
+
// Hide a database's or table's differences for this session, or show them again
|
|
1403
|
+
async toggleIgnored(node) {
|
|
1404
|
+
const { databases, tables } = this.ignored
|
|
1405
|
+
if (node.type === 'database') {
|
|
1406
|
+
const dbName = node.dbName
|
|
1407
|
+
await this.setIgnored({
|
|
1408
|
+
databases: databases.includes(dbName) ? databases.filter((d) => d !== dbName) : [...databases, dbName],
|
|
1409
|
+
tables,
|
|
1410
|
+
})
|
|
1411
|
+
} else if (node.type === 'table') {
|
|
1412
|
+
const key = `${node.dbName}.${node.tableName}`
|
|
1413
|
+
await this.setIgnored({
|
|
1414
|
+
databases,
|
|
1415
|
+
tables: tables.includes(key) ? tables.filter((t) => t !== key) : [...tables, key],
|
|
1416
|
+
})
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
async showAllIgnored() {
|
|
1420
|
+
await this.setIgnored({ databases: [], tables: [] })
|
|
1421
|
+
},
|
|
1422
|
+
async rebuildTreeKeepingExpanded() {
|
|
1423
|
+
const expanded = [...this.expandedNodes]
|
|
1424
|
+
await this.buildDatabaseTree()
|
|
1425
|
+
await this.restoreExpanded(expanded)
|
|
1426
|
+
},
|
|
1427
|
+
// Reload the differences (and the template if it changed) and rebuild the tree,
|
|
1428
|
+
// re-opening the nodes that were expanded
|
|
1429
|
+
async refreshKeepingExpanded({ reloadTemplate = false } = {}) {
|
|
1430
|
+
const expanded = [...this.expandedNodes]
|
|
1431
|
+
await this.fetchAllDifferences()
|
|
1432
|
+
if (reloadTemplate) await this.getDatabasesFromExistingDumps()
|
|
1433
|
+
await this.buildDatabaseTree()
|
|
1434
|
+
await this.restoreExpanded(expanded)
|
|
1435
|
+
},
|
|
1436
|
+
// Expand the given nodes again. Children of lazy nodes only exist once their
|
|
1437
|
+
// parent is expanded, so expand level by level until nothing more is found
|
|
1438
|
+
async restoreExpanded(keys) {
|
|
1439
|
+
const tree = this.$refs.tree
|
|
1440
|
+
if (!tree) return
|
|
1441
|
+
const pending = new Set(keys)
|
|
1442
|
+
while (pending.size > 0) {
|
|
1443
|
+
await this.$nextTick()
|
|
1444
|
+
const found = [...pending].filter((key) => tree.getNodeByKey(key))
|
|
1445
|
+
if (found.length === 0) break
|
|
1446
|
+
for (const key of found) {
|
|
1447
|
+
tree.setExpanded(key, true)
|
|
1448
|
+
pending.delete(key)
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
// Which part of the tree a difference is shown under: tables, procedures, views or other
|
|
1453
|
+
issueCategory(issue) {
|
|
1454
|
+
if (issue.type?.includes('procedure')) return 'procedures'
|
|
1455
|
+
if (issue.type?.includes('view')) return 'views'
|
|
1456
|
+
if (issue.tableName) return 'tables'
|
|
1457
|
+
return 'other'
|
|
1458
|
+
},
|
|
1459
|
+
// The column, index, trigger, procedure, view or row an issue is about
|
|
1460
|
+
issueName(issue) {
|
|
1461
|
+
if (!issue) return ''
|
|
1462
|
+
if (issue.field?.Field) return issue.field.Field
|
|
1463
|
+
if (issue.indexName || issue.index?.Name) return issue.indexName || issue.index.Name
|
|
1464
|
+
if (issue.trigger?.Name) return issue.trigger.Name
|
|
1465
|
+
if (issue.key) return Object.entries(issue.key).map(([k, v]) => `${k} = ${v}`).join(', ')
|
|
1466
|
+
return issue.viewName || issue.Name || issue.database || ''
|
|
1467
|
+
},
|
|
1468
|
+
issueNameLabel(issue) {
|
|
1469
|
+
const kind = issue?.type?.split('_').slice(1).join('_')
|
|
1470
|
+
return { field: 'Column', index: 'Index', trigger: 'Trigger', procedure: 'Procedure', view: 'View', row: 'Row', database: 'Database' }[kind] || 'Name'
|
|
1471
|
+
},
|
|
1472
|
+
issueSummary(issue) {
|
|
1473
|
+
if (!issue?.type) return ''
|
|
1474
|
+
const [state, ...rest] = issue.type.split('_')
|
|
1475
|
+
const kind = this.issueNameLabel(issue).toLowerCase()
|
|
1476
|
+
const thing = rest.join('_') === 'table' ? 'This table' : `The ${kind}`
|
|
1477
|
+
if (state === 'extra') return `${thing} is in the database but not in the template.`
|
|
1478
|
+
if (state === 'missing') return `${thing} is in the template but not in the database.`
|
|
1479
|
+
if (state === 'mismatched') {
|
|
1480
|
+
const what = issue.info || issue.from
|
|
1481
|
+
return `${thing} differs between the database and the template${what ? ` (${what})` : ''}.`
|
|
1482
|
+
}
|
|
1483
|
+
return ''
|
|
1484
|
+
},
|
|
1485
|
+
// [{ property, current, expected, ... }] comparing the database and template side of an issue, or null
|
|
1486
|
+
issueComparison(issue) {
|
|
1487
|
+
const sides = {
|
|
1488
|
+
extra_field: [issue?.field, undefined],
|
|
1489
|
+
missing_field: [undefined, issue?.field],
|
|
1490
|
+
mismatched_field: [issue?.currentField, issue?.field],
|
|
1491
|
+
extra_index: [issue?.index, undefined],
|
|
1492
|
+
missing_index: [undefined, issue?.index],
|
|
1493
|
+
missing_row: [undefined, issue?.row],
|
|
1494
|
+
}[issue?.type]
|
|
1495
|
+
if (!sides) return null
|
|
1496
|
+
const [current, expected] = sides
|
|
1497
|
+
const keys = [...new Set([...Object.keys(current || {}), ...Object.keys(expected || {})])]
|
|
1498
|
+
return keys
|
|
1499
|
+
.filter((key) => (current?.[key] ?? null) !== null || (expected?.[key] ?? null) !== null)
|
|
1500
|
+
.map((key) => ({
|
|
1501
|
+
property: key,
|
|
1502
|
+
current: current?.[key],
|
|
1503
|
+
expected: expected?.[key],
|
|
1504
|
+
currentPresent: !!current,
|
|
1505
|
+
expectedPresent: !!expected,
|
|
1506
|
+
differs: !!current && !!expected && JSON.stringify(current[key] ?? null) !== JSON.stringify(expected[key] ?? null),
|
|
1507
|
+
}))
|
|
1508
|
+
},
|
|
1509
|
+
// { expected, current } definitions of a mismatched procedure, view or trigger, or null
|
|
1510
|
+
definitionComparison(issue) {
|
|
1511
|
+
const pair = {
|
|
1512
|
+
mismatched_procedure: [issue?.Definition, issue?.currentDefinition],
|
|
1513
|
+
mismatched_view: [issue?.definition, issue?.current],
|
|
1514
|
+
mismatched_trigger: [issue?.trigger?.Definition, issue?.currentTrigger?.Definition],
|
|
1515
|
+
}[issue?.type]
|
|
1516
|
+
// Diffs from an older server have no current definition to compare
|
|
1517
|
+
if (!pair || pair[1] === undefined) return null
|
|
1518
|
+
return { expected: pair[0] ?? '', current: pair[1] ?? '' }
|
|
1519
|
+
},
|
|
1520
|
+
// { label, sql } for an issue that has a definition on one side only
|
|
1521
|
+
singleDefinition(issue) {
|
|
1522
|
+
if (!issue) return null
|
|
1523
|
+
const sql = issue.createSQL || issue.Definition || issue.definition || issue.trigger?.Definition
|
|
1524
|
+
if (!sql) return null
|
|
1525
|
+
const fromDatabase = issue.type?.startsWith('extra_')
|
|
1526
|
+
return { label: `Definition in the ${fromDatabase ? 'database (current)' : 'template (expected)'}`, sql }
|
|
1527
|
+
},
|
|
1528
|
+
formatIssueValue(value, present) {
|
|
1529
|
+
if (!present) return '(not present)'
|
|
1530
|
+
if (value === undefined || value === null) return '—'
|
|
1531
|
+
if (Array.isArray(value)) return value.join(', ')
|
|
1532
|
+
if (typeof value === 'boolean') return value ? 'yes' : 'no'
|
|
1533
|
+
return String(value)
|
|
1534
|
+
},
|
|
1535
|
+
// Drop resolved issues from the dialog: back to the list when the details view
|
|
1536
|
+
// is done, and close once nothing is left
|
|
1537
|
+
afterIssuesResolved(resolved) {
|
|
1538
|
+
const remaining = (list) => list?.filter((issue) => !resolved.includes(issue))
|
|
1539
|
+
this.issueListBeforeDetails = remaining(this.issueListBeforeDetails)
|
|
1540
|
+
this.selectedIssues = remaining(this.selectedIssues)
|
|
1541
|
+
if (this.selectedIssues.length === 0 && this.issueListBeforeDetails?.length) {
|
|
1542
|
+
this.backToIssueList()
|
|
1543
|
+
}
|
|
1544
|
+
if (this.selectedIssues.length === 0) this.issueDialog = false
|
|
1545
|
+
},
|
|
1546
|
+
backToIssueList() {
|
|
1547
|
+
this.selectedIssues = this.issueListBeforeDetails
|
|
1548
|
+
this.issueListBeforeDetails = null
|
|
1549
|
+
},
|
|
1550
|
+
// Every difference except these can be kept by changing the template
|
|
1551
|
+
canKeepCurrent(issue) {
|
|
1552
|
+
return !!issue && !['missing_database', 'missing_row'].includes(issue.type)
|
|
1553
|
+
},
|
|
1554
|
+
// The opposite of fixNodeIssues: change the template to match the database
|
|
1555
|
+
async keepCurrentInTemplate(node, loadingKey = node?.id) {
|
|
1556
|
+
if (!node) return
|
|
1557
|
+
const issues = node.issueData
|
|
1558
|
+
? (Array.isArray(node.issueData) ? node.issueData : [node.issueData])
|
|
1559
|
+
: await this.collectIssuesForNode(node)
|
|
1560
|
+
const keepable = issues.filter((issue) => this.canKeepCurrent(issue))
|
|
1561
|
+
if (!keepable.length) {
|
|
1562
|
+
this.$q.notify({ type: 'warning', message: 'Nothing here can be kept as the current value', position: 'top' })
|
|
1563
|
+
return
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const confirmed = await new Promise((resolve) => {
|
|
1567
|
+
this.$q
|
|
1568
|
+
.dialog({
|
|
1569
|
+
title: 'Keep Current',
|
|
1570
|
+
message: `Overwrite the template in ${this.dbFolder} with the database's current value for ${keepable.length} difference(s)? The database isn't changed.`,
|
|
1571
|
+
cancel: true,
|
|
1572
|
+
persistent: true,
|
|
1573
|
+
})
|
|
1574
|
+
.onOk(() => resolve(true))
|
|
1575
|
+
.onCancel(() => resolve(false))
|
|
1576
|
+
})
|
|
1577
|
+
if (!confirmed) return
|
|
1578
|
+
|
|
1579
|
+
this.isNodeKeeping = { ...this.isNodeKeeping, [loadingKey]: true }
|
|
1580
|
+
try {
|
|
1581
|
+
await this.$api.post('/api/acceptCurrent', {
|
|
1582
|
+
config: {
|
|
1583
|
+
host: this.host,
|
|
1584
|
+
port: this.port,
|
|
1585
|
+
user: this.user,
|
|
1586
|
+
password: this.password,
|
|
1587
|
+
output: this.dbFolder,
|
|
1588
|
+
},
|
|
1589
|
+
database: node.dbName || this.getDbNameFromNode(node),
|
|
1590
|
+
diffs: keepable,
|
|
1591
|
+
})
|
|
1592
|
+
this.$q.notify({
|
|
1593
|
+
type: 'positive',
|
|
1594
|
+
message: `Template updated for ${keepable.length} difference(s)`,
|
|
1595
|
+
position: 'top',
|
|
1596
|
+
})
|
|
1597
|
+
this.afterIssuesResolved(keepable)
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
console.error('Error keeping current values:', error)
|
|
1600
|
+
this.$q.notify({
|
|
1601
|
+
type: 'negative',
|
|
1602
|
+
message: error.response?.data?.message || 'Error updating the template',
|
|
1603
|
+
position: 'top',
|
|
1604
|
+
})
|
|
1605
|
+
} finally {
|
|
1606
|
+
this.isNodeKeeping = { ...this.isNodeKeeping, [loadingKey]: false }
|
|
1607
|
+
// Refresh even after a partial failure: the differences that succeeded were saved.
|
|
1608
|
+
// The template changed too, so reload it
|
|
1609
|
+
await this.refreshKeepingExpanded({ reloadTemplate: true })
|
|
1610
|
+
}
|
|
1611
|
+
},
|
|
989
1612
|
getDbNameFromNode(node) {
|
|
990
1613
|
// Extract database name from node ID (assumes format: "dbName-something-etc")
|
|
991
1614
|
if(!node || !node.id) node = this.selectedNode
|
|
1615
|
+
if (!node || !node.id) {
|
|
1616
|
+
console.error('Node ID is required to get db name', node)
|
|
1617
|
+
return null
|
|
1618
|
+
}
|
|
992
1619
|
if(node.id.startsWith('db-')){
|
|
993
1620
|
return node.id.split('-')[1]
|
|
994
1621
|
}
|
|
995
|
-
if (!node.id) return null, console.error('Node ID is required to get db name', node);
|
|
996
1622
|
console.log('Node ID:', node.id)
|
|
997
1623
|
return node.id.split('-')[0]
|
|
998
1624
|
},
|