Esempio n. 1
0
class WinApp(QMainWindow, Ui_MainWindow):
    stmt_signal = pyqtSignal(str)

    def __init__(self):
        super(QMainWindow, self).__init__()

        self.setupUi(self)
        self.connButtonBox.button(QDialogButtonBox.Ok).setText("Connect")
        self.runButtonBox.button(QDialogButtonBox.Ok).setText("Run")
        self.tunnelButton.clicked[bool].connect(self.toggle_tunnel)
        self.tunnelWidget.hide()
        self.keyFileButton.clicked.connect(self.choose_keyfile)
        self.clearButton.clicked.connect(self.clear_tunnel)
        self.keyComboBox.activated[str].connect(self.set_key_type)

        self.db_engine = DBEngine()
        self.db_engine.result_signal.connect(self.show_results)
        self.db_engine.progress_signal.connect(self.set_progress)
        self.db_engine.error_signal.connect(self.show_error)
        self.db_engine.msg_signal.connect(self.show_msg)
        self.stmt_signal.connect(self.db_engine.receive_stmt)

        self.db_lite = DBLite()
        self.tunnel = Tunnel()
        self.tunnel_keyfile = ""
        self.data_schema = []
        self.settings = QSettings()
        self.restore_settings()

        history = self.db_lite.history()
        self.historyMenuBar = QMenuBar(self.sqlWidget)
        self.historyMenuBar.setNativeMenuBar(False)
        self.historyMenu = self.historyMenuBar.addMenu('     &History   ⇲    ')
        actions = [
            QAction(sql.replace('\n', ' '), self.historyMenu)
            for sql in history
        ]
        for i in range(len(actions)):
            actions[i].triggered.connect(partial(self.use_history, history[i]))
        self.historyMenu.addActions(actions)

        self.history_cache = pylru.lrucache(history_limit, self.remove_action)
        for i in reversed(range(len(history))):
            self.history_cache[history[i]] = {
                KEY_ACTION: actions[i],
                KEY_RESULTS: [],
                KEY_COLUMNS: []
            }

        self.historyMenu.setStyleSheet("""
        QMenu {
            max-width: 1000px;
            font-size: 12px;
        }
        """)
        self.historyMenuBar.setStyleSheet("""
        QMenuBar {
            border: None;
        }
        QMenuBar::item {
            background: #404040;
            color: #ffffff;
            border-radius: 4px;
        }
        QMenuBar::item:selected {
            background: rgba(1, 1, 1, 0.2);;
            color: #404040;
            border-radius: 4px;
        }
        """)
        self.sqlWidgetLayout.insertWidget(0, self.historyMenuBar)

        self.connButtonBox.accepted.connect(self.connect)
        self.connButtonBox.rejected.connect(self.disconnect)
        self.runButtonBox.accepted.connect(self.run)
        self.runButtonBox.rejected.connect(self.cancel)
        self.tablesWidget.itemDoubleClicked.connect(self.get_meta)
        self.tablesWidget.itemExpanded.connect(self.get_meta)

        self.progressBar = QProgressBar()
        self.statusbar.addPermanentWidget(self.progressBar)
        self.progressBar.setValue(100)
        QApplication.processEvents()

        self.highlight = syntax.PrestoHighlighter(self.sqlEdit)

        self.dataWidget.setContextMenuPolicy(Qt.CustomContextMenu)
        self.dataWidget.customContextMenuRequested.connect(
            self.show_table_context_menu)
        self.dataWidget.horizontalHeader().setStretchLastSection(False)
        self.dataWidget.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)

        self.schemaView.header().setStretchLastSection(False)
        self.schemaView.header().setSectionResizeMode(
            QHeaderView.ResizeToContents)

        self.runButtonBox.button(
            QDialogButtonBox.Ok).setShortcut("Ctrl+Return")

        self.completer = SQLCompleter(self)
        self.sqlEdit.setCompleter(self.completer)

        self.set_key_type(self.keyComboBox.currentText())
        self.sqlEdit.setFocus()

    def connect(self):
        self.tablesWidget.clear()
        self.schemaView.clear()
        try:
            if self.serverEdit.text().strip() and self.gatewayEdit.text(
            ).strip() and self.tunnelUserEdit.text().strip():
                self.statusbar.showMessage('Starting tunnel')
                QApplication.processEvents()
                self.tunnel.start_tunnel(
                    self.serverEdit.text().strip(),
                    self.gatewayEdit.text().strip(),
                    self.tunnelUserEdit.text().strip(),
                    self.keyComboBox.currentText() == 'KeyFile',
                    self.tunnel_keyfile, self.pwdLineEdit.text(),
                    self.urlEdit.text().strip())

            self.statusbar.showMessage('Connecting')
            QApplication.processEvents()
            self.db_engine.connect(self.urlEdit.text().strip(),
                                   self.userEdit.text().strip())
            self.statusbar.showMessage('Fetching db info')
            QApplication.processEvents()

            dbs = self.db_engine.dbs()
            for db in dbs:
                db_item = QTreeWidgetItem([db])
                db_item.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)
                self.tablesWidget.addTopLevelItem(db_item)
            self.completer.addItems(dbs)
            self.statusbar.showMessage('Connected')

        except Exception as err:
            QMessageBox.critical(self, "Error", str(err))
            self.db_engine.close()
            self.statusbar.showMessage('Disconnected')

    def disconnect(self):
        self.db_engine.close()
        self.tunnel.stop_tunnel()
        self.schemaView.clear()
        self.tablesWidget.clear()
        self.statusbar.showMessage('Disconnected')

    def get_meta(self, item):
        try:
            if item.parent():
                columns = self.db_engine.columns(item.parent().text(0),
                                                 item.text(0))
                self.set_columns(columns)
                self.completer.addItems([column[0] for column in columns])
            else:
                if item.childCount() <= 0:
                    tables = self.db_engine.tables(item.text(0))
                    item.addChildren(
                        [QTreeWidgetItem([table]) for table in tables])
                    self.completer.addTreeItem(item)
        except Exception as err:
            QMessageBox.critical(self, "Error", str(err))

    def set_columns(self, columns):
        try:
            self.schemaView.clear()
            for column in columns:
                column_item = self.type_tree(column[0],
                                             type_parser.parse(column[1]))
                self.schemaView.addTopLevelItem(column_item)
            self.schemaView.expandToDepth(2)
            for j in range(self.schemaView.columnCount()):
                self.schemaView.resizeColumnToContents(j)
        except Exception as err:
            QMessageBox.critical(self, "Error", str(err))

    def run(self):
        self.dataWidget.setRowCount(0)
        self.dataWidget.setColumnCount(0)
        try:
            self.stmt_signal.emit(self.sqlEdit.toPlainText().strip().replace(
                ';', ''))
            self.db_engine.start()
        except Exception as err:
            QMessageBox.critical(self, "Error", str(err))

    def show_results(self, results, columns, sql, save=True):
        try:
            num_cols = len(columns)
            num_rows = len(results)
            self.dataWidget.setRowCount(num_rows)
            self.dataWidget.setColumnCount(num_cols)
            for i in range(num_rows):
                for j in range(num_cols):
                    self.dataWidget.setItem(
                        i, j, QTableWidgetItem(str(results[i][j])))
            self.dataWidget.resizeColumnsToContents()
            for j in range(num_cols):
                header_label = columns[j][0]
                # header_width = self.fm.width(header_label)
                # if header_width > self.dataWidget.columnWidth(j):
                #     self.dataWidget.setColumnWidth(j, header_width)
                self.dataWidget.setHorizontalHeaderItem(
                    j, QTableWidgetItem(header_label))
            self.data_schema = [column[1] for column in columns]
            if not columns and not results:
                self.dataWidget.setColumnCount(1)
                self.dataWidget.setHorizontalHeaderItem(
                    0, QTableWidgetItem("Empty Result"))
            if save:
                self.save_history(sql, results, columns)
        except Exception as err:
            QMessageBox.critical(self, "Error", str(err))
        finally:
            self.statusbar.showMessage('Finished')

    def cancel(self):
        self.db_engine.cancel()

    def set_progress(self, progress):
        self.progressBar.setValue(progress)
        QApplication.processEvents()

    def save_history(self, sql, results, columns):
        if sql not in self.history_cache:
            action = QAction(sql.replace('\n', ' '), self.historyMenu)
            action.triggered.connect(partial(self.use_history, sql))
            self.history_cache[sql] = {
                KEY_ACTION: action,
                KEY_RESULTS: results,
                KEY_COLUMNS: columns
            }
        else:
            action = self.history_cache[sql][KEY_ACTION]
            self.historyMenu.removeAction(action)
            if len(results) * len(columns) <= CACHE_LIMIT:
                self.history_cache[sql][KEY_RESULTS] = results
                self.history_cache[sql][KEY_COLUMNS] = columns
            else:
                self.history_cache[sql][KEY_RESULTS] = []
                self.history_cache[sql][KEY_COLUMNS] = [
                    ("results too large to cache", "varchar")
                ]
        self.historyMenu.insertAction(
            self.historyMenu.actions()[0] if
            (self.historyMenu.actions()) else None, action)
        self.db_lite.upsert(sql)

    def use_history(self, sql):
        self.sqlEdit.setText(sql)
        QApplication.processEvents()
        cached = self.history_cache[sql]
        self.db_engine.result_signal.emit(cached[KEY_RESULTS],
                                          cached[KEY_COLUMNS], sql, False)
        self.statusbar.showMessage("Loaded cached history")

    def remove_action(self, sql, cached):
        self.historyMenu.removeAction(cached[KEY_ACTION])

    def show_table_context_menu(self, position):
        col = self.dataWidget.columnAt(position.x())
        dialog = QDialog(self, Qt.Popup)
        schema_view = QTreeWidget(dialog)
        schema_view.headerItem().setText(0, "Field")
        schema_view.headerItem().setText(1, "Type")
        root = type_parser.parse(self.data_schema[col])
        schema_tree = self.type_tree(
            self.dataWidget.horizontalHeaderItem(col).text(), root)
        schema_view.addTopLevelItem(schema_tree)
        schema_view.expandToDepth(2)
        schema_view.header().setStretchLastSection(False)
        schema_view.setSizeAdjustPolicy(
            QAbstractScrollArea.AdjustToContentsOnFirstShow)
        schema_view.header().setSectionResizeMode(QHeaderView.ResizeToContents)
        schema_view.setMinimumHeight(30)
        schema_view.setMaximumHeight(400)
        schema_view.adjustSize()
        schema_view.setHeaderHidden(True)

        dialog.move(
            self.dataWidget.mapToGlobal(position) +
            QPoint(dialog.width() / 5 * 2,
                   dialog.height() + 5))
        dialog.adjustSize()
        dialog.show()

    def type_tree(self, name, root):
        if root.children[0].data == 'primitive_type':
            return QTreeWidgetItem([name, root.children[0].children[0].value])
        elif root.children[0].data == 'row_type':
            root_widget = QTreeWidgetItem([name, 'row'])
            for i in range(len(root.children[0].children) // 2):
                name = root.children[0].children[i * 2].value
                child_type = root.children[0].children[i * 2 + 1]
                root_widget.addChild(self.type_tree(name, child_type))
            return root_widget
        elif root.children[0].data == 'array_type':
            if root.children[0].children[0].children[
                    0].data == 'primitive_type':
                return QTreeWidgetItem([
                    name, 'array(' +
                    root.children[0].children[0].children[0].children[0].value
                    + ')'
                ])
            else:
                root_widget = QTreeWidgetItem([name, 'array(row)'])
                for i in range(
                        len(root.children[0].children[0].children[0].children)
                        // 2):
                    name = root.children[0].children[0].children[0].children[
                        i * 2].value
                    child_type = root.children[0].children[0].children[
                        0].children[i * 2 + 1]
                    root_widget.addChild(self.type_tree(name, child_type))
                return root_widget

        elif root.children[0].data == 'map_type':
            root_widget = QTreeWidgetItem([name, 'map'])
            key = self.type_tree('_key', root.children[0].children[0])
            value = self.type_tree('_value', root.children[0].children[1])
            root_widget.addChildren([key, value])
            return root_widget
        else:
            pass

    def toggle_tunnel(self, pressed):
        if pressed:
            self.tunnelWidget.show()
            self.repaint()
        else:
            self.tunnelWidget.hide()

    def choose_keyfile(self):
        file_name, _ = QFileDialog.getOpenFileName(self, "Choose Key File",
                                                   QDir.homePath() + "/.ssh",
                                                   "All Files (*)")
        if file_name:
            self.tunnel_keyfile = file_name
            self.keyFileButton.setText(file_name.split('/')[-1])

    def clear_tunnel(self):
        self.tunnel_keyfile = None
        self.keyFileButton.setText("Choose Key File...")
        self.serverEdit.clear()
        self.gatewayEdit.clear()
        self.tunnelUserEdit.clear()
        self.pwdLineEdit.clear()

    def close_all(self):
        self.save_settings()
        self.db_engine.close()
        self.tunnel.stop_tunnel()

    def restore_settings(self):
        self.urlEdit.setText(self.settings.value(URL, ""))
        self.userEdit.setText(self.settings.value(USER, ""))
        self.serverEdit.setText(self.settings.value(SERVER, ""))
        self.gatewayEdit.setText(self.settings.value(GATEWAY, ""))
        self.tunnelUserEdit.setText(self.settings.value(TUNNEL_USER, ""))
        self.tunnel_keyfile = self.settings.value(KEYFILE, "")
        if self.tunnel_keyfile:
            self.keyFileButton.setText(self.tunnel_keyfile.split('/')[-1])
        self.pwdLineEdit.setText(self.settings.value(TUNNEL_PWD, ""))
        self.keyComboBox.setCurrentText(self.settings.value(
            KEYTYPE, "KeyFile"))

    def save_settings(self):
        self.settings.setValue(URL, self.urlEdit.text().strip())
        self.settings.setValue(USER, self.userEdit.text().strip())
        self.settings.setValue(SERVER, self.serverEdit.text().strip())
        self.settings.setValue(GATEWAY, self.gatewayEdit.text().strip())
        self.settings.setValue(TUNNEL_USER, self.tunnelUserEdit.text().strip())
        self.settings.setValue(KEYFILE, self.tunnel_keyfile)
        self.settings.setValue(KEYTYPE, self.keyComboBox.currentText())
        self.settings.setValue(TUNNEL_PWD, self.pwdLineEdit.text())
        self.settings.sync()

    def show_error(self, error):
        QMessageBox.critical(self, "Error", error)

    def show_msg(self, msg):
        self.statusbar.showMessage(msg)

    def set_key_type(self, key_type):
        if key_type == 'KeyFile':
            self.pwdLineEdit.hide()
            self.keyFileButton.show()
        else:
            self.keyFileButton.hide()
            self.pwdLineEdit.show()