""" Table structure viewer — shows columns, indexes, foreign keys, and DDL. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, QPlainTextEdit, QLabel, QPushButton, QHeaderView, QMessageBox, ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QColor, QFont from app.ui.syntax_highlighter import SQLHighlighter from app.ui.column_dialog import ColumnDialog from app.utils.worker import SchemaWorker class TableStructureView(QWidget): status_message = pyqtSignal(str) def __init__(self, driver, database: str, table: str, parent=None): super().__init__(parent) self._driver = driver self._database = database self._table = table self._build_ui() self._load_all() def _build_ui(self): root = QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) # Header hdr = QHBoxLayout() hdr.setContentsMargins(8, 6, 8, 4) title = QLabel(f"🏗️ {self._table}") title.setObjectName("structureTitle") f = title.font() f.setBold(True) f.setPointSize(12) title.setFont(f) refresh_btn = QPushButton("🔄 Refresh") refresh_btn.setFixedWidth(90) refresh_btn.clicked.connect(self._load_all) hdr.addWidget(title) hdr.addStretch() hdr.addWidget(refresh_btn) root.addLayout(hdr) tabs = QTabWidget() tabs.addTab(self._build_columns_tab(), "Columns") tabs.addTab(self._build_indexes_tab(), "Indexes") tabs.addTab(self._build_fks_tab(), "Foreign Keys") tabs.addTab(self._build_ddl_tab(), "DDL") root.addWidget(tabs) # ── Tab builders ────────────────────────────────────────────────────────── def _build_columns_tab(self) -> QWidget: w = QWidget() lay = QVBoxLayout(w) lay.setContentsMargins(0, 4, 0, 0) # Designer toolbar toolbar = QHBoxLayout() toolbar.setContentsMargins(4, 0, 4, 4) self._add_col_btn = QPushButton("+ Add Column") self._add_col_btn.clicked.connect(self._add_column) self._rename_col_btn = QPushButton("✏️ Rename") self._rename_col_btn.setEnabled(False) self._rename_col_btn.clicked.connect(self._rename_column) self._drop_col_btn = QPushButton("🗑 Drop Column") self._drop_col_btn.setObjectName("deleteBtn") self._drop_col_btn.setEnabled(False) self._drop_col_btn.clicked.connect(self._drop_column) toolbar.addWidget(self._add_col_btn) toolbar.addWidget(self._rename_col_btn) toolbar.addWidget(self._drop_col_btn) toolbar.addStretch() lay.addLayout(toolbar) self._col_table = self._make_table([ "Column", "Type", "Nullable", "Default", "PK", "FK", "Extra" ]) self._col_table.itemSelectionChanged.connect(self._on_col_selection) lay.addWidget(self._col_table) return w def _build_indexes_tab(self) -> QWidget: w = QWidget() lay = QVBoxLayout(w) lay.setContentsMargins(0, 0, 0, 0) self._idx_table = self._make_table([ "Name", "Columns", "Unique", "Type" ]) lay.addWidget(self._idx_table) return w def _build_fks_tab(self) -> QWidget: w = QWidget() lay = QVBoxLayout(w) lay.setContentsMargins(0, 0, 0, 0) self._fk_table = self._make_table([ "Name", "Column", "References", "On Update", "On Delete" ]) lay.addWidget(self._fk_table) return w def _build_ddl_tab(self) -> QWidget: w = QWidget() lay = QVBoxLayout(w) lay.setContentsMargins(0, 0, 0, 0) self._ddl_view = QPlainTextEdit() self._ddl_view.setReadOnly(True) self._ddl_view.setFont(QFont("Consolas", 12)) SQLHighlighter(self._ddl_view.document()) lay.addWidget(self._ddl_view) return w @staticmethod def _make_table(headers: list) -> QTableWidget: t = QTableWidget(0, len(headers)) t.setHorizontalHeaderLabels(headers) t.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) t.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) t.verticalHeader().setDefaultSectionSize(24) t.setAlternatingRowColors(True) t.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) t.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) return t # ── Data loading ────────────────────────────────────────────────────────── def _load_all(self): self._load_columns() self._load_indexes() self._load_fks() self._load_ddl() def _load_columns(self): w = SchemaWorker(self._driver.get_columns, self._database, self._table, parent=self) w.result.connect(self._populate_columns) w.error.connect(lambda e: self.status_message.emit(f"Error: {e}")) w.start() def _populate_columns(self, cols: list): t = self._col_table t.setRowCount(0) for col in cols: r = t.rowCount() t.insertRow(r) items = [ col.name, col.data_type, "YES" if col.nullable else "NO", col.default or "", "✔" if col.is_primary_key else "", "✔" if col.is_foreign_key else "", col.extra, ] for c, val in enumerate(items): item = QTableWidgetItem(str(val)) item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) if col.is_primary_key and c == 0: item.setForeground(QColor("#e5c890")) elif col.is_foreign_key and c == 0: item.setForeground(QColor("#99d1db")) t.setItem(r, c, item) def _load_indexes(self): w = SchemaWorker(self._driver.get_indexes, self._database, self._table, parent=self) w.result.connect(self._populate_indexes) w.error.connect(lambda e: self.status_message.emit(f"Error: {e}")) w.start() def _populate_indexes(self, indexes: list): t = self._idx_table t.setRowCount(0) for idx in indexes: r = t.rowCount() t.insertRow(r) vals = [ idx.name, ", ".join(idx.columns), "✔" if idx.is_unique else "", idx.index_type, ] for c, val in enumerate(vals): item = QTableWidgetItem(str(val)) item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) t.setItem(r, c, item) def _load_fks(self): w = SchemaWorker(self._driver.get_foreign_keys, self._database, self._table, parent=self) w.result.connect(self._populate_fks) w.error.connect(lambda e: self.status_message.emit(f"Error: {e}")) w.start() def _populate_fks(self, fks: list): t = self._fk_table t.setRowCount(0) for fk in fks: r = t.rowCount() t.insertRow(r) vals = [ fk.name, fk.column, f"{fk.ref_table}.{fk.ref_column}", fk.on_update, fk.on_delete, ] for c, val in enumerate(vals): item = QTableWidgetItem(str(val)) item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) t.setItem(r, c, item) def _load_ddl(self): w = SchemaWorker(self._driver.get_table_ddl, self._database, self._table, parent=self) w.result.connect(self._ddl_view.setPlainText) w.error.connect(lambda e: self._ddl_view.setPlainText(f"Error: {e}")) w.start() # ── Table designer ──────────────────────────────────────────────────────── def _on_col_selection(self): has_sel = bool(self._col_table.selectedItems()) self._rename_col_btn.setEnabled(has_sel) self._drop_col_btn.setEnabled(has_sel) def _selected_col_name(self) -> str: row = self._col_table.currentRow() if row < 0: return "" item = self._col_table.item(row, 0) return item.text() if item else "" def _add_column(self): dlg = ColumnDialog(mode="add", db_type=getattr(self._driver, "db_type", ""), parent=self) if not dlg.exec(): return w = SchemaWorker( self._driver.add_column, self._database, self._table, dlg.column_name, dlg.column_type, dlg.nullable, dlg.default_value or None, parent=self, ) w.result.connect(lambda _: ( self.status_message.emit( f"Column '{dlg.column_name}' added to {self._table}." ), self._load_all(), )) w.error.connect(lambda e: QMessageBox.critical( self, "Add Column Error", f"Could not add column:\n{e}" )) w.start() def _drop_column(self): col = self._selected_col_name() if not col: return btn = QMessageBox.warning( self, "Drop Column", f"Drop column '{col}' from '{self._table}'?\n\nThis cannot be undone.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if btn != QMessageBox.StandardButton.Yes: return w = SchemaWorker( self._driver.drop_column, self._database, self._table, col, parent=self, ) w.result.connect(lambda _: ( self.status_message.emit(f"Column '{col}' dropped."), self._load_all(), )) w.error.connect(lambda e: QMessageBox.critical( self, "Drop Column Error", f"Could not drop column:\n{e}" )) w.start() def _rename_column(self): old_name = self._selected_col_name() if not old_name: return dlg = ColumnDialog(mode="rename", column_name=old_name, parent=self) if not dlg.exec(): return new_name = dlg.column_name if new_name == old_name: return w = SchemaWorker( self._driver.rename_column, self._database, self._table, old_name, new_name, parent=self, ) w.result.connect(lambda _: ( self.status_message.emit( f"Column '{old_name}' renamed to '{new_name}'." ), self._load_all(), )) w.error.connect(lambda e: QMessageBox.critical( self, "Rename Column Error", f"Could not rename column:\n{e}" )) w.start()