Skip to content

Commit

Permalink
chore: Merge pull request #6 from arrai-innovations/multiple_latest_a…
Browse files Browse the repository at this point in the history
…nd_same_numbered_migrations

chore: Multiple latest and same numbered migrations
  • Loading branch information
jayden-arrai authored Aug 14, 2023
2 parents b8ffd5c + a2dc1f8 commit ff66689
Show file tree
Hide file tree
Showing 36 changed files with 864 additions and 17 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A management command for django, designed to provide a way in pull requests, to see a diff of the sql (`CREATE VIEW ...`) for unmanaged models.

Capable of handling migrations with the same number (refer to the musicians app and test) and partially capable of handing views that use other views. Views using views requires some manual additions to migrations, so it can drop required views and set them back up (refer to migration 0004 in the store app for how this can be accomplished). Once set up, it is capable of updating the sql view name in any migration that uses it.

The management command creates an `sql` folder inside an app, along with files like `view-animals_pets-latest.sql` (live) and `view-animals_pets-0002.sql` (historical), where you write your sql. Migrations are also created in the process, which read these files, so you don't need to create them yourself.

Refer to folder and file structure, and usage, for more detailed information.
Expand Down Expand Up @@ -30,6 +32,8 @@ Refer to folder and file structure, and usage, for more detailed information.

![Python%203.7%20-%20Django%203.2](https://docs.arrai-dev.com/django-view-manager/artifacts/main/python%203.11%20-%20django%204.1.svg) [![Coverage](https://docs.arrai-dev.com/django-view-manager/artifacts/main/python%203.11%20-%20django%204.1.coverage.svg)](https://docs.arrai-dev.com/django-view-manager/artifacts/main/htmlcov_python%203.11%20-%20django%204.1/)

**Table of Contents**

<!-- prettier-ignore-start -->
<!--TOC-->

Expand Down Expand Up @@ -112,8 +116,8 @@ The results will be:

```shell
$ python manage.py makeviewmigration [-h] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
                                   [--skip-checks]
                                   {animals_pets,employees_employeelikes,food_sweets} migration_name
                                     [--skip-checks]
                                     {animals_pets,employees_employeelikes,food_sweets} migration_name
manage.py makeviewmigration: error: the following arguments are required: db_table_name, migration_name
```

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.3
1.0.4
2 changes: 2 additions & 0 deletions config/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"tests.animals",
"tests.employees",
"tests.food",
"tests.musicians",
"tests.store",
"django_view_manager.utils",
]

Expand Down
82 changes: 70 additions & 12 deletions django_view_manager/utils/management/commands/makeviewmigration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.db.transaction import atomic


VERSION = "1.0.3"
VERSION = "1.0.4"


