Files
DBClient/app/ui/table_structure.py
T
nngoandClaude Sonnet 4.6 b003976d39 theme: upgrade palette from Catppuccin Mocha to Frappé
Replaces all dark-Mocha colours (#1e1e2e base, #313244 surface0, etc.)
with the noticeably brighter Frappé variants (#303446 base, #414559
surface0, etc.) across the QSS and every hardcoded colour in Python
source files (syntax highlighter, completer popup, log viewer, explain
view, table viewer, schema browser, icons, etc.).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 16:51:37 -04:00

325 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()