From fa60499078dc4bf3dabf16b77850b2bacd78a0f2 Mon Sep 17 00:00:00 2001 From: rainerosion Date: Thu, 31 Aug 2023 00:51:35 +0800 Subject: [PATCH] init. --- .gitignore | 1 + dm.py | 59 +++++++ main.py | 470 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 530 insertions(+) create mode 100644 .gitignore create mode 100644 dm.py create mode 100644 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f21b54 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/venv/ diff --git a/dm.py b/dm.py new file mode 100644 index 0000000..34b1f4b --- /dev/null +++ b/dm.py @@ -0,0 +1,59 @@ +import sys + +from PySide6 import QtWidgets +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, \ + QHBoxLayout, QSplitter, QTextEdit + + +class TableWidgetExample(QMainWindow): + def __init__(self): + super().__init__() + + self.initUI() + + def initUI(self): + self.setWindowTitle("QTableWidget Example") + self.setGeometry(100, 100, 800, 600) + + top_layout = QVBoxLayout() + central_widget = QWidget(self) + central_widget.setLayout(top_layout) + self.setCentralWidget(central_widget) + + table = QTableWidget(self) + table.setColumnCount(3) + + table.setHorizontalHeaderLabels(["URL", "状态", "操作"]) + header = table.horizontalHeader() + + header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) + header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) + + data = [ + ["https://baidu.com", "队列中", "None"], + ["https://google.com", "队列中", "None"], + ["https://rainss.cn", "队列中", "None"] + ] + + table.setRowCount(len(data)) + + for row, rowData in enumerate(data): + for col, value in enumerate(rowData): + item = QTableWidgetItem(value) + table.setItem(row, col, item) + + top_layout.addWidget(table) + + # 下部分布局 + bottom_widget = QWidget() + bottom_layout = QVBoxLayout(bottom_widget) + input_text = QTextEdit() + bottom_layout.addWidget(input_text) + # central_widget.addWidget(bottom_widget) + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = TableWidgetExample() + window.show() + sys.exit(app.exec()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..af0934a --- /dev/null +++ b/main.py @@ -0,0 +1,470 @@ +import sys +import csv +import re +import sqlite3 +import pymysql +import psycopg2 +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QTextEdit, QPushButton, QMessageBox, \ + QLineEdit, QHBoxLayout, QTableWidget, QTableWidgetItem, QMenu, QFileDialog, QInputDialog, QComboBox, QSplitter, \ + QPlainTextEdit +from PySide6.QtGui import QAction, QKeySequence, QPainter, QSyntaxHighlighter, QTextCharFormat, QColor, QFont + + +class SQLTextEdit(QTextEdit): + def __init__(self, parent=None): + super().__init__(parent) + self.highlighter = SQLSyntaxHighlighter(self.document()) + + def paintEvent(self, event): + # 绘制文本框背景 + super().paintEvent(event) + + +class SQLSyntaxHighlighter(QSyntaxHighlighter): + def __init__(self, parent=None): + super().__init__(parent) + self.highlighting_rules = [] + + keyword_format = QTextCharFormat() + keyword_format.setForeground(Qt.darkBlue) + keyword_format.setFontWeight(QFont.Bold) + keywords = [ + "SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES", + "UPDATE", "SET", "DELETE", "CREATE", "TABLE", "DROP", + "DATABASE", "ALTER", "ADD", "PRIMARY", "KEY", "FOREIGN", + "REFERENCES", "INDEX", "IF", "NOT", "NULL", "AND", "OR", + "AS", "LIKE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", + "ASC", "DESC", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", + "OFFSET", "UNION", "ALL", "DISTINCT", "CASE", "WHEN", + "THEN", "ELSE", "END" + ] + for keyword in keywords: + pattern = r"\b" + re.escape(keyword) + r"\b" + rule = (re.compile(pattern, re.IGNORECASE), keyword_format) + self.highlighting_rules.append(rule) + + quotation_format = QTextCharFormat() + quotation_format.setForeground(Qt.darkGreen) + self.highlighting_rules.append((re.compile(r"'[^']*'"), quotation_format)) + self.highlighting_rules.append((re.compile(r'"[^"]*"'), quotation_format)) + + comment_format = QTextCharFormat() + comment_format.setForeground(Qt.darkGray) + self.highlighting_rules.append((re.compile(r"--[^\n]*"), comment_format)) + + def highlightBlock(self, text): + for rule in self.highlighting_rules: + expression, char_format = rule + + matches = expression.finditer(text) + for match in matches: + start, end = match.span() + self.setFormat(start, end - start, char_format) + + +class SQLClient(QMainWindow): + def __init__(self): + super().__init__() + self.connection = None + self.cursor = None + self.init_ui() + + def init_ui(self): + self.setWindowTitle("SQL Client") + self.setGeometry(100, 100, 800, 600) + + central_widget = QSplitter(self) + self.setCentralWidget(central_widget) + + # 上部分布局 + top_widget = QWidget() + top_layout = QVBoxLayout(top_widget) + + # 输入区域布局 + input_layout = QHBoxLayout() + top_layout.addLayout(input_layout) + + # self.query_text = QTextEdit() + # input_layout.addWidget(self.query_text) + + # 创建 SQL 输入框 + self.query_text = SQLTextEdit(self) + # self.setCentralWidget(self.query_text) + input_layout.addWidget(self.query_text) + + # 设置上部分为可伸缩部件 + top_widget.setLayout(top_layout) + central_widget.addWidget(top_widget) + + # 下部分布局 + bottom_widget = QWidget() + bottom_layout = QVBoxLayout(bottom_widget) + + # 结果区域布局 + self.result_table = QTableWidget() + self.result_table.setEditTriggers(QTableWidget.DoubleClicked) + self.result_table.setSelectionBehavior(QTableWidget.SelectRows) + bottom_layout.addWidget(self.result_table) + + # 连接区域布局 + connection_layout = QHBoxLayout() + bottom_layout.addLayout(connection_layout) + + splitter_handle = QSplitter(Qt.Vertical) + splitter_handle.addWidget(top_widget) + splitter_handle.addWidget(bottom_widget) + central_widget.addWidget(splitter_handle) + + self.db_type_input = QComboBox() + self.db_type_input.addItems(["sqlite", "mysql", "pgsql"]) + self.db_type_input.setPlaceholderText("Database Type") + connection_layout.addWidget(self.db_type_input) + + self.db_host_input = QLineEdit() + self.db_host_input.setPlaceholderText("Host") + connection_layout.addWidget(self.db_host_input) + + self.db_user_input = QLineEdit() + self.db_user_input.setPlaceholderText("Username") + connection_layout.addWidget(self.db_user_input) + + self.db_password_input = QLineEdit() + self.db_password_input.setEchoMode(QLineEdit.Password) + self.db_password_input.setPlaceholderText("Password") + connection_layout.addWidget(self.db_password_input) + + self.db_name_input = QLineEdit() + self.db_name_input.setPlaceholderText("Database Name") + connection_layout.addWidget(self.db_name_input) + + self.connect_button = QPushButton("Connect") + self.connect_button.clicked.connect(self.connect_to_database) + connection_layout.addWidget(self.connect_button) + + self.statusBar() + + self.create_menu() + + def create_menu(self): + # 创建菜单栏 + menu_bar = self.menuBar() + + # 创建文件菜单 + file_menu = menu_bar.addMenu("File") + + # 创建退出子菜单 + exit_action = QAction("Exit", self) + file_menu.addAction(exit_action) + + # 连接退出子菜单的信号槽 + exit_action.triggered.connect(self.close) + + # 创建插入菜单 + insert_menu = menu_bar.addMenu("Insert") + + # 创建插入子菜单项 + insert_action = QAction("INSERT INTO", self) + insert_menu.addAction(insert_action) + delete_action = QAction("DELETE FROM", self) + insert_menu.addAction(delete_action) + update_action = QAction("UPDATE", self) + insert_menu.addAction(update_action) + select_action = QAction("SELECT", self) + insert_menu.addAction(select_action) + create_action = QAction("CREATE DATABASE", self) + insert_menu.addAction(create_action) + show_tables_action = QAction("SHOW TABLES", self) + insert_menu.addAction(show_tables_action) + show_dbs_action = QAction("SHOW DATABASES", self) + insert_menu.addAction(show_dbs_action) + create_table_action = QAction("CREATE TABLE", self) + insert_menu.addAction(create_table_action) + + # 连接插入子菜单项的信号槽 + insert_action.triggered.connect(lambda: self.insert_sql("INSERT INTO")) + delete_action.triggered.connect(lambda: self.insert_sql("DELETE FROM")) + update_action.triggered.connect(lambda: self.insert_sql("UPDATE")) + select_action.triggered.connect(lambda: self.insert_sql("SELECT")) + create_action.triggered.connect(lambda: self.insert_sql("CREATE DATABASE")) + show_tables_action.triggered.connect(lambda: self.insert_sql("SHOW TABLES")) + show_dbs_action.triggered.connect(lambda: self.insert_sql("SHOW DATABASES")) + create_table_action.triggered.connect(lambda: self.insert_sql("CREATE TABLE")) + + # 创建执行动作 + execute_action = QAction("Execute (F5)", self) + execute_action.triggered.connect(self.execute_query) + execute_action.setShortcut(QKeySequence(Qt.Key_F5)) + # 将执行动作添加到菜单栏 + menu_bar.addAction(execute_action) + + def insert_sql(self, sql_type): + if sql_type: + # 根据SQL语句类型生成相应的语句 + if sql_type == "INSERT INTO": + sql = "INSERT INTO table_name (column1, column2) VALUES (?, ?)" + elif sql_type == "DELETE FROM": + sql = "DELETE FROM table_name WHERE condition" + elif sql_type == "UPDATE": + sql = "UPDATE table_name SET column1 = value1 WHERE condition" + elif sql_type == "SELECT": + sql = "SELECT * FROM table_name WHERE condition" + elif sql_type == "CREATE DATABASE": + sql = "CREATE DATABASE database_name" + elif sql_type == "SHOW TABLES": + if isinstance(self.connection, sqlite3.Connection): + sql = "SELECT name FROM sqlite_master WHERE type='table'" + else: + sql = "SHOW TABLES" + elif sql_type == "SHOW DATABASES": + if isinstance(self.connection, sqlite3.Connection): + sql = "SELECT name FROM sqlite_master WHERE type='database'" + elif isinstance(self.connection, psycopg2.extensions.connection): + sql = "SELECT datname FROM pg_database WHERE datistemplate = false" + else: + sql = "SHOW DATABASES" + elif sql_type == "CREATE TABLE": + sql = """\ +CREATE TABLE IF NOT EXISTS table_name ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL UNIQUE, + age INT, + hire_date DATE, + price DECIMAL(10, 2), + content TEXT, + is_deleted BOOLEAN, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +);\ +""" + # 将语句插入到输入框中 + cursor = self.query_text.textCursor() + cursor.insertText(sql) + + def connect_to_database(self): + db_type = self.db_type_input.currentText() + db_host = self.db_host_input.text() + db_user = self.db_user_input.text() + db_password = self.db_password_input.text() + db_name = self.db_name_input.text() + + try: + if db_type.lower() == "mysql": + host_parts = db_host.split(':') + host = host_parts[0] + port = int(host_parts[1]) if len(host_parts) > 1 else 3306 + self.connection = pymysql.connect(host=host, port=port, user=db_user, password=db_password, db=db_name) + self.cursor = self.connection.cursor() + elif db_type.lower() == "sqlite": + self.connection = sqlite3.connect(db_name) + self.cursor = self.connection.cursor() + elif db_type.lower() == "pgsql": + host_parts = db_host.split(':') + host = host_parts[0] + port = int(host_parts[1]) if len(host_parts) > 1 else 5432 + self.connection = psycopg2.connect(host=host, port=port, user=db_user, password=db_password, + dbname=db_name) + self.cursor = self.connection.cursor() + else: + raise ValueError("Invalid database type") + + # 更新窗口标题 + title = f"SQLClient - {db_host}/{db_name}" + self.setWindowTitle(title) + + self.statusBar().showMessage("Connected to database" + " " + db_name) + except (sqlite3.Error, pymysql.Error, psycopg2.Error) as e: + QMessageBox.critical(self, "Error", str(e)) + self.statusBar().showMessage("Connection failed") + + def execute_query(self): + if not self.connection: + QMessageBox.warning(self, "Warning", "Not connected to a database") + return + + # 获取当前光标对象 + cursor = self.query_text.textCursor() + + # 判断是否有选中内容 + if cursor.hasSelection(): + # 获取选中的文本 + selected_text = cursor.selectedText() + query = selected_text + else: + # 获取整个输入框的文本 + query = self.query_text.toPlainText() + + try: + # 使用分号分割多个 SQL 语句 + sql_statements = query.split(";") + + # 执行每个 SQL 语句 + for statement in sql_statements: + # 忽略空语句 + if not statement.strip(): + continue + + # 执行 + self.cursor.execute(statement) + + # 提交事务 + self.connection.commit() + + # 获取查询结果 + result = self.cursor.fetchall() + + # 设置表格行列数 + num_rows = len(result) + num_columns = len(self.cursor.description) + self.result_table.setRowCount(num_rows) + self.result_table.setColumnCount(num_columns) + + # 设置表头 + header = [field[0] for field in self.cursor.description] + self.result_table.setHorizontalHeaderLabels(header) + + # 填充结果到表格 + for row_idx, row_data in enumerate(result): + for col_idx, col_data in enumerate(row_data): + item = QTableWidgetItem(str(col_data)) + item.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.result_table.setItem(row_idx, col_idx, item) + + self.result_table.setContextMenuPolicy(Qt.CustomContextMenu) + self.result_table.customContextMenuRequested.connect(self.show_context_menu) + + self.statusBar().showMessage("Query executed successfully") + except (sqlite3.Error, pymysql.Error, psycopg2.Error) as e: + QMessageBox.critical(self, "Error", str(e)) + self.statusBar().showMessage("Query execution failed") + + def show_context_menu(self, pos): + if self.result_table.selectionModel().hasSelection(): + menu = QMenu(self) + update_action = QAction("Update to Database", self) + update_action.triggered.connect(self.update_to_database) + menu.addAction(update_action) + + # 导出为CSV菜单项 + export_csv_action = QAction("Export as CSV", self) + export_csv_action.triggered.connect(self.export_as_csv) + menu.addAction(export_csv_action) + + menu.exec(self.result_table.viewport().mapToGlobal(pos)) + + def get_target_table_name(self): + table_name, ok = QInputDialog.getText(self, "Target Table", "Enter the target table name:") + if ok and table_name: + return table_name + else: + return None + + def update_to_database(self): + if not self.connection: + QMessageBox.warning(self, "Warning", "Not connected to a database") + return + + table_name = self.get_target_table_name() + if not table_name: + return + + try: + + # 获取表的主键字段名 + primary_key_column = self.get_primary_key_column(table_name) + if not primary_key_column: + QMessageBox.warning(self, "Warning", "Table doesn't have a primary key column") + return + + for row in range(self.result_table.rowCount()): + values = [] + update_pairs = [] + primary_key_value = None + + for col in range(self.result_table.columnCount()): + item = self.result_table.item(row, col) + value = item.text() + values.append(value) + + # 构建更新语句的列名=值对 + column_name = self.result_table.horizontalHeaderItem(col).text() + update_pairs.append(f"{column_name} = '{value}'") + + # 获取主键列的值 + if column_name == primary_key_column: + primary_key_value = value + + # 构建更新语句 + update_query = f"UPDATE {table_name} SET {', '.join(update_pairs)} WHERE {primary_key_column} = '{primary_key_value}'" + self.cursor.execute(update_query) + + self.connection.commit() + + self.statusBar().showMessage("Updated to database successfully") + except (sqlite3.Error, pymysql.Error, psycopg2.Error) as e: + QMessageBox.critical(self, "Error", str(e)) + self.statusBar().showMessage("Update to database failed") + + def get_primary_key_column(self, table_name): + # 根据数据库类型查询表的主键字段名 + if self.db_type_input.currentText().lower() == "mysql": + query = f"SHOW KEYS FROM {table_name} WHERE Key_name = 'PRIMARY'" + self.cursor.execute(query) + primary_key_info = self.cursor.fetchone() + if primary_key_info: + return primary_key_info[4] + elif self.db_type_input.currentText().lower() == "sqlite": + query = f"PRAGMA table_info({table_name})" + self.cursor.execute(query) + columns = self.cursor.fetchall() + for column in columns: + if column[5] == 1: # 判断是否为主键 + return column[1] + elif self.db_type_input.currentText().lower() == "pgsql": + query = f"SELECT column_name FROM information_schema.key_column_usage WHERE table_name = '{table_name}' AND constraint_name LIKE '%_pkey'" + self.cursor.execute(query) + primary_key_info = self.cursor.fetchone() + if primary_key_info: + return primary_key_info[0] + return None + + def export_as_csv(self): + file_dialog = QFileDialog() + file_path, _ = file_dialog.getSaveFileName(self, "Export as CSV", "", "CSV Files (*.csv)") + + if file_path: + try: + with open(file_path, "w", newline="") as file: + writer = csv.writer(file) + header_data = [self.result_table.horizontalHeaderItem(col).text() for col in + range(self.result_table.columnCount())] + writer.writerow(header_data) + for row in range(self.result_table.rowCount()): + row_data = [self.result_table.item(row, col).text() for col in + range(self.result_table.columnCount())] + writer.writerow(row_data) + self.statusBar().showMessage("Exported as CSV successfully") + except IOError as e: + QMessageBox.critical(self, "Error", str(e)) + self.statusBar().showMessage("Export as CSV failed") + + def closeEvent(self, event): + if self.connection: + self.connection.close() + + event.accept() + + +if __name__ == '__main__': + app = QApplication(sys.argv) + + if len(sys.argv) > 1: + db_file = sys.argv[1] + sql_client = SQLClient() + sql_client.db_name_input.setText(db_file) + else: + sql_client = SQLClient() + + sql_client.show() + sys.exit(app.exec()) \ No newline at end of file