FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Merge pull request #935 from QueryaHub/issue/869-extension-table-fail… · QueryaHub/Querya-Desktop@134a007 · GitHub

Commit 134a007

Browse files
Merge pull request #935 from QueryaHub/issue/869-extension-table-fail-open
fix(extensions): require primary key and guard dirty edits in ExtensionTableView
2 parents 2d94a24 + 3209224 commit 134a007

3 files changed

Lines changed: 313 additions & 71 deletions

File tree

‎lib/core/extensions/extension_driver_session.dart‎

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -445,21 +445,18 @@ class ExtensionDriverSession {
445445
required String tableName,
446446
}) async {
447447
final bridge = await ensureConnected(row);
448-
try {
449-
final result = await bridge.sendRequest('db.getTableSchema', {
450-
'connectionId': row.id,
451-
'database': database,
452-
if (schema != null && schema.isNotEmpty) 'schema': schema,
453-
'tableName': tableName,
454-
});
455-
if (result is Map) {
456-
return TableSchemaMeta.fromJson(Map<String, dynamic>.from(result));
457-
}
458-
return TableSchemaMeta(tableName: tableName, schema: schema);
459-
} catch (e) {
460-
debugPrint('ExtensionDriverSession getTableSchema fallback ($e)');
461-
return TableSchemaMeta(tableName: tableName, schema: schema);
448+
final result = await bridge.sendRequest('db.getTableSchema', {
449+
'connectionId': row.id,
450+
'database': database,
451+
if (schema != null && schema.isNotEmpty) 'schema': schema,
452+
'tableName': tableName,
453+
});
454+
if (result is Map) {
455+
return TableSchemaMeta.fromJson(Map<String, dynamic>.from(result));
462456
}
457+
throw StateError(
458+
'Driver db.getTableSchema returned unexpected payload: $result',
459+
);
463460
}
464461

465462
/// Executes batch data mutations (insert, update, delete) via `db.mutate`.

‎lib/features/extensions/extension_table_view.dart‎

Lines changed: 167 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
5353

5454
ExtensionDriverCapabilities? _capabilities;
5555
DataGridStagingBuffer? _stagingBuffer;
56+
bool _schemaLoaded = false;
57+
Object? _schemaError;
58+
List<String> _primaryKeys = const [];
5659
bool _isSaving = false;
5760

5861
bool _restartingDriver = false;
@@ -80,6 +83,12 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
8083

8184
Future<void> _restartDriver() async {
8285
if (_restartingDriver) return;
86+
if (!await _confirmDiscardIfNeeded()) return;
87+
if (!mounted) return;
88+
_stagingBuffer?.dispose();
89+
_stagingBuffer = null;
90+
_schemaLoaded = false;
91+
_schemaError = null;
8392
setState(() {
8493
_restartingDriver = true;
8594
});
@@ -122,13 +131,19 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
122131
if (oldWidget.connectionRow.id != widget.connectionRow.id ||
123132
oldWidget.database != widget.database ||
124133
oldWidget.tableName != widget.tableName) {
125-
_offset = 0;
126-
_totalRows = null;
127-
_filterController.clear();
128-
_filterActive = false;
129-
_stagingBuffer?.dispose();
130-
_stagingBuffer = null;
131-
unawaited(_loadPage(refreshCount: true));
134+
unawaited(() async {
135+
if (!await _confirmDiscardIfNeeded()) return;
136+
if (!mounted) return;
137+
_stagingBuffer?.dispose();
138+
_stagingBuffer = null;
139+
_schemaLoaded = false;
140+
_schemaError = null;
141+
_offset = 0;
142+
_totalRows = null;
143+
_filterController.clear();
144+
_filterActive = false;
145+
await _loadPage(refreshCount: true);
146+
}());
132147
}
133148
}
134149

@@ -140,16 +155,89 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
140155
super.dispose();
141156
}
142157

