Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/features/extensions/extension_table_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
columns: _columns,
rows: _rows,
enabled: editingEnabled,
primaryKeys: _primaryKeys,
);
_updateStatusLine();
});
Expand Down
12 changes: 10 additions & 2 deletions lib/features/mysql/mysql_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,11 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
session.resultGridColumnDataTypes = types;
session.stagingBuffer?.dispose();
session.stagingBuffer = canSave
? DataGridStagingBuffer(columns: cols, rows: outRows)
? DataGridStagingBuffer(
columns: cols,
rows: outRows,
primaryKeys: pks,
)
: null;
if (cols.isEmpty && outRows.isEmpty) {
session.statusLine = affected != null
Expand Down Expand Up @@ -543,7 +547,11 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
setState(() {
session.rows = newRows;
session.stagingBuffer =
DataGridStagingBuffer(columns: session.columns, rows: session.rows);
DataGridStagingBuffer(
columns: session.columns,
rows: session.rows,
primaryKeys: session.resultGridPrimaryKeys,
);
session.savingChanges = false;
});
} catch (e) {
Expand Down
3 changes: 3 additions & 0 deletions lib/features/mysql/mysql_table_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class _MysqlTableViewState extends material.State<MysqlTableView> {
columns: _columnNames,
rows: _rows,
enabled: _editingEnabled,
primaryKeys: _primaryKeys,
);
}
if (mounted) setState(() {});
Expand Down Expand Up @@ -300,6 +301,7 @@ class _MysqlTableViewState extends material.State<MysqlTableView> {
columns: columns,
rows: rows,
enabled: _editingEnabled,
primaryKeys: _primaryKeys,
);
}

Expand Down Expand Up @@ -585,6 +587,7 @@ class _MysqlTableViewState extends material.State<MysqlTableView> {
columns: _columnNames,
rows: newRows,
enabled: _editingEnabled,
primaryKeys: _primaryKeys,
);
_isSaving = false;
});
Expand Down
12 changes: 10 additions & 2 deletions lib/features/postgresql/postgres_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,11 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
session.resultGridColumnDataTypes = types;
session.stagingBuffer?.dispose();
session.stagingBuffer = canSave
? DataGridStagingBuffer(columns: cols, rows: outRows)
? DataGridStagingBuffer(
columns: cols,
rows: outRows,
primaryKeys: pks,
)
: null;
if (cols.isEmpty && outRows.isEmpty) {
session.statusLine =
Expand Down Expand Up @@ -671,7 +675,11 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
setState(() {
session.rows = newRows;
session.stagingBuffer =
DataGridStagingBuffer(columns: session.columns, rows: session.rows);
DataGridStagingBuffer(
columns: session.columns,
rows: session.rows,
primaryKeys: session.resultGridPrimaryKeys,
);
session.savingChanges = false;
});
await _refreshTxStatus();
Expand Down
2 changes: 2 additions & 0 deletions lib/features/postgresql/postgres_table_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ class _PostgresTableViewState extends material.State<PostgresTableView> {
columns: columns,
rows: rows,
enabled: _editingEnabled,
primaryKeys: _primaryKeys,
);
}

Expand Down Expand Up @@ -606,6 +607,7 @@ class _PostgresTableViewState extends material.State<PostgresTableView> {
columns: _columnNames,
rows: newRows,
enabled: _editingEnabled,
primaryKeys: _primaryKeys,
);
_isSaving = false;
});
Expand Down
12 changes: 10 additions & 2 deletions lib/features/sqlite/sqlite_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,11 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
session.resultGridColumnDataTypes = types;
session.stagingBuffer?.dispose();
session.stagingBuffer = canSave
? DataGridStagingBuffer(columns: cols, rows: outRows)
? DataGridStagingBuffer(
columns: cols,
rows: outRows,
primaryKeys: pks,
)
: null;
if (cols.isEmpty && outRows.isEmpty) {
session.statusLine = 'Command completed.';
Expand Down Expand Up @@ -509,7 +513,11 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
setState(() {
session.rows = newRows;
session.stagingBuffer =
DataGridStagingBuffer(columns: session.columns, rows: session.rows);
DataGridStagingBuffer(
columns: session.columns,
rows: session.rows,
primaryKeys: session.resultGridPrimaryKeys,
);
session.savingChanges = false;
});
} catch (e) {
Expand Down
3 changes: 3 additions & 0 deletions lib/features/sqlite/sqlite_table_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class _SqliteTableViewState extends material.State<SqliteTableView> {
columns: _columnNames,
rows: _rows,
enabled: _editingEnabled,
primaryKeys: _primaryKeys,
);
}
if (mounted) setState(() {});
Expand Down Expand Up @@ -262,6 +263,7 @@ class _SqliteTableViewState extends material.State<SqliteTableView> {
columns: columns,
rows: rows,
enabled: _editingEnabled,
primaryKeys: _primaryKeys,
);
}

