""" QAbstractTableModel that wraps a list of plain tuples (rows) for display in a QTableView. Supports sorting and in-place data refresh. """ from PyQt6.QtCore import ( QAbstractTableModel, QModelIndex, Qt, QSortFilterProxyModel ) from PyQt6.QtGui import QColor, QFont from app.config.theme import get_palette class ResultTableModel(QAbstractTableModel): """Immutable result-set model — replaces data via set_data().""" @property def NULL_COLOR(self): return QColor(get_palette()["overlay"]) NUM_ALIGN = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter TEXT_ALIGN = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter _NUMERIC_TYPES = (int, float) def __init__(self, parent=None): super().__init__(parent) self._columns: list = [] self._rows: list = [] # ── Public API ──────────────────────────────────────────────────────────── def set_data(self, columns: list, rows: list) -> None: self.beginResetModel() self._columns = list(columns) self._rows = [tuple(r) for r in rows] self.endResetModel() def clear(self) -> None: self.set_data([], []) def get_row(self, row: int) -> tuple: return self._rows[row] def column_names(self) -> list: return list(self._columns) def export_csv(self, filepath: str, delimiter: str = ",") -> None: import csv with open(filepath, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f, delimiter=delimiter) writer.writerow(self._columns) writer.writerows(self._rows) def export_json(self, filepath: str) -> None: import json records = [dict(zip(self._columns, row)) for row in self._rows] with open(filepath, "w", encoding="utf-8") as f: json.dump(records, f, indent=2, default=str) def export_sql(self, filepath: str, table_name: str = "table") -> None: with open(filepath, "w", encoding="utf-8") as f: cols = ", ".join(self._columns) for row in self._rows: vals = ", ".join( "NULL" if v is None else f"'{str(v).replace(chr(39), chr(39)*2)}'" for v in row ) f.write(f"INSERT INTO {table_name} ({cols}) VALUES ({vals});\n") # ── QAbstractTableModel interface ───────────────────────────────────────── def rowCount(self, parent=QModelIndex()) -> int: return len(self._rows) def columnCount(self, parent=QModelIndex()) -> int: return len(self._columns) def headerData(self, section: int, orientation, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DisplayRole: if orientation == Qt.Orientation.Horizontal: return self._columns[section] if section < len(self._columns) else "" else: return str(section + 1) if role == Qt.ItemDataRole.FontRole and orientation == Qt.Orientation.Horizontal: f = QFont() f.setBold(True) return f return None def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole): if not index.isValid(): return None row, col = index.row(), index.column() if row >= len(self._rows) or col >= len(self._columns): return None value = self._rows[row][col] if role == Qt.ItemDataRole.DisplayRole: if value is None: return "NULL" return str(value) if role == Qt.ItemDataRole.ForegroundRole and value is None: return self.NULL_COLOR if role == Qt.ItemDataRole.TextAlignmentRole: if isinstance(value, self._NUMERIC_TYPES): return int(self.NUM_ALIGN) return int(self.TEXT_ALIGN) if role == Qt.ItemDataRole.UserRole: return value # raw Python value return None def flags(self, index: QModelIndex): return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable