From 94d847e84fe2aa4e1e1c01c761b6b894cc88ac47 Mon Sep 17 00:00:00 2001 From: Muhammed Zafar Date: Thu, 16 Nov 2023 21:33:27 +0530 Subject: [PATCH 1/6] [FEAT] Lists the cyclic tables --- data.py | 2 +- src/populate.py | 135 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 106 insertions(+), 31 deletions(-) diff --git a/data.py b/data.py index 14454b1..9a032a7 100644 --- a/data.py +++ b/data.py @@ -27,7 +27,7 @@ fake = faker.Faker() number_of_fields = 1 excluded_tables = ["system_setting"] -tables_to_fill = ["user"] +tables_to_fill = [] graph = True fields = [ diff --git a/src/populate.py b/src/populate.py index 8feb9a0..0fc2a44 100644 --- a/src/populate.py +++ b/src/populate.py @@ -17,7 +17,7 @@ from sqlalchemy import create_engine, inspect from sqlalchemy_utils import has_unique_index from rich import print - +import keyboard class DatabasePopulator: """ @@ -49,7 +49,6 @@ def __init__( graph: bool = True, special_fields: list[dict] = None, ) -> None: - db_url = f"mysql+mysqlconnector://{user}:{password}@{host}/{database}" self.completed_tables_list = [] @@ -60,7 +59,7 @@ def __init__( self.engine = create_engine(db_url, echo=False) self.rows = rows inspector = inspect(self.engine) - + # If no tables are specified, fill all tables in the database # Otherwise, fill the specified tables tables_to_fill = tables_to_fill or inspector.get_table_names() @@ -88,7 +87,7 @@ def __init__( ) # Arranges inheritance relations in a directed graph - self.arrange_graph() + self.arrange_graph(tables_to_fill=tables_to_fill) # Populates the database with random data self.fill_table(inspector=inspector) @@ -105,13 +104,15 @@ def show_end_banner(self): banner = f.readlines() print() - [ print( - Align( - bane.strip(), - align="center", + [ + print( + Align( + bane.strip(), + align="center", + ) ) - - ) for bane in banner ] + for bane in banner + ] success = random.choice( [ @@ -313,13 +314,14 @@ def draw_graph(self): plt.axis("off") plt.show() - def arrange_graph(self): + def arrange_graph(self, tables_to_fill): """ The function arranges identified inheritance relations in a directed graph and orders them - topologically. + topologically. It involves the user in resolving circular dependencies. """ graph = nx.DiGraph() + # Populate the graph for table, inherited_tables in self.inheritance_relations.items(): if inherited_tables: for inherited_table in inherited_tables: @@ -330,10 +332,17 @@ def arrange_graph(self): self.job_progress.advance(self.identifying_relations) - ordered_tables = list(nx.topological_sort(graph)) + # Detect cycles + try: + ordered_tables = list(nx.topological_sort(graph)) + except nx.NetworkXUnfeasible as e: + cycles = list(nx.simple_cycles(graph)) + ordered_tables = self.resolve_cycles_with_user_input( + cycles=cycles, tables_to_fill=tables_to_fill + ) + # Order the tables based on topological sort or user resolution ordered_inheritance_relations = OrderedDict() - for table in ordered_tables: if table in self.inheritance_relations: ordered_inheritance_relations[table] = self.inheritance_relations[table] @@ -343,9 +352,67 @@ def arrange_graph(self): self.inheritance_relations = ordered_inheritance_relations self.job_progress.advance(self.identifying_relations) + def resolve_cycles_with_user_input(self, cycles, tables_to_fill): + """ + Ask the user to resolve the detected cycles and return the adjusted order of tables. + """ + indexed_list = self.get_indexed_list(cycles) + + self.layout["body"].update( + Panel( + Align.center("\n".join(indexed_list)), + highlight=True, + padding=1, + expand=True, + title="[yellow b]WARNING", + ) + ) + + + + # Implement user interaction here to resolve cycles + # This could be as simple as asking the user to provide a new order for the tables + # or more complex logic based on your application's needs. + + # For simplicity, let's assume user provides a new order of tables + user_ordered_tables = input( + "Please provide a new order of tables (comma-separated): " + ) + return user_ordered_tables.split(",") + + def generate_circular_dependency_list(self, cycles): + """ + Generate a formatted list of circular dependencies for user interaction. + + Args: + cycles (list): A list of lists, where each inner list represents a cycle of dependencies. + + Returns: + list: A formatted list with circular dependencies and instructions. + """ + # Find the table with the most circular dependencies + cyclic_tables = list(max(cycles, key=len)) + + # Highlight the first table + cyclic_tables[0] = f"[black on white]{cyclic_tables[0]}[/]" + + # Create the indexed list with proper formatting + indexed_list = [ + f"{index}. {item}" for index, item in enumerate(cyclic_tables, start=1) + ] + + # Insert headers and instructions + indexed_list.insert(0, "[b red]Circular dependencies detected in the following tables,") + indexed_list.insert(1, "provide an order to fill:[/b red]\n") + indexed_list.insert(2, "[i yellow]Use arrow keys to navigate, `+` to move an item up the list,") + indexed_list.insert(3, "`-` to move an item down the list, and `Enter` to save.[/i yellow]\n\n") + + return indexed_list + + def populate_fields(self, column, table): """ - The function `populate_fields` populates a + The function `populate_fields` populates a column with a value based on the column's name, type, and table name. """ @@ -411,7 +478,11 @@ def handle_column_population(self, table, column): count -= 1 if count <= 0: raise ValueError( - f"I can't find a unique value to insert into column '{column.name}' in table '{table.name}'" + ( + f"I can't find a unique value " + f"to insert into column '{column.name}' in " + f"table '{table.name}'" + ) ) return value @@ -429,13 +500,13 @@ def get_unique_column_values(self, column, unique_columns, table): conn = self.engine.connect() s = sqlalchemy.select(table.c[column.name]) - + # Cache the column's unique values self.cached_unique_column_values[column] = { row[0] for row in conn.execute(s).fetchall() } conn.close() - + return self.cached_unique_column_values[column] return set() @@ -443,7 +514,7 @@ def get_value(self, column, foreign_columns, unique_columns, table): """ The function `get_value` returns a value for a column in a table. """ - # It first checks if the column is unique, if it is, it fetches a + # It first checks if the column is unique, if it is, it fetches a # set of unique values to insert self.existing_values = self.get_unique_column_values( column=column, unique_columns=unique_columns, table=table @@ -451,19 +522,23 @@ def get_value(self, column, foreign_columns, unique_columns, table): # it calls the `process_foreign` # function to check if the column is a foreign key # if it is, it returns a value from the related table - value = self.process_foreign( - column=column, - foreign_columns=foreign_columns, - table=table, - ) - if value is not None: + if None is not ( + value := self.process_foreign( + column=column, + foreign_columns=foreign_columns, + table=table, + ) + ): return value # if the column is not a foreign key, it calls the `handle_column_population` # function to populate the column with a value based on the definition from # the `data.py` file - value = self.handle_column_population(table=table, column=column) - if value is not None: + elif None is not ( + value := self.handle_column_population(table=table, column=column) + ): return value + elif column.nullable: + return None else: raise NotImplementedError( "I have no idea what value to assign " @@ -475,7 +550,7 @@ def get_related_table_fields(self, column, foreign_columns): """ The function `get_related_table_fields` returns a set of values from a related table """ - # desc is a tuple containing the + # desc is a tuple containing the # (name of the column, the name of the related table) desc = foreign_columns[column.name] # If the related table fields have already been cached, return them @@ -577,10 +652,10 @@ def fill_table(self, inspector): # Update the table panel with the current table being filled's name self.handle_table_panel(self.inheritance_relations_list) - + # Call the `handle_database_insertion` function to fill the current table self.handle_database_insertion(table_name, inspector) - + # Logic for how to display the table after it has been filled self.inheritance_relations_list.remove(f"[yellow]{table_name}") self.completed_tables_list.append(f"[green]{table_name}") From 483b88d37849a43aa0ce84a6fd10d5aa9ccc1933 Mon Sep 17 00:00:00 2001 From: Muhammed Zafar Date: Thu, 16 Nov 2023 22:37:00 +0530 Subject: [PATCH 2/6] [FEAT] Checkpoint #1 --- src/populate.py | 113 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/src/populate.py b/src/populate.py index 0fc2a44..5bde2eb 100644 --- a/src/populate.py +++ b/src/populate.py @@ -19,6 +19,7 @@ from rich import print import keyboard + class DatabasePopulator: """ The `DatabasePopulator` class is used to populate a database with random data. It uses the SQLAlchemy library to @@ -320,7 +321,6 @@ def arrange_graph(self, tables_to_fill): topologically. It involves the user in resolving circular dependencies. """ graph = nx.DiGraph() - # Populate the graph for table, inherited_tables in self.inheritance_relations.items(): if inherited_tables: @@ -337,9 +337,24 @@ def arrange_graph(self, tables_to_fill): ordered_tables = list(nx.topological_sort(graph)) except nx.NetworkXUnfeasible as e: cycles = list(nx.simple_cycles(graph)) - ordered_tables = self.resolve_cycles_with_user_input( - cycles=cycles, tables_to_fill=tables_to_fill - ) + ordered_cycles = self.resolve_cycles_with_user_input(cycles=cycles) + + graph = nx.DiGraph() + + # Populate the graph + for table, inherited_tables in self.inheritance_relations.items(): + if inherited_tables: + for inherited_table in inherited_tables: + if table != inherited_table: # Skip self-references + if ordered_cycles.index(table) < ordered_cycles.index( + inherited_table + ): + graph.add_edge(inherited_table, table) + else: + graph.add_node(table) + + self.job_progress.advance(self.identifying_relations) + ordered_tables = list(nx.topological_sort(graph)) # Order the tables based on topological sort or user resolution ordered_inheritance_relations = OrderedDict() @@ -352,12 +367,43 @@ def arrange_graph(self, tables_to_fill): self.inheritance_relations = ordered_inheritance_relations self.job_progress.advance(self.identifying_relations) - def resolve_cycles_with_user_input(self, cycles, tables_to_fill): + def resolve_cycles_with_user_input(self, cycles): """ Ask the user to resolve the detected cycles and return the adjusted order of tables. """ - indexed_list = self.get_indexed_list(cycles) + current_index = 0 + cycles = list({item for cycle in cycles for item in cycle}) + + self.generate_circular_dependency_list(cycles, current_index) + while True: + key = keyboard.read_event(suppress=False) + if key.event_type == "down": + key = key.name + if key == "up": + if current_index > 0: + current_index -= 1 + self.generate_circular_dependency_list(cycles, current_index) + elif key == "down": + if current_index < len(cycles) - 1: + current_index += 1 + self.generate_circular_dependency_list(cycles, current_index) + elif key in ["+", "="]: + cycles, current_index = self.update_list_based_on_user_input( + cycles, 1, current_index + ) + elif key in ["-", "_"]: + cycles, current_index = self.update_list_based_on_user_input( + cycles, -1, current_index + ) + elif key == "enter": + # Handle 'Enter' key press (e.g., save the selected order) + return cycles + elif key == "esc": + # Handle 'Esc' key press (e.g., exit or cancel) + break + + def display_list(self, indexed_list): self.layout["body"].update( Panel( Align.center("\n".join(indexed_list)), @@ -367,48 +413,59 @@ def resolve_cycles_with_user_input(self, cycles, tables_to_fill): title="[yellow b]WARNING", ) ) - - - - # Implement user interaction here to resolve cycles - # This could be as simple as asking the user to provide a new order for the tables - # or more complex logic based on your application's needs. - # For simplicity, let's assume user provides a new order of tables - user_ordered_tables = input( - "Please provide a new order of tables (comma-separated): " - ) - return user_ordered_tables.split(",") + def update_list_based_on_user_input(self, cycles, step, current_index): + if step == -1: + if (current_index + 1) < len(cycles): + cycles[current_index], cycles[current_index + 1] = ( + cycles[current_index + 1], + cycles[current_index], + ) + current_index += 1 + elif step == 1: + if current_index > 0: + cycles[current_index], cycles[current_index - 1] = ( + cycles[current_index - 1], + cycles[current_index], + ) + current_index -= 1 + self.generate_circular_dependency_list(cycles, current_index) + return cycles, current_index - def generate_circular_dependency_list(self, cycles): + def generate_circular_dependency_list(self, cycles, current_index): """ Generate a formatted list of circular dependencies for user interaction. Args: cycles (list): A list of lists, where each inner list represents a cycle of dependencies. + and prints a formatted list with circular dependencies and instructions. Returns: - list: A formatted list with circular dependencies and instructions. + None """ - # Find the table with the most circular dependencies - cyclic_tables = list(max(cycles, key=len)) + cycles = cycles.copy() # Highlight the first table - cyclic_tables[0] = f"[black on white]{cyclic_tables[0]}[/]" + cycles[current_index] = f"[black on white]{cycles[current_index]}[/]" # Create the indexed list with proper formatting indexed_list = [ - f"{index}. {item}" for index, item in enumerate(cyclic_tables, start=1) + f"{index}. {item}" for index, item in enumerate(cycles, start=1) ] # Insert headers and instructions - indexed_list.insert(0, "[b red]Circular dependencies detected in the following tables,") + indexed_list.insert( + 0, "[b red]Circular dependencies detected in the following tables," + ) indexed_list.insert(1, "provide an order to fill:[/b red]\n") - indexed_list.insert(2, "[i yellow]Use arrow keys to navigate, `+` to move an item up the list,") - indexed_list.insert(3, "`-` to move an item down the list, and `Enter` to save.[/i yellow]\n\n") - - return indexed_list + indexed_list.insert( + 2, "[i yellow]Use arrow keys to navigate, `+` to move an item up the list," + ) + indexed_list.insert( + 3, "`-` to move an item down the list, and `Enter` to save.[/i yellow]\n\n" + ) + self.display_list(indexed_list) def populate_fields(self, column, table): """ From c33b9ebea725e6bef2097dd31c3bd5122223c6f1 Mon Sep 17 00:00:00 2001 From: Muhammed Zafar Date: Thu, 16 Nov 2023 22:52:27 +0530 Subject: [PATCH 3/6] [PATCH] Support cyclic inheritance --- data.py | 13 ++++++++++++- src/populate.py | 17 +++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/data.py b/data.py index 9a032a7..3b97067 100644 --- a/data.py +++ b/data.py @@ -81,7 +81,18 @@ "table": None, "generator": lambda: fake.word().capitalize(), }, - {"name": None, "type": "date", "table": None, "generator": lambda: fake.date()}, + { + "name": None, + "type": "float", + "table": None, + "generator": lambda: fake.random_element(elements=(1.0, 10.0)), + }, + { + "name": None, + "type": "date", + "table": None, + "generator": lambda: fake.date(), + }, { "name": None, "type": "datetime", diff --git a/src/populate.py b/src/populate.py index 5bde2eb..9658a76 100644 --- a/src/populate.py +++ b/src/populate.py @@ -88,7 +88,7 @@ def __init__( ) # Arranges inheritance relations in a directed graph - self.arrange_graph(tables_to_fill=tables_to_fill) + self.arrange_graph() # Populates the database with random data self.fill_table(inspector=inspector) @@ -315,7 +315,7 @@ def draw_graph(self): plt.axis("off") plt.show() - def arrange_graph(self, tables_to_fill): + def arrange_graph(self): """ The function arranges identified inheritance relations in a directed graph and orders them topologically. It involves the user in resolving circular dependencies. @@ -346,10 +346,14 @@ def arrange_graph(self, tables_to_fill): if inherited_tables: for inherited_table in inherited_tables: if table != inherited_table: # Skip self-references - if ordered_cycles.index(table) < ordered_cycles.index( - inherited_table + if ( + table in ordered_cycles + and inherited_table in ordered_cycles + and ordered_cycles.index(table) + > ordered_cycles.index(inherited_table) ): - graph.add_edge(inherited_table, table) + continue + graph.add_edge(inherited_table, table) else: graph.add_node(table) @@ -599,7 +603,8 @@ def get_value(self, column, foreign_columns, unique_columns, table): else: raise NotImplementedError( "I have no idea what value to assign " - f"to the field '{column.name}' in '{table}'. " + f"to the field '{column.name}' of type ", + f"{column.type} in '{table}'. " "Maybe updating my `data.py` will help?" ) From e1194bf894228bab55e822106668cb2b58ee0e7f Mon Sep 17 00:00:00 2001 From: Muhammed Zafar Date: Sat, 18 Nov 2023 00:07:14 +0530 Subject: [PATCH 4/6] [PATCH] Resolved cyclic dependencies errors --- requirements.txt | 27 +++++++++++++++++++++-- src/populate.py | 56 ++++++++++++++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/requirements.txt b/requirements.txt index ef3ccc1..2225ea6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,31 @@ +black==23.7.0 +click==8.1.7 +colorama==0.4.6 +contourpy==1.1.0 +cycler==0.11.0 Faker==18.9.0 +fonttools==4.42.0 +greenlet==2.0.2 +keyboard==0.13.5 +kiwisolver==1.4.4 +markdown-it-py==3.0.0 matplotlib==3.7.2 +mdurl==0.1.2 +mypy-extensions==1.0.0 +mysql-connector-python==8.1.0 networkx==3.1 +numpy==1.25.2 +packaging==23.1 +pathspec==0.11.2 +Pillow==10.0.0 +platformdirs==3.10.0 +protobuf==4.21.12 +Pygments==2.16.1 +pyparsing==3.0.9 +python-dateutil==2.8.2 python-decouple==3.8 rich==13.5.2 +six==1.16.0 SQLAlchemy==2.0.20 -mysql-connector-python==8.1.0 -SQLAlchemy-Utils==0.41.1 \ No newline at end of file +SQLAlchemy-Utils==0.41.1 +typing_extensions==4.7.1 diff --git a/src/populate.py b/src/populate.py index 9658a76..99bd37f 100644 --- a/src/populate.py +++ b/src/populate.py @@ -1,9 +1,10 @@ import contextlib import random import re -from collections import OrderedDict import time +from collections import OrderedDict +import keyboard import matplotlib.pyplot as plt import networkx as nx import sqlalchemy @@ -16,8 +17,10 @@ from rich.table import Table from sqlalchemy import create_engine, inspect from sqlalchemy_utils import has_unique_index -from rich import print -import keyboard + +from .enums import Nothing + +Nan = Nothing.Nan.value class DatabasePopulator: @@ -322,6 +325,7 @@ def arrange_graph(self): """ graph = nx.DiGraph() # Populate the graph + h = self.inheritance_relations.items() for table, inherited_tables in self.inheritance_relations.items(): if inherited_tables: for inherited_table in inherited_tables: @@ -340,7 +344,6 @@ def arrange_graph(self): ordered_cycles = self.resolve_cycles_with_user_input(cycles=cycles) graph = nx.DiGraph() - # Populate the graph for table, inherited_tables in self.inheritance_relations.items(): if inherited_tables: @@ -350,14 +353,13 @@ def arrange_graph(self): table in ordered_cycles and inherited_table in ordered_cycles and ordered_cycles.index(table) - > ordered_cycles.index(inherited_table) + < ordered_cycles.index(inherited_table) ): continue graph.add_edge(inherited_table, table) else: graph.add_node(table) - self.job_progress.advance(self.identifying_relations) ordered_tables = list(nx.topological_sort(graph)) # Order the tables based on topological sort or user resolution @@ -425,14 +427,14 @@ def update_list_based_on_user_input(self, cycles, step, current_index): cycles[current_index + 1], cycles[current_index], ) - current_index += 1 + current_index += 1 elif step == 1: if current_index > 0: cycles[current_index], cycles[current_index - 1] = ( cycles[current_index - 1], cycles[current_index], ) - current_index -= 1 + current_index -= 1 self.generate_circular_dependency_list(cycles, current_index) return cycles, current_index @@ -459,14 +461,12 @@ def generate_circular_dependency_list(self, cycles, current_index): # Insert headers and instructions indexed_list.insert( - 0, "[b red]Circular dependencies detected in the following tables," + 0, + "[b red]Circular dependencies detected in the following tables, provide an order to fill:[/b red]\n", ) - indexed_list.insert(1, "provide an order to fill:[/b red]\n") indexed_list.insert( - 2, "[i yellow]Use arrow keys to navigate, `+` to move an item up the list," - ) - indexed_list.insert( - 3, "`-` to move an item down the list, and `Enter` to save.[/i yellow]\n\n" + 1, + "[i yellow]Use arrow keys to navigate, `+` to move an item up the list, `-` to move an item down the list, and `Enter` to save.[/i yellow]\n\n", ) self.display_list(indexed_list) @@ -538,6 +538,8 @@ def handle_column_population(self, table, column): value = self.populate_fields(column, table) count -= 1 if count <= 0: + if column.nullable: + return None raise ValueError( ( f"I can't find a unique value " @@ -575,15 +577,20 @@ def get_value(self, column, foreign_columns, unique_columns, table): """ The function `get_value` returns a value for a column in a table. """ + # Check if the column is nullable with a 1 in 300 chance of returning None + if column.nullable and random.random() < 1 / 300: + return None + # It first checks if the column is unique, if it is, it fetches a # set of unique values to insert + self.existing_values = self.get_unique_column_values( column=column, unique_columns=unique_columns, table=table ) # it calls the `process_foreign` # function to check if the column is a foreign key # if it is, it returns a value from the related table - if None is not ( + if Nan is not ( value := self.process_foreign( column=column, foreign_columns=foreign_columns, @@ -594,18 +601,16 @@ def get_value(self, column, foreign_columns, unique_columns, table): # if the column is not a foreign key, it calls the `handle_column_population` # function to populate the column with a value based on the definition from # the `data.py` file - elif None is not ( + elif Nan is not ( value := self.handle_column_population(table=table, column=column) ): return value - elif column.nullable: - return None else: raise NotImplementedError( "I have no idea what value to assign " f"to the field '{column.name}' of type ", f"{column.type} in '{table}'. " - "Maybe updating my `data.py` will help?" + "Maybe updating my `data.py` will help?", ) def get_related_table_fields(self, column, foreign_columns): @@ -641,17 +646,22 @@ def process_foreign(self, foreign_columns, table, column): it returns a value from the related table. """ if column.name not in foreign_columns: - return None + return Nan # Gets the related table fields from the `get_related_table_fields` function related_table_fields = self.get_related_table_fields(column, foreign_columns) # self.existing_values only gets populated if the column only accepts to unique values if selectable_fields := related_table_fields - self.existing_values: return random.choice(list(selectable_fields)) - else: - raise ValueError( - f"Can't find a unique value to insert into column '{column.name}' in table '{table.name}'" + elif column.nullable: + return None + raise ValueError( + ( + f"I can't find a unique value " + f"to insert into column '{column.name}' in " + f"table '{table.name}'" ) + ) def get_unique_columns(self, table): return [column.name for column in table.columns if has_unique_index(column)] From c375234c3fc492aa33a8bb02bce9e1a161822d0b Mon Sep 17 00:00:00 2001 From: Muhammed Zafar Date: Sat, 18 Nov 2023 00:29:55 +0530 Subject: [PATCH 5/6] [FEAT] enums created --- src/enums.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/enums.py diff --git a/src/enums.py b/src/enums.py new file mode 100644 index 0000000..d36f30e --- /dev/null +++ b/src/enums.py @@ -0,0 +1,4 @@ +import enum + +class Nothing(enum.Enum): + Nan = "This operation returned nothing" From 1816f490c6bb6cd81ee31ef20935d0664c822637 Mon Sep 17 00:00:00 2001 From: Muhammed Zafar Date: Wed, 22 Nov 2023 01:13:29 +0530 Subject: [PATCH 6/6] [PATCH] Cyclic issues resolved --- src/enums.py | 2 +- src/populate.py | 161 +++++++++--------------------------------------- 2 files changed, 31 insertions(+), 132 deletions(-) diff --git a/src/enums.py b/src/enums.py index d36f30e..d895e5a 100644 --- a/src/enums.py +++ b/src/enums.py @@ -1,4 +1,4 @@ import enum class Nothing(enum.Enum): - Nan = "This operation returned nothing" + Nada = "This operation returned nothing" diff --git a/src/populate.py b/src/populate.py index 99bd37f..65d6852 100644 --- a/src/populate.py +++ b/src/populate.py @@ -20,7 +20,7 @@ from .enums import Nothing -Nan = Nothing.Nan.value +Nada = Nothing.Nada.value class DatabasePopulator: @@ -317,6 +317,20 @@ def draw_graph(self): plt.title("Database Inheritance Relationships") plt.axis("off") plt.show() + + def remove_cycles(self, graph): + try: + # Find a cycle in the graph + cycle = nx.find_cycle(graph, orientation='original') + except nx.NetworkXNoCycle: + # No cycle found, return the graph as is + return graph + + # If a cycle is found, remove an edge from the cycle + graph.remove_edge(*cycle[0][:2]) + + # Recursively call remove_cycles to remove other cycles + return self.remove_cycles(graph) def arrange_graph(self): """ @@ -325,44 +339,27 @@ def arrange_graph(self): """ graph = nx.DiGraph() # Populate the graph - h = self.inheritance_relations.items() for table, inherited_tables in self.inheritance_relations.items(): if inherited_tables: for inherited_table in inherited_tables: if table != inherited_table: # Skip self-references - graph.add_edge(inherited_table, table) + adjacency = dict(graph.adjacency()) + if table in adjacency: + adjacency = adjacency[table] + if inherited_table not in adjacency: + graph.add_edge(inherited_table, table) + else: + graph.add_edge(inherited_table, table) + else: graph.add_node(table) self.job_progress.advance(self.identifying_relations) - # Detect cycles - try: - ordered_tables = list(nx.topological_sort(graph)) - except nx.NetworkXUnfeasible as e: - cycles = list(nx.simple_cycles(graph)) - ordered_cycles = self.resolve_cycles_with_user_input(cycles=cycles) - - graph = nx.DiGraph() - # Populate the graph - for table, inherited_tables in self.inheritance_relations.items(): - if inherited_tables: - for inherited_table in inherited_tables: - if table != inherited_table: # Skip self-references - if ( - table in ordered_cycles - and inherited_table in ordered_cycles - and ordered_cycles.index(table) - < ordered_cycles.index(inherited_table) - ): - continue - graph.add_edge(inherited_table, table) - else: - graph.add_node(table) - - ordered_tables = list(nx.topological_sort(graph)) - - # Order the tables based on topological sort or user resolution + graph = self.remove_cycles(graph) + ordered_tables = list(nx.topological_sort(graph)) + + # Order the tables based on topological sort ordered_inheritance_relations = OrderedDict() for table in ordered_tables: if table in self.inheritance_relations: @@ -373,104 +370,6 @@ def arrange_graph(self): self.inheritance_relations = ordered_inheritance_relations self.job_progress.advance(self.identifying_relations) - def resolve_cycles_with_user_input(self, cycles): - """ - Ask the user to resolve the detected cycles and return the adjusted order of tables. - """ - current_index = 0 - cycles = list({item for cycle in cycles for item in cycle}) - - self.generate_circular_dependency_list(cycles, current_index) - while True: - key = keyboard.read_event(suppress=False) - if key.event_type == "down": - key = key.name - if key == "up": - if current_index > 0: - current_index -= 1 - self.generate_circular_dependency_list(cycles, current_index) - elif key == "down": - if current_index < len(cycles) - 1: - current_index += 1 - self.generate_circular_dependency_list(cycles, current_index) - elif key in ["+", "="]: - cycles, current_index = self.update_list_based_on_user_input( - cycles, 1, current_index - ) - elif key in ["-", "_"]: - cycles, current_index = self.update_list_based_on_user_input( - cycles, -1, current_index - ) - - elif key == "enter": - # Handle 'Enter' key press (e.g., save the selected order) - return cycles - elif key == "esc": - # Handle 'Esc' key press (e.g., exit or cancel) - break - - def display_list(self, indexed_list): - self.layout["body"].update( - Panel( - Align.center("\n".join(indexed_list)), - highlight=True, - padding=1, - expand=True, - title="[yellow b]WARNING", - ) - ) - - def update_list_based_on_user_input(self, cycles, step, current_index): - if step == -1: - if (current_index + 1) < len(cycles): - cycles[current_index], cycles[current_index + 1] = ( - cycles[current_index + 1], - cycles[current_index], - ) - current_index += 1 - elif step == 1: - if current_index > 0: - cycles[current_index], cycles[current_index - 1] = ( - cycles[current_index - 1], - cycles[current_index], - ) - current_index -= 1 - self.generate_circular_dependency_list(cycles, current_index) - return cycles, current_index - - def generate_circular_dependency_list(self, cycles, current_index): - """ - Generate a formatted list of circular dependencies for user interaction. - - Args: - cycles (list): A list of lists, where each inner list represents a cycle of dependencies. - and prints a formatted list with circular dependencies and instructions. - - Returns: - None - """ - cycles = cycles.copy() - - # Highlight the first table - cycles[current_index] = f"[black on white]{cycles[current_index]}[/]" - - # Create the indexed list with proper formatting - indexed_list = [ - f"{index}. {item}" for index, item in enumerate(cycles, start=1) - ] - - # Insert headers and instructions - indexed_list.insert( - 0, - "[b red]Circular dependencies detected in the following tables, provide an order to fill:[/b red]\n", - ) - indexed_list.insert( - 1, - "[i yellow]Use arrow keys to navigate, `+` to move an item up the list, `-` to move an item down the list, and `Enter` to save.[/i yellow]\n\n", - ) - - self.display_list(indexed_list) - def populate_fields(self, column, table): """ The function `populate_fields` populates a @@ -590,7 +489,7 @@ def get_value(self, column, foreign_columns, unique_columns, table): # it calls the `process_foreign` # function to check if the column is a foreign key # if it is, it returns a value from the related table - if Nan is not ( + if Nada is not ( value := self.process_foreign( column=column, foreign_columns=foreign_columns, @@ -601,7 +500,7 @@ def get_value(self, column, foreign_columns, unique_columns, table): # if the column is not a foreign key, it calls the `handle_column_population` # function to populate the column with a value based on the definition from # the `data.py` file - elif Nan is not ( + elif Nada is not ( value := self.handle_column_population(table=table, column=column) ): return value @@ -646,7 +545,7 @@ def process_foreign(self, foreign_columns, table, column): it returns a value from the related table. """ if column.name not in foreign_columns: - return Nan + return Nada # Gets the related table fields from the `get_related_table_fields` function related_table_fields = self.get_related_table_fields(column, foreign_columns)