Expand Down Expand Up @@ -408,6 +410,7 @@ class _SqliteTableViewState extends material.State<SqliteTableView> {
columns: _columnNames,
rows: newRows,
enabled: _editingEnabled,
primaryKeys: _primaryKeys,
);
_isSaving = false;
});
Expand Down
25 changes: 25 additions & 0 deletions lib/features/workspace/data_grid_staging_buffer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ class DataGridStagingBuffer extends ChangeNotifier {
DataGridStagingBuffer({
required List<String> columns,
required List<List<String>> rows,
List<String> primaryKeys = const [],
}) : _originalColumns = List.unmodifiable(columns),
_primaryKeys = List.unmodifiable(primaryKeys),
_originalRows = List.unmodifiable(
rows.map((r) => List<String>.unmodifiable(r)).toList(),
) {
Expand All @@ -36,6 +38,10 @@ class DataGridStagingBuffer extends ChangeNotifier {
final List<String> _originalColumns;
final List<List<String>> _originalRows;

/// Primary key column names; blanked when a row is duplicated so the
/// database can generate a fresh key instead of hitting a unique violation.
final List<String> _primaryKeys;

/// Map of `rowIndex -> (colIndex -> stagedValue)` for modified cells in baseline rows.
final Map<int, Map<int, String>> _modifiedCells = {};

Expand Down Expand Up @@ -322,6 +328,25 @@ class DataGridStagingBuffer extends ChangeNotifier {
);
}

/// Stages a copy of the row at [index] (baseline or already inserted) as a
/// new row with its primary key cells blanked.
///
/// Composite or natural keys the database cannot generate stay blank for the
/// user to fill in before saving. No-op when [index] is out of range.
void duplicateRow(int index) {
if (index < 0 || index >= totalRowCount) return;
final pkNames = _primaryKeys
.map((pk) => TableMutationEngine.unquoteIdentifier(pk).toLowerCase())
.toSet();
final row = List<String>.generate(_originalColumns.length, (c) {
final name = TableMutationEngine.unquoteIdentifier(_originalColumns[c])
.toLowerCase();
if (pkNames.contains(name)) return '';
return _sanitizeNull(getCellValue(index, c));
});
addRow(row);
}

/// Returns the full list of effective rows (original with modifications applied + inserted rows).
///
/// Row indices stay aligned with the grid (rows marked for deletion are still
Expand Down
3 changes: 1 addition & 2 deletions lib/features/workspace/result_grid_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1509,8 +1509,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {

void _handleDuplicateRow(int row) {
if (widget.stagingBuffer == null || row >= _sortedRows.length) return;
final rowData = _sortedRows[row];
widget.stagingBuffer!.addRow(List<String>.from(rowData));
widget.stagingBuffer!.duplicateRow(_toModelRowIndex(row));
}

void _handleToggleDeleteRow(int row) {
Expand Down
7 changes: 6 additions & 1 deletion lib/features/workspace/table_view_staging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,15 @@ DataGridStagingBuffer? replaceTableViewStagingBuffer({
required List<String> columns,
required List<List<String>> rows,
required bool enabled,
List<String> primaryKeys = const [],
}) {
previous?.dispose();
if (!enabled || columns.isEmpty) return null;
return DataGridStagingBuffer(columns: columns, rows: rows);
return DataGridStagingBuffer(
columns: columns,
rows: rows,
primaryKeys: primaryKeys,
);
}

/// Confirms discarding dirty staged edits. Returns true when it is safe to proceed.
Expand Down
56 changes: 56 additions & 0 deletions test/features/workspace/data_grid_staging_buffer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,62 @@ void main() {
]);
});

test('duplicateRow blanks primary key cells and keeps other values', () {
final keyed = DataGridStagingBuffer(
columns: ['id', 'name', 'email'],
rows: [
['42', 'Alice', 'alice@test.com'],
],
primaryKeys: ['id'],
);
addTearDown(keyed.dispose);
keyed.setCell(0, 1, 'Alice Edited');

keyed.duplicateRow(0);

expect(keyed.insertedRowCount, 1);
expect(keyed.effectiveRows.last, ['', 'Alice Edited', 'alice@test.com']);
expect(keyed.getRowStatus(1), StagedRowStatus.inserted);
});

test(
'duplicateRow blanks every column of a composite key, case-insensitively',
() {
final keyed = DataGridStagingBuffer(
columns: ['Org', 'user_id', 'role'],
rows: [
['1', '2', 'admin'],
],
primaryKeys: ['org', '"user_id"'],
);
addTearDown(keyed.dispose);

keyed.duplicateRow(0);

expect(keyed.effectiveRows.last, ['', '', 'admin']);
});

test('duplicateRow copies staged NULLs as NULL and can copy inserted rows',
() {
buffer.setCellNull(0, 2);
buffer.duplicateRow(0);
expect(buffer.effectiveRows.last, ['1', 'Alice', 'NULL']);

buffer.duplicateRow(3); // the row inserted above
expect(buffer.insertedRowCount, 2);
});

test(
'duplicateRow without primary keys copies verbatim; bad index is a no-op',
() {
buffer.duplicateRow(1);
expect(buffer.effectiveRows.last, ['2', 'Bob', 'bob@test.com']);

buffer.duplicateRow(-1);
buffer.duplicateRow(99);
expect(buffer.insertedRowCount, 1);
});

test('committedRows equals baseline when clean', () {
expect(buffer.committedRows, buffer.originalRows);
});
Expand Down
Loading