diff --git a/learn_sql_model/cli/model.py b/learn_sql_model/cli/model.py index eb5ea67..8937ef3 100644 --- a/learn_sql_model/cli/model.py +++ b/learn_sql_model/cli/model.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Annotated import alembic from alembic.config import Config @@ -6,6 +7,7 @@ import copier import typer from learn_sql_model.cli.common import verbose_callback +from learn_sql_model.config import get_config model_app = typer.Typer() @@ -40,11 +42,18 @@ def create_revision( callback=verbose_callback, help="show the log messages", ), - message: str = typer.Option( - prompt=True, - ), + message: Annotated[ + str, + typer.Option( + "--message", + "-m", + prompt=True, + ), + ] = None, ): alembic_cfg = Config("alembic.ini") + config = get_config() + alembic_cfg.set_main_option("sqlalchemy.url", config.database_url) alembic.command.revision( config=alembic_cfg, message=message, @@ -63,7 +72,17 @@ def checkout( revision: str = typer.Option("head"), ): alembic_cfg = Config("alembic.ini") - alembic.command.upgrade(config=alembic_cfg, revision="head") + config = get_config() + alembic_cfg.set_main_option("sqlalchemy.url", config.database_url) + alembic.command.upgrade(config=alembic_cfg, revision=revision) + + +@model_app.command() +def status(): + alembic_cfg = Config("alembic.ini") + config = get_config() + alembic_cfg.set_main_option("sqlalchemy.url", config.database_url) + alembic.command.current(config=alembic_cfg) @model_app.command() diff --git a/learn_sql_model/er_diagram.py b/learn_sql_model/er_diagram.py new file mode 100644 index 0000000..4a48fa5 --- /dev/null +++ b/learn_sql_model/er_diagram.py @@ -0,0 +1,151 @@ +import sqlite3 + +from graphviz import Digraph + +from learn_sql_model.config import get_config + +config = get_config() + + +def generate_er_diagram(output_path): + # Connect to the SQLite database + database_path = config.database_url.replace("sqlite:///", "") + conn = sqlite3.connect(database_path) + cursor = conn.cursor() + + # Get the table names from the database + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + + # Create a new Digraph + dot = Digraph(format="png") + dot.attr(rankdir="TD") + + # Iterate over the tables + for table in tables: + table_name = table[0] + dot.node(table_name, shape="box") + cursor.execute(f"PRAGMA table_info({table_name});") + columns = cursor.fetchall() + + # Add the columns to the table node + for column in columns: + column_name = column[1] + dot.node(f"{table_name}.{column_name}", label=column_name, shape="oval") + dot.edge(table_name, f"{table_name}.{column_name}") + + # Check for foreign key relationships + cursor.execute(f"PRAGMA foreign_key_list({table_name});") + foreign_keys = cursor.fetchall() + + # Add dotted lines for foreign key relationships + for foreign_key in foreign_keys: + from_column = foreign_key[3] + to_table = foreign_key[2] + to_column = foreign_key[4] + dot.node(f"{to_table}.{to_column}", shape="oval") + dot.edge( + f"{table_name}.{from_column}", f"{to_table}.{to_column}", style="dotted" + ) + + # Render and save the diagram + dot.render(output_path.replace(".png", ""), cleanup=True) + + # Close the database connection + cursor.close() + conn.close() + + +def generate_er_markdown(output_path, er_diagram_path): + # Connect to the SQLite database + database_path = config.database_url.replace("sqlite:///", "") + conn = sqlite3.connect(database_path) + cursor = conn.cursor() + + # Get the table names from the database + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + + with open(output_path, "w") as f: + # Write the ER Diagram image + f.write(f"![ER Diagram]({er_diagram_path})\n\n---\n\n") + + # Iterate over the tables + for table in tables: + table_name = table[0] + + f.write(f"## Table: {table_name}\n\n") + + # Get the table columns + cursor.execute(f"PRAGMA table_info({table_name});") + columns = cursor.fetchall() + + f.write("### First 5 rows\n\n") + cursor.execute(f"SELECT * FROM {table_name} LIMIT 5;") + rows = cursor.fetchall() + f.write(f'| {" | ".join([c[1] for c in columns])} |\n') + f.write("|") + for column in columns: + # --- + f.write(f'{"-"*(len(column[1]) + 2)}|') + f.write("\n") + for row in rows: + f.write(f'| {" | ".join([str(r) for r in row])} |\n') + f.write("\n") + + cursor.execute(f"PRAGMA foreign_key_list({table_name});") + foreign_keys = cursor.fetchall() + + # Add dotted lines for foreign key relationships + fkeys = {} + for foreign_key in foreign_keys: + from_column = foreign_key[3] + to_table = foreign_key[2] + to_column = foreign_key[4] + fkeys[from_column] = f"{to_table}.{to_column}" + + # Replace 'description' with the actual column name in the table that contains the description, if applicable + try: + cursor.execute(f"SELECT description FROM {table_name} LIMIT 1;") + description = cursor.fetchone() + if description: + f.write(f"### Description\n\n{description[0]}\n\n") + except: + ... + + # Write the table columns + f.write("### Columns\n\n") + f.write("| Column Name | Type | Foreign Key | Example Value |\n") + f.write("|-------------|------|-------------|---------------|\n") + + for column in columns: + + column_name = column[1] + column_type = column[2] + fkey = "" + if column_name in fkeys: + fkey = fkeys[column_name] + f.write(f"| {column_name} | {column_type} | {fkey} | | |\n") + + f.write("\n") + + # Get the count of records + cursor.execute(f"SELECT COUNT(*) FROM {table_name};") + records_count = cursor.fetchone()[0] + f.write( + f"### Records Count\n\nThe table {table_name} contains {records_count} records.\n\n---\n\n" + ) + + # Close the database connection + cursor.close() + conn.close() + + +if __name__ == "__main__": + # Usage example + database_path = "database.db" + md_output_path = "database.md" + er_output_path = "er_diagram.png" + + generate_er_diagram(database_path, er_output_path) + generate_markdown(database_path, md_output_path, er_output_path) diff --git a/learn_sql_model/models/hero.py b/learn_sql_model/models/hero.py index 14e17b1..5d40c07 100644 --- a/learn_sql_model/models/hero.py +++ b/learn_sql_model/models/hero.py @@ -2,10 +2,9 @@ from typing import Dict, Optional import httpx from pydantic import BaseModel -from sqlmodel import Field, Relationship, SQLModel +from sqlmodel import Field, SQLModel from learn_sql_model.config import config -from learn_sql_model.models.pet import Pet class HeroBase(SQLModel, table=False): @@ -13,12 +12,12 @@ class HeroBase(SQLModel, table=False): secret_name: str x: int y: int - size: int - age: Optional[int] = None - shoe_size: Optional[int] = None + # size: int + # age: Optional[int] = None + # shoe_size: Optional[int] = None - pet_id: Optional[int] = Field(default=None, foreign_key="pet.id") - pet: Optional[Pet] = Relationship(back_populates="hero") + # pet_id: Optional[int] = Field(default=None, foreign_key="pet.id") + # pet: Optional[Pet] = Relationship(back_populates="hero") class Hero(HeroBase, table=True): @@ -73,13 +72,13 @@ class HeroUpdate(SQLModel): # all other fields, must match the model, but with Optional default None name: Optional[str] = None secret_name: Optional[str] = None - age: Optional[int] = None - shoe_size: Optional[int] = None - x: Optional[int] - y: Optional[int] + # age: Optional[int] = None + # shoe_size: Optional[int] = None + # x: Optional[int] + # y: Optional[int] - pet_id: Optional[int] = Field(default=None, foreign_key="pet.id") - pet: Optional[Pet] = Relationship(back_populates="hero") + # pet_id: Optional[int] = Field(default=None, foreign_key="pet.id") + # pet: Optional[Pet] = Relationship(back_populates="hero") def update(self) -> Hero: r = httpx.patch( diff --git a/migrations/script.py.mako b/migrations/script.py.mako index 3124b62..567e915 100644 --- a/migrations/script.py.mako +++ b/migrations/script.py.mako @@ -8,6 +8,9 @@ Create Date: ${create_date} from alembic import op import sqlalchemy as sa import sqlmodel +from learn_sql_model.er_diagram import generate_er_diagram, generate_er_markdown +from learn_sql_model.config import get_config + ${imports if imports else ""} # revision identifiers, used by Alembic. @@ -19,6 +22,8 @@ depends_on = ${repr(depends_on)} def upgrade() -> None: ${upgrades if upgrades else "pass"} + generate_er_diagram(f'migrations/versions/{revision}_er_diagram.png') + generate_er_markdown(f'migrations/versions/{revision}_er_diagram.md', f'migrations/versions/er_diagram_{revision}.png') def downgrade() -> None: diff --git a/migrations/versions/e26398d96dd0_add_x_y_size.py b/migrations/versions/3555f61aaa79_add_x_and_y.py similarity index 55% rename from migrations/versions/e26398d96dd0_add_x_y_size.py rename to migrations/versions/3555f61aaa79_add_x_and_y.py index 4219bde..0ff42c5 100644 --- a/migrations/versions/e26398d96dd0_add_x_y_size.py +++ b/migrations/versions/3555f61aaa79_add_x_and_y.py @@ -1,18 +1,21 @@ -"""add x, y, size +"""add x and y -Revision ID: e26398d96dd0 -Revises: a9bb6625c57b -Create Date: 2023-06-10 18:37:04.751553 +Revision ID: 3555f61aaa79 +Revises: 79972ec5f79d +Create Date: 2023-06-22 15:03:27.338959 """ from alembic import op import sqlalchemy as sa import sqlmodel +from learn_sql_model.er_diagram import generate_er_diagram, generate_er_markdown +from learn_sql_model.config import get_config + # revision identifiers, used by Alembic. -revision = 'e26398d96dd0' -down_revision = 'a9bb6625c57b' +revision = '3555f61aaa79' +down_revision = '79972ec5f79d' branch_labels = None depends_on = None @@ -21,13 +24,13 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.add_column('hero', sa.Column('x', sa.Integer(), nullable=False)) op.add_column('hero', sa.Column('y', sa.Integer(), nullable=False)) - op.add_column('hero', sa.Column('size', sa.Integer(), nullable=False)) # ### end Alembic commands ### + generate_er_diagram(f'migrations/versions/{revision}_er_diagram.png') + generate_er_markdown(f'migrations/versions/{revision}_er_diagram.md', f'migrations/versions/er_diagram_{revision}.png') def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('hero', 'size') op.drop_column('hero', 'y') op.drop_column('hero', 'x') # ### end Alembic commands ### diff --git a/migrations/versions/3555f61aaa79_er_diagram.md b/migrations/versions/3555f61aaa79_er_diagram.md new file mode 100644 index 0000000..caf6156 --- /dev/null +++ b/migrations/versions/3555f61aaa79_er_diagram.md @@ -0,0 +1,68 @@ +![ER Diagram](migrations/versions/er_diagram_3555f61aaa79.png) + +--- + +## Table: learn_sql_model_alembic_version + +### First 5 rows + +| version_num | +|-------------| +| 79972ec5f79d | + +### Columns + +| Column Name | Type | Foreign Key | Example Value | +|-------------|------|-------------|---------------| +| version_num | VARCHAR(32) | | | | + +### Records Count + +The table learn_sql_model_alembic_version contains 1 records. + +--- + +## Table: hero + +### First 5 rows + +| name | secret_name | id | x | y | +|------|-------------|----|---|---| + +### Columns + +| Column Name | Type | Foreign Key | Example Value | +|-------------|------|-------------|---------------| +| name | VARCHAR | | | | +| secret_name | VARCHAR | | | | +| id | INTEGER | | | | +| x | INTEGER | | | | +| y | INTEGER | | | | + +### Records Count + +The table hero contains 0 records. + +--- + +## Table: pet + +### First 5 rows + +| name | birthday | id | +|------|----------|----| + +### Columns + +| Column Name | Type | Foreign Key | Example Value | +|-------------|------|-------------|---------------| +| name | VARCHAR | | | | +| birthday | DATETIME | | | | +| id | INTEGER | | | | + +### Records Count + +The table pet contains 0 records. + +--- + diff --git a/migrations/versions/3555f61aaa79_er_diagram.png b/migrations/versions/3555f61aaa79_er_diagram.png new file mode 100644 index 0000000..7c6bdab Binary files /dev/null and b/migrations/versions/3555f61aaa79_er_diagram.png differ diff --git a/migrations/versions/79972ec5f79d_er_diagram.md b/migrations/versions/79972ec5f79d_er_diagram.md new file mode 100644 index 0000000..9f3ece5 --- /dev/null +++ b/migrations/versions/79972ec5f79d_er_diagram.md @@ -0,0 +1,65 @@ +![ER Diagram](migrations/versions/er_diagram_79972ec5f79d.png) + +--- + +## Table: learn_sql_model_alembic_version + +### First 5 rows + +| version_num | +|-------------| + +### Columns + +| Column Name | Type | Foreign Key | Example Value | +|-------------|------|-------------|---------------| +| version_num | VARCHAR(32) | | | | + +### Records Count + +The table learn_sql_model_alembic_version contains 0 records. + +--- + +## Table: hero + +### First 5 rows + +| name | secret_name | id | +|------|-------------|----| + +### Columns + +| Column Name | Type | Foreign Key | Example Value | +|-------------|------|-------------|---------------| +| name | VARCHAR | | | | +| secret_name | VARCHAR | | | | +| id | INTEGER | | | | + +### Records Count + +The table hero contains 0 records. + +--- + +## Table: pet + +### First 5 rows + +| name | birthday | id | +|------|----------|----| + +### Columns + +| Column Name | Type | Foreign Key | Example Value | +|-------------|------|-------------|---------------| +| name | VARCHAR | | | | +| birthday | DATETIME | | | | +| id | INTEGER | | | | + +### Records Count + +The table pet contains 0 records. + +--- + diff --git a/migrations/versions/79972ec5f79d_er_diagram.png b/migrations/versions/79972ec5f79d_er_diagram.png new file mode 100644 index 0000000..cc4f37b Binary files /dev/null and b/migrations/versions/79972ec5f79d_er_diagram.png differ diff --git a/migrations/versions/c8516c888495_init.py b/migrations/versions/79972ec5f79d_int.py similarity index 66% rename from migrations/versions/c8516c888495_init.py rename to migrations/versions/79972ec5f79d_int.py index c351dd9..5fd59b7 100644 --- a/migrations/versions/c8516c888495_init.py +++ b/migrations/versions/79972ec5f79d_int.py @@ -1,17 +1,20 @@ -"""init +"""int -Revision ID: c8516c888495 +Revision ID: 79972ec5f79d Revises: -Create Date: 2023-05-25 18:42:37.057225 +Create Date: 2023-06-22 15:02:20.292322 """ from alembic import op import sqlalchemy as sa import sqlmodel +from learn_sql_model.er_diagram import generate_er_diagram, generate_er_markdown +from learn_sql_model.config import get_config + # revision identifiers, used by Alembic. -revision = 'c8516c888495' +revision = '79972ec5f79d' down_revision = None branch_labels = None depends_on = None @@ -19,26 +22,25 @@ depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('pet', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) op.create_table('hero', - sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column('secret_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('age', sa.Integer(), nullable=True), - sa.Column('shoe_size', sa.Integer(), nullable=True), - sa.Column('pet_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['pet_id'], ['pet.id'], ), + sa.Column('id', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('pet', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('birthday', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), sa.PrimaryKeyConstraint('id') ) # ### end Alembic commands ### + generate_er_diagram(f'migrations/versions/{revision}_er_diagram.png') + generate_er_markdown(f'migrations/versions/{revision}_er_diagram.md', f'migrations/versions/er_diagram_{revision}.png') def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('hero') op.drop_table('pet') + op.drop_table('hero') # ### end Alembic commands ### diff --git a/migrations/versions/a9bb6625c57b_add_birthday.py b/migrations/versions/a9bb6625c57b_add_birthday.py deleted file mode 100644 index 9e8feb4..0000000 --- a/migrations/versions/a9bb6625c57b_add_birthday.py +++ /dev/null @@ -1,29 +0,0 @@ -"""add birthday - -Revision ID: a9bb6625c57b -Revises: c8516c888495 -Create Date: 2023-05-25 19:00:58.137464 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel - - -# revision identifiers, used by Alembic. -revision = 'a9bb6625c57b' -down_revision = 'c8516c888495' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('pet', sa.Column('birthday', sa.DateTime(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('pet', 'birthday') - # ### end Alembic commands ###