From 5b4ff89abb745fbdaab2d56e37d92687b8d52a7b Mon Sep 17 00:00:00 2001 From: Lea Laux Date: Fri, 19 Feb 2021 14:27:55 +0100 Subject: [PATCH] Insert and Create/Insert in csv importer Inserting the data in the csv import dialog is now possible. In addition, create/insert in one action is possible. The building process for the activation in the main window has started. --- pygadmin/csv_importer.py | 35 +++++--- pygadmin/widgets/csv_import.py | 142 ++++++++++++++++++++++++-------- pygadmin/widgets/main_window.py | 17 +++- 3 files changed, 146 insertions(+), 48 deletions(-) diff --git a/pygadmin/csv_importer.py b/pygadmin/csv_importer.py index d8e8e27..0bc51ad 100644 --- a/pygadmin/csv_importer.py +++ b/pygadmin/csv_importer.py @@ -30,6 +30,10 @@ class CSVImporter: self.database_query_executor = DatabaseQueryExecutor() # Use the given database connection for further execution of database queries on the given database. self.database_query_executor.database_connection = database_connection + # Define a chunk size for splitting the data in separate chunks for inserting later. 5000 is an acceptable value + # between the two extreme cases (inserting all data in one INSERT or inserting every row/list with an own + # INSERT). A class attribute is chosen for potential (read) access. + self.chunk_size = 5000 def check_existence_csv_file(self): """ @@ -217,31 +221,32 @@ class CSVImporter: # Check the name of the table. self.table_name = self.check_ddl_parameter(self.table_name) - def create_and_execute_insert_queries(self): + def create_and_execute_insert_queries(self, table_name=None): """ Create the necessary queries for inserting the data in the table based on splitting the data in sub lists for improving the performance of the insert. The attribute of the class for the csv data should not be harmed, so - a copy is used for the working process. Execute the queries after their creation. + a copy is used for the working process. The default for the table name is None, so the attribute of the class + for the table name is used. Execute the queries after their creation. """ + # Use the table name defined by the class/csv file as table name. + if table_name is None: + table_name = self.table_name + # Copy the data list, so the work data list can be used and modified. work_data_list = copy.copy(self.csv_data) # Delete the header, because the header does not have to be inserted. del work_data_list[0] - # Define a chunk size for separation the work data list in those chunks. 5000 is an acceptable value between the - # two extreme cases (inserting all data in one INSERT or inserting every row/list with an own INSERT). - chunk_size = 5000 - # Split the work data list in lists with the given chunk size. - work_data_list = [work_data_list[i * chunk_size:(i+1) * chunk_size] - for i in range((len(work_data_list) + chunk_size - 1) // chunk_size)] + work_data_list = [work_data_list[i * self.chunk_size:(i+1) * self.chunk_size] + for i in range((len(work_data_list) + self.chunk_size - 1) // self.chunk_size)] # Iterate over every sub list in the work data list. Those sub lists contain their separate chunk of data for # inserting. for sub_data_list in work_data_list: - # Get the begin of an insert query. - insert_query = self.create_insert_query_begin() + # Get the begin of an insert query with the given table name. + insert_query = self.create_insert_query_begin(table_name) # Define a list for the parameters, because the data is used as parameter in the query. parameter_list = [] @@ -292,13 +297,17 @@ class CSVImporter: # Execute the insert query. self.execute_query(insert_query, parameter_list) - def create_insert_query_begin(self): + def create_insert_query_begin(self, table_name=None): """ - Create the begin of an insert query. + Create the begin of an insert query. The default table name is None, so the name of the csv file is used. """ + # Get the default name, the name of the csv file. + if table_name is None: + table_name = self.table_name + # Begin with the INSERT INTO and the checked table name, so an formatted input is okay. - insert_query = "INSERT INTO {} (".format(self.table_name) + insert_query = "INSERT INTO {} (".format(table_name) # Get the header for the column names. header = self.csv_data[0] diff --git a/pygadmin/widgets/csv_import.py b/pygadmin/widgets/csv_import.py index 7fa47d4..f6cff94 100644 --- a/pygadmin/widgets/csv_import.py +++ b/pygadmin/widgets/csv_import.py @@ -1,10 +1,11 @@ -import sys -import time - -from PyQt5.QtWidgets import QDialog, QGridLayout, QLabel, QApplication, QPushButton, QMessageBox, QLineEdit, \ +from PyQt5.QtWidgets import QDialog, QGridLayout, QLabel, QPushButton, QMessageBox, QLineEdit, \ + QInputDialog + +from pygadmin.csv_importer import CSVImporter +from pygadmin.widgets.widget_icon_adder import IconAdder +from PyQt5.QtWidgets import QDialog, QGridLayout, QLabel, QPushButton, QMessageBox, QLineEdit, \ QInputDialog -from pygadmin.connectionfactory import global_connection_factory from pygadmin.csv_importer import CSVImporter from pygadmin.widgets.widget_icon_adder import IconAdder @@ -56,7 +57,6 @@ class CSVImportDialog(QDialog): self.init_ui() # Initialize the grid layout. self.init_grid() - # TODO: Better functions for connecting for the signals of the database query executor. # Show an error for a non existing file. else: @@ -145,16 +145,29 @@ class CSVImportDialog(QDialog): # Create a drop button for dropping the table. self.drop_button = QPushButton("Drop Table") - # Connect the button with the signal for actually dropping the table. + # Connect the button with the function for actually dropping the table. self.drop_button.clicked.connect(self.drop_table) - # Connect the signals for the result data and the error with functions for processing. - self.csv_importer.database_query_executor.result_data.connect(self.process_create_drop_success) - self.csv_importer.database_query_executor.error.connect(self.process_create_drop_error) - - # Under construction TODO - self.insert_button = QPushButton("Insert") + # Create an insert button for inserting the data. + self.insert_button = QPushButton("Insert Data") + # Connect the button with the function for inserting the data. self.insert_button.clicked.connect(self.insert_data) + + self.create_and_insert_button = QPushButton("Create Table and Insert Data") + self.create_and_insert_button.clicked.connect(self.create_table_and_insert_data) + + # Define two parameters/attributes for controlling the number of expected inserts. + self.number_of_inserts = 0 + self.executed_inserts = 0 + + # Define an attribute for the usage after a create statement: If the attribute is True, the data is inserted + # right after the creation of the table. + self.proceed_with_insert = False + + # Connect the signals for the result data and the error with functions for processing. + self.csv_importer.database_query_executor.result_data.connect(self.process_sql_success) + self.csv_importer.database_query_executor.error.connect(self.process_sql_error) + self.show() def init_grid(self): @@ -185,10 +198,13 @@ class CSVImportDialog(QDialog): # Place the close label at the end of CREATE. grid_layout.addWidget(self.close_label, line_edit_count, 0) - # Set the create button at the end of the statement, created by the line edits. - grid_layout.addWidget(self.create_button, line_edit_count, 1) - # Set the drop button on the right of the widget. - grid_layout.addWidget(self.drop_button, 0, 4) + # Set the create and insert button at the end of the statement, created by the line edits. + grid_layout.addWidget(self.create_and_insert_button, line_edit_count, 1) + # Set the drop button on the right of the create and insert button. + grid_layout.addWidget(self.drop_button, line_edit_count, 2) + # Set the insert and the create button under the row with the create and insert button. + grid_layout.addWidget(self.insert_button, line_edit_count+1, 0) + grid_layout.addWidget(self.create_button, line_edit_count+1, 1) # Set the spacing of the grid. grid_layout.setSpacing(10) @@ -218,8 +234,6 @@ class CSVImportDialog(QDialog): """ create_statement = self.get_user_create_statement() - self.csv_importer.database_query_executor.result_data.connect(self.process_create_drop_success) - self.csv_importer.database_query_executor.error.connect(self.process_create_drop_error) self.csv_importer.create_table_for_csv_data(create_statement) def get_user_create_statement(self): @@ -267,32 +281,92 @@ class CSVImportDialog(QDialog): table_name = self.table_name_line_edit.text() self.csv_importer.drop_table(table_name) - def process_create_drop_success(self, result): + def process_sql_success(self, result): """ Process the result of the table creation or dropping. """ # The parameter result contains a list. If the list is not empty, a success can be processed. if result: - QMessageBox.information(self, "Create Success", "The result is {}".format(result)) + # Get the title of the result list for further usage as QMessageBox title. + title = result[0][0] + # A data definition query has been executed. + if self.number_of_inserts == 0 and self.executed_inserts == 0: + # Get the message. + message = result[1][0] + # Show the title and the message to the user. + QMessageBox.information(self, title, message) - def process_create_drop_error(self, result): + # Check the attribute for proceeding with the insert of the data. + if self.proceed_with_insert is True: + # Insert the data of the csv file. + self.insert_data() + + # The inserts are not done yet. + elif self.number_of_inserts != self.executed_inserts: + # Increment the insert counter for executed inserts. + self.executed_inserts += 1 + + # The inserts are done. + elif self.number_of_inserts == self.executed_inserts: + # Show the success to the user. + QMessageBox.information(self, title, "The data has been inserted successfully.") + # Reset the number of excepted inserts and the number of executed inserts back to 0. + self.number_of_inserts = 0 + self.executed_inserts = 0 + + def process_sql_error(self, result): """ - Process the error during the create or drop process. + Process the SQL error after the try to execute a query. """ - QMessageBox.critical(self, "Create Error", "An error occurred during the create process: {}".format(result)) + # Get the title of the error message. + title = result[0] + # Get the error message. + message = result[1] + + # This function defines the error case, so if a table should have been created and the data inserted, set the + # attribute for proceeding with an insert to False, which is the default value for this attribute. + if self.proceed_with_insert is True: + self.proceed_with_insert = False + + # A data definition query should have been executed. + if self.number_of_inserts == 0 and self.executed_inserts == 0: + QMessageBox.critical(self, title, message) + + # The inserts are not done yet. + elif self.number_of_inserts != self.executed_inserts: + # Increment the counter for one processed insert. + self.executed_inserts += 1 + + # The inserts are done. + elif self.number_of_inserts == self.executed_inserts: + # Show the insert error + QMessageBox.critical(self, "Insert Error", "An error occurred during the insert: {}".format(message)) + # Reset the number of excepted inserts and the number of executed inserts back to 0. + self.number_of_inserts = 0 + self.executed_inserts = 0 - # under construction: TODO def insert_data(self): - begin = time.time() - self.csv_importer.create_and_execute_insert_queries() - end = time.time() - print("Runtime: {}".format(end - begin)) + """ + Insert the data of the csv file into the given table. + """ + self.proceed_with_insert = False -if __name__ == "__main__": - app = QApplication(sys.argv) - csv_import = CSVImportDialog(global_connection_factory.get_database_connection("localhost", "testuser", "testdb"), - "/home/sqlea/test.csv") - sys.exit(app.exec()) + # Get the table name out of the line edit. + table_name = self.table_name_line_edit.text() + # Calculate the expected number of inserts by the chunk size of the csv importer. + self.number_of_inserts = (len(self.csv_importer.csv_data) - 2) // self.csv_importer.chunk_size + # Start the insert process. + self.csv_importer.create_and_execute_insert_queries(table_name) + + def create_table_and_insert_data(self): + """ + Create the table and insert the data. + """ + + # Set the attribute for inserting the data to True. + self.proceed_with_insert = True + # Create the table. The insert will be triggered, if the create statement is successful. + self.create_table() diff --git a/pygadmin/widgets/main_window.py b/pygadmin/widgets/main_window.py index 9de2070..834e3ae 100644 --- a/pygadmin/widgets/main_window.py +++ b/pygadmin/widgets/main_window.py @@ -3,7 +3,7 @@ import logging from PyQt5 import QtCore from PyQt5.QtGui import QIcon, QPixmap -from PyQt5.QtWidgets import QMainWindow, QAction, QToolBar, QMessageBox, QMenu +from PyQt5.QtWidgets import QMainWindow, QAction, QToolBar, QMessageBox, QMenu, QFileDialog from PyQt5.QtCore import Qt, pyqtSlot import pygadmin @@ -140,6 +140,7 @@ class MainWindow(QMainWindow): self.add_action_to_menu_bar("Change Database Connections", self.activate_new_connection_dialog) # Create an action for showing the current history. self.add_action_to_menu_bar("Show History", self.activate_command_history_dialog) + self.add_action_to_menu_bar("Import CSV", self.activate_csv_import) # Create a sub menu for settings. settings_menu = QMenu("Settings", self) # Add the sub menu to the edit menu point. @@ -465,6 +466,20 @@ class MainWindow(QMainWindow): # Set the text to the query input editor of the editor widget. empty_editor_widget.query_input_editor.setText(command) + def activate_csv_import(self): + """ + Activate the necessary steps for starting the csv import dialog. This process includes getting the csv file by + a file dialog and getting the current database connection. TODO: Think about a better place for the importer or + error for missing database connection. + """ + + file_name_and_type = QFileDialog.getOpenFileName(self, "Open CSV", "", "CSV (*.csv)") + file_name = file_name_and_type[0] + + # The user has aborted the process, so the file name is an empty string, which is useless. + if file_name == "": + return + @pyqtSlot(str) def show_status_bar_message(self, message): """