COPIED_SQL_VIEW_CONTENT = f"""/*
Expand Down Expand Up @@ -163,7 +163,7 @@ def _parse_migration_number_from_show_migrations(line):

@staticmethod
def _is_migration_modified(db_table_name, migrations_path, migration_name, num):
with open(os.path.join(migrations_path, f"{migration_name}.py"), "r") as f:
with open(os.path.join(migrations_path, f"{migration_name}.py"), "r", encoding="utf-8") as f:
# Did we modify this migration? Check the first 10 lines for our modified comment.
found_modified_comment = False
for migration_line_no, migration_line in enumerate(f.readlines()):
Expand Down Expand Up @@ -221,29 +221,32 @@ def _get_sql_numbers_and_names(sql_path, db_table_name):
if filename.startswith(view_name_start) and filename.endswith(view_name_end):
sql_file_num = filename.replace(view_name_start, "").replace(view_name_end, "")

sql_file_name = None
if "-" in sql_file_num:
sql_file_num, sql_file_name = sql_file_num.split("-")

# Convert the number to an int, so we can max them.
if sql_file_num == LATEST_VIEW_NAME:
sql_numbers_and_names[LATEST_VIEW_NUMBER] = filename
sql_numbers_and_names[(LATEST_VIEW_NUMBER, sql_file_name)] = filename

elif sql_file_num.isdigit():
sql_numbers_and_names[decimal.Decimal(sql_file_num)] = filename
sql_numbers_and_names[(decimal.Decimal(sql_file_num), sql_file_name)] = filename

return sql_numbers_and_names

@staticmethod
def _get_latest_migration_number_and_name(migration_numbers_and_names, sql_numbers_and_names):
largest_migration_number = decimal.Decimal(0)
latest_sql_number = None
largest_migration_number = latest_sql_number = None

for migration_number in migration_numbers_and_names:
largest_migration_number = migration_number
for migration_number, migration_name in migration_numbers_and_names.items():
largest_migration_number = (migration_number, migration_name)

for sql_number in sql_numbers_and_names:
for sql_number, sql_name in sql_numbers_and_names:
if sql_number is LATEST_VIEW_NUMBER:
latest_sql_number = sql_number
latest_sql_number = (sql_number, sql_name)

if largest_migration_number and latest_sql_number is not None:
return largest_migration_number, migration_numbers_and_names[largest_migration_number]
return largest_migration_number

return None, None

Expand Down Expand Up @@ -289,6 +292,37 @@ def _create_latest_sql_file(self, sql_path, db_table_name):

self.stdout.write(self.style.SUCCESS(f"\nCreated new SQL view file - '{latest_sql_filename}'."))

def _find_and_rewrite_migrations_containing_latest(
self,
migration_numbers_and_names,
migrations_path,
latest_sql_filename,
historical_sql_filename,
):
for migration_name in migration_numbers_and_names.values():
with open(os.path.join(migrations_path, f"{migration_name}.py"), "r+", encoding="utf-8") as f:
lines = f.readlines()
modified_migration = False
sql_line_no = 0
for line_no, line in enumerate(lines):
if line.find(latest_sql_filename) != -1:
sql_line_no = line_no
break

if sql_line_no:
lines[sql_line_no] = lines[sql_line_no].replace(latest_sql_filename, historical_sql_filename)
modified_migration = True

if modified_migration:
self.stdout.write(
self.style.SUCCESS(
f"\nModified migration '{migration_name}' to read from '{historical_sql_filename}'."
)
)
f.seek(0)
f.truncate(0)
f.writelines(lines)

def _rewrite_latest_migration(self, migrations_path, migration_name, latest_sql_filename, historical_sql_filename):
with open(os.path.join(migrations_path, migration_name + ".py"), "r+", encoding="utf-8") as f:
lines = f.readlines()
Expand Down Expand Up @@ -367,6 +401,20 @@ def _rewrite_migration(
f.seek(0)
f.writelines(lines)

def _get_historical_sql_filename(
self, db_table_name, latest_migration_number, latest_migration_name, sql_numbers_and_names
):
historical_sql_filename = f"view-{db_table_name}-{str(latest_migration_number).zfill(4)}.sql"
# Do multiple migrations with the same number exist?
# If so, we need to include the migration name in the sql view name.
if historical_sql_filename in sql_numbers_and_names.values():
latest_migration_name = latest_migration_name.split("_", 1)[1] # Remove the migration number.
historical_sql_filename = (
f"view-{db_table_name}-{str(latest_migration_number).zfill(4)}-{latest_migration_name}.sql"
)

return historical_sql_filename

@atomic
def handle(self, *args, **options):
# Get passed in args.
Expand Down Expand Up @@ -421,7 +469,9 @@ def handle(self, *args, **options):
# Is there a `latest` SQL view and migration?
if latest_migration_number is not None and latest_migration_name is not None:
latest_sql_filename = f"view-{db_table_name}-{LATEST_VIEW_NAME}.sql"
historical_sql_filename = f"view-{db_table_name}-{str(latest_migration_number).zfill(4)}.sql"
historical_sql_filename = self._get_historical_sql_filename(
db_table_name, latest_migration_number, latest_migration_name, sql_numbers_and_names
)

# Copy the `latest` SQL view to match the latest migration number.
self._copy_latest_sql_view(sql_path, latest_sql_filename, historical_sql_filename)
Expand All @@ -436,6 +486,14 @@ def handle(self, *args, **options):
)
)

# Find any additional migrations which should be switched to use the historical sql view filename.
self._find_and_rewrite_migrations_containing_latest(
migration_numbers_and_names,
migrations_path,
latest_sql_filename,
historical_sql_filename,
)

# Update the empty migration to use the `latest` sql view filename.
self._rewrite_migration(
migrations_path,
Expand Down
9 changes: 7 additions & 2 deletions tests/employees/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ def test_no_args(self):
)
# Depending on the width of your console, migration_name may be on the
# same line as the db_table_name, or it may wrap it onto the next line.
self.assertIn("{animals_pets,employees_employeelikes,food_sweets} migration_name", " ".join(err))
self.assertIn(
"{animals_pets,band_info,employees_employeelikes,food_sweets,"
"store_productcalculations,store_purchasedproductcalculations} migration_name",
" ".join(err),
)

def test_bad_db_table_name(self):
out, err = self.call_command(["manage.py", "makeviewmigration", "employees_employeehates", "create_view"])
self.assertIn(
"manage.py makeviewmigration: error: argument db_table_name: invalid choice: 'employees_"
"employeehates' (choose from 'animals_pets', 'employees_employeelikes', 'food_sweets')",
"employeehates' (choose from 'animals_pets', 'band_info', 'employees_employeelikes', 'food_sweets', "
"'store_productcalculations', 'store_purchasedproductcalculations')",
err,
)

Expand Down
Empty file added tests/musicians/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions tests/musicians/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class FoodConfig(AppConfig):
name = "tests.musicians"
verbose_name = "Musicians"
61 changes: 61 additions & 0 deletions tests/musicians/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Generated by Django 3.2.17 on 2023-08-11 16:16

import django.db.models.deletion
from django.db import migrations
from django.db import models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="BandInfo",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=1024)),
("genre", models.CharField(max_length=1024)),
],
options={
"db_table": "band_info",
"ordering": ("name",),
"managed": False,
"default_related_name": "musicians_bandinfo",
},
),
migrations.CreateModel(
name="Bands",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=1024)),
("genre", models.CharField(max_length=1024)),
("formed", models.DateField()),
],
options={
"ordering": ("name",),
"default_related_name": "bands",
},
),
migrations.CreateModel(
name="BandMembers",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=1024)),
("active_member", models.BooleanField(default=True)),
(
"band",
models.ForeignKey(
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="band_members",
to="musicians.bands",
),
),
],
options={
"ordering": ("name",),
"default_related_name": "band_members",
},
),
]
24 changes: 24 additions & 0 deletions tests/musicians/migrations/0002_create_band_info_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.17 on 2023-08-11 16:17
# Modified using django-view-manager 1.0.4. Please do not delete this comment.
import os

from django.db import migrations

sql_path = "tests/musicians/sql"
forward_sql_filename = "view-band_info-0002.sql"

with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f:
forwards_sql = f.read()


class Migration(migrations.Migration):
dependencies = [
("musicians", "0001_initial"),
]

operations = [
migrations.RunSQL(
sql=forwards_sql,
reverse_sql="DROP VIEW IF EXISTS band_info;",
),
]
28 changes: 28 additions & 0 deletions tests/musicians/migrations/0003_add_founded_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.17 on 2023-08-11 16:30
# Modified using django-view-manager 1.0.4. Please do not delete this comment.
import os

from django.db import migrations

sql_path = "tests/musicians/sql"
forward_sql_filename = "view-band_info-latest.sql"
reverse_sql_filename = "view-band_info-0003.sql"

with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f:
forwards_sql = f.read()

with open(os.path.join(sql_path, reverse_sql_filename), mode="r") as f:
reverse_sql = f.read()


class Migration(migrations.Migration):
dependencies = [
("musicians", "0002_create_band_info_view"),
]

operations = [
migrations.RunSQL(
sql=forwards_sql,
reverse_sql=reverse_sql,
),
]
28 changes: 28 additions & 0 deletions tests/musicians/migrations/0003_add_member_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.17 on 2023-08-11 16:21
# Modified using django-view-manager 1.0.4. Please do not delete this comment.
import os

from django.db import migrations

sql_path = "tests/musicians/sql"
forward_sql_filename = "view-band_info-0003.sql"
reverse_sql_filename = "view-band_info-0002.sql"

with open(os.path.join(sql_path, forward_sql_filename), mode="r") as f:
forwards_sql = f.read()

with open(os.path.join(sql_path, reverse_sql_filename), mode="r") as f:
reverse_sql = f.read()


class Migration(migrations.Migration):
dependencies = [
("musicians", "0002_create_band_info_view"),
]

operations = [
migrations.RunSQL(
sql=forwards_sql,
reverse_sql=reverse_sql,
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generated by Django 3.2.17 on 2023-08-11 16:34

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("musicians", "0003_add_founded_date"),
("musicians", "0003_add_member_count"),
]

operations = []
Empty file.
Loading

0 comments on commit ff66689

Please sign in to comment.