158+
String get _tableTitle => '${widget.database}.${widget.tableName}';
159+
160+
bool get _isDirty => _stagingBuffer?.isDirty == true;
161+
162+
Future<bool> _confirmDiscardIfNeeded() {
163+
return confirmDiscardTableEditsIfDirty(
164+
context: context,
165+
buffer: _stagingBuffer,
166+
tableTitle: _tableTitle,
167+
);
168+
}
169+
170+
/// Guards navigation with a discard confirmation and then runs [action]
171+
/// before triggering a page load.
172+
Future<void> _navigateAndLoad(void Function() action) async {
173+
if (!await _confirmDiscardIfNeeded()) return;
174+
if (!mounted) return;
175+
_stagingBuffer?.dispose();
176+
_stagingBuffer = null;
177+
action();
178+
unawaited(_loadPage(refreshCount: true));
179+
}
180+
181+
Future<void> _onRefresh() async {
182+
if (!await _confirmDiscardIfNeeded()) return;
183+
if (!mounted) return;
184+
_stagingBuffer?.dispose();
185+
_stagingBuffer = null;
186+
_schemaLoaded = false;
187+
_schemaError = null;
188+
await _loadPage(refreshCount: true);
189+
}
190+
191+
Future<void> _ensureSchema() async {
192+
if (_schemaLoaded) return;
193+
if (widget.isView || _capabilities?.supportsMutations != true) {
194+
_schemaLoaded = true;
195+
_schemaError = null;
196+
_primaryKeys = const [];
197+
return;
198+
}
199+
final loaded = await loadTableViewSchema(
200+
() => ExtensionDriverSession.instance.getTableSchema(
201+
widget.connectionRow,
202+
database: widget.database,
203+
tableName: widget.tableName,
204+
),
205+
);
206+
final schema = loaded.schema;
207+
if (schema != null) {
208+
_primaryKeys = List<String>.from(schema.primaryKeys);
209+
_schemaError = null;
210+
} else {
211+
_primaryKeys = const [];
212+
_schemaError = loaded.error;
213+
}
214+
_schemaLoaded = true;
215+
}
216+
143217
void _updateStatusLine() {
144218
final total = _totalRows;
145219
final shownFrom = _rows.isEmpty ? 0 : _offset + 1;
146220
final shownTo = _offset + _rows.length;
221+
final String pag;
147222
if (total == null) {
148-
_statusLine = _loading
223+
pag = _loading
149224
? 'Loading data...'
150225
: 'Showing $shownTo row(s) (Calculating count...).';
151226
} else {
152-
_statusLine = 'Rows $shownFrom–$shownTo of $total.';
227+
pag = 'Rows $shownFrom–$shownTo of $total.';
228+
}
229+
final reason = tableViewEditDisabledReason(
230+
isView: widget.isView,
231+
customSqlActive: false,
232+
hasPrimaryKey: _primaryKeys.isNotEmpty,
233+
schemaLoaded: _schemaLoaded,
234+
readOnly: _capabilities != null && !_capabilities!.supportsMutations,
235+
schemaError: _schemaError,
236+
);
237+
if (reason != null) {
238+
_statusLine = '$pag · $reason';
239+
} else {
240+
_statusLine = pag;
153241
}
154242
}
155243

@@ -208,18 +296,26 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
208296
'SELECT * FROM $_qualifiedName$_whereClause LIMIT ${widget.pageSize} OFFSET $_offset',
209297
);
210298

299+
await _ensureSchema();
300+
211301
if (!mounted) return;
212302
setState(() {
213303
_columns = dataResult.columns;
214304
_rows = dataResult.rows;
215305
_loading = false;
216-
_stagingBuffer?.dispose();
217-
if (!widget.isView && (_capabilities?.supportsMutations == true)) {
218-
_stagingBuffer =
219-
DataGridStagingBuffer(columns: _columns, rows: _rows);
220-
} else {
221-
_stagingBuffer = null;
222-
}
306+
final editingEnabled = tableViewEditingEnabled(
307+
isView: widget.isView,
308+
customSqlActive: false,
309+
hasPrimaryKey: _primaryKeys.isNotEmpty,
310+
readOnly: _capabilities?.supportsMutations != true,
311+
schemaError: _schemaError,
312+
);
313+
_stagingBuffer = replaceTableViewStagingBuffer(
314+
previous: _stagingBuffer,
315+
columns: _columns,
316+
rows: _rows,
317+
enabled: editingEnabled,
318+
);
223319
_updateStatusLine();
224320
});
225321

@@ -238,14 +334,22 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
238334
final buffer = _stagingBuffer;
239335
if (buffer == null || !buffer.isDirty) return;
240336

337+
final primaryKeys = _primaryKeys;
338+
if (primaryKeys.isEmpty) {
339+
if (mounted) {
340+
showAppToast(
341+
context: context,
342+
message:
343+
'Cannot save: no primary key is available for ${widget.tableName}. '
344+
'Edits would match all rows.',
345+
variant: AppToastVariant.error,
346+
);
347+
}
348+
return;
349+
}
350+
241351
setState(() => _isSaving = true);
242352
try {
243-
final schema = await ExtensionDriverSession.instance.getTableSchema(
244-
widget.connectionRow,
245-
database: widget.database,
246-
tableName: widget.tableName,
247-
);
248-
249353
final mutations = <Map<String, dynamic>>[];
250354

251355
// 1. Updates
@@ -255,16 +359,10 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
255359
final origRow = buffer.originalRows[rowIndex];
256360

257361
final whereMap = <String, dynamic>{};
258-
if (schema.primaryKeys.isNotEmpty) {
259-
for (final pk in schema.primaryKeys) {
260-
final idx = _columns.indexOf(pk);
261-
if (idx != -1 && idx < origRow.length) {
262-
whereMap[pk] = origRow[idx];
263-
}
264-
}
265-
} else {
266-
for (var c = 0; c < _columns.length; c++) {
267-
whereMap[_columns[c]] = c < origRow.length ? origRow[c] : null;
362+
for (final pk in primaryKeys) {
363+
final idx = _columns.indexOf(pk);
364+
if (idx != -1 && idx < origRow.length) {
365+
whereMap[pk] = origRow[idx];
268366
}
269367
}
270368

@@ -304,16 +402,10 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
304402
for (final rowIndex in buffer.deletedRowIndices) {
305403
final origRow = buffer.originalRows[rowIndex];
306404
final whereMap = <String, dynamic>{};
307-
if (schema.primaryKeys.isNotEmpty) {
308-
for (final pk in schema.primaryKeys) {
309-
final idx = _columns.indexOf(pk);
310-
if (idx != -1 && idx < origRow.length) {
311-
whereMap[pk] = origRow[idx];
312-
}
313-
}
314-
} else {
315-
for (var c = 0; c < _columns.length; c++) {
316-
whereMap[_columns[c]] = c < origRow.length ? origRow[c] : null;
405+
for (final pk in primaryKeys) {
406+
final idx = _columns.indexOf(pk);
407+
if (idx != -1 && idx < origRow.length) {
408+
whereMap[pk] = origRow[idx];
317409
}
318410
}
319411
mutations.add({
@@ -330,10 +422,17 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
330422
mutations: mutations,
331423
);
332424
if (!mounted) return;
333-
final count = res['affectedRows'] ?? mutations.length;
425+
final affectedRows = res['affectedRows'];
426+
if (affectedRows is! int) {
427+
throw StateError(
428+
'Save failed: driver did not return an affectedRows count.',
429+
);
430+
}
431+
expectDmlMatchedRows(affectedRows);
432+
334433
showAppToast(
335434
context: context,
336-
message: 'Successfully applied $count mutation(s).',
435+
message: 'Successfully applied $affectedRows mutation(s).',
337436
variant: AppToastVariant.success,
338437
);
339438
unawaited(_loadPage(refreshCount: true));
@@ -351,16 +450,18 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
351450
}
352451

353452
void _applyFilter() {
354-
_offset = 0;
355-
_totalRows = null;
356-
unawaited(_loadPage(refreshCount: true));
453+
unawaited(_navigateAndLoad(() {
454+
_offset = 0;
455+
_totalRows = null;
456+
}));
357457
}
358458

359459
void _clearFilter() {
360460
_filterController.clear();
361-
_offset = 0;
362-
_totalRows = null;
363-
unawaited(_loadPage(refreshCount: true));
461+
unawaited(_navigateAndLoad(() {
462+
_offset = 0;
463+
_totalRows = null;
464+
}));
364465
}
365466

366467
Future<void> _openDdlDialog() async {
@@ -437,24 +538,27 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
437538
}
438539
}
439540

440-
bool get _canGoBack => _offset > 0;
541+
bool get _canGoBack => _offset > 0 && !_isDirty;
441542

442543
bool get _canGoForward {
544+
if (_isDirty) return false;
443545
final total = _totalRows;
444546
if (total == null) return _rows.length >= widget.pageSize;
445547
return _offset + widget.pageSize < total;
446548
}
447549

448550
void _previousPage() {
449551
if (!_canGoBack || _loading) return;
450-
_offset = (_offset - widget.pageSize).clamp(0, 1 << 30);
451-
unawaited(_loadPage());
552+
unawaited(_navigateAndLoad(() {
553+
_offset = (_offset - widget.pageSize).clamp(0, 1 << 30);
554+
}));
452555
}
453556

454557
void _nextPage() {
455558
if (!_canGoForward || _loading) return;
456-
_offset += widget.pageSize;
457-
unawaited(_loadPage());
559+
unawaited(_navigateAndLoad(() {
560+
_offset += widget.pageSize;
561+
}));
458562
}
459563

460564
@override
@@ -473,7 +577,13 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
473577
loading: _loading,
474578
canGoPrevious: _canGoBack && !_loading,
475579
canGoNext: _canGoForward && !_loading,
476-
onNavigateHome: widget.onNavigateHome,
580+
onNavigateHome: widget.onNavigateHome != null
581+
? () => unawaited(() async {
582+
if (!await _confirmDiscardIfNeeded()) return;
583+
if (!mounted) return;
584+
widget.onNavigateHome!();
585+
}())
586+
: null,
477587
filterActive: _filterActive || _filterController.text.isNotEmpty,
478588
filterText: _filterController.text,
479589
onToggleFilter: () {
@@ -484,7 +594,7 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
484594
onOpenDdl: _openDdlDialog,
485595
onGoPrevious: _previousPage,
486596
onGoNext: _nextPage,
487-
onRefresh: () => _loadPage(refreshCount: true),
597+
onRefresh: () => unawaited(_onRefresh()),
488598
onRestartDriver: () => unawaited(_restartDriver()),
489599
isRestarting: _restartingDriver,
490600
onCopyFormat: (format) {

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL