Laravel migration sample generator

# This code is dedicated to the public domain under CC0 1.0.
# You may use it freely for any purpose. No warranty is provided.
# https://creativecommons.org/publicdomain/zero/1.0/

import os
import sys
import xml.etree.ElementTree as ET
from datetime import datetime

INDENT = "    "
NEWLINE = "\n"

up = []
down = []
actions = []

def parse_column_list(s):
    columns = s.split(",")
    return '[' + ', '.join(f"'{c.strip()}'" for c in columns) + ']'

def parse_column_element(column_element):
    name = column_element.get('Name')
    data_type = column_element.get('Type')
    nullable = column_element.get('Nullable', 'false').lower() == 'true'
    default = column_element.get('Default', '')
    comment = column_element.get('Comment', '')
    auto_increment = column_element.get('AutoIncrement', 'false').lower() == 'true'

    method = f"$table->{laravel_type(data_type)}('{name}')"
    if auto_increment:
        method += '->increments()'
    if nullable:
        method += '->nullable(true)'
    if default:
        if default.lower() == 'null':
            method += "->default(null)"
        else:
            method += f"->default('{default}')"
    if comment:
        method += f"->comment('{comment}')"

    return method

def laravel_type(db_type):
    mapping = {
        'integer': 'integer',
        'string': 'string',
        'text': 'text',
        'boolean': 'boolean',
        'datetime': 'dateTime',
        'date': 'date',
        'float': 'float',
        'double': 'double',
        'bigint': 'bigInteger',
        'smallint': 'smallInteger',
        'tinyint': 'tinyInteger',
        'char': 'char',
        'decimal': 'decimal'
    }
    return mapping.get(db_type.lower(), db_type)

def handle_create_table(diff):
    table = diff.attrib['Name']
    actions.append(f"create_{table}_table")

    up.append(f"Schema::create('{table}', function (Blueprint $table) {{")
    for column in diff.findall('Column'):
        up.append(INDENT + parse_column_element(column) + ';')

    for pk in diff.findall('PrimaryKey'):
        cols = parse_column_list(pk.get('Columns'))
        up.append(INDENT + f"$table->primary({cols}, '{pk.get('Name', 'pk_' + table)}');")

    for idx in diff.findall('Index'):
        up.append(INDENT + f"$table->index({parse_column_list(idx.get('Columns'))}, '{idx.get('Name')}');")

    for uq in diff.findall('Unique'):
        up.append(INDENT + f"$table->unique({parse_column_list(uq.get('Columns'))}, '{uq.get('Name')}');")

    for fk in diff.findall('ForeignKey'):
        up.append(INDENT + handle_foreign_key_inline(fk))

    up.append("});")

    down.append(f"Schema::dropIfExists('{table}');")

def handle_drop_table(diff):
    table = diff.attrib['Name']
    actions.append(f"drop_table_{table}")
    up.append(f"Schema::dropIfExists('{table}');")
    down.append(f"echo \"Cannot safely revert this migration: table '{table}' was dropped and data is lost.\";")
    down.append("return false;")

def handle_alter_table(diff):
    table = diff.find('Name').text
    actions.append(f"alter_{table}_table")
    unable_to_revert = False

    rename_to = diff.attrib.get('RenameTo')
    if rename_to:
        up.append(f"Schema::rename('{table}', '{rename_to}');")
        down.append(f"Schema::rename('{rename_to}', '{table}');")
        table = rename_to

    for col in diff.findall('DropColumn'):
        name = col.get('Name')
        up.append(f"Schema::table('{table}', function (Blueprint $table) {{ $table->dropColumn('{name}'); }});")
        down.append(f"echo \"Cannot safely revert this migration: column '{name}' was dropped.\";")
        unable_to_revert = True

    for col in diff.findall('AddColumn'):
        up.append(f"Schema::table('{table}', function (Blueprint $table) {{ {parse_column_element(col)}; }});")
        down.append(f"Schema::table('{table}', function (Blueprint $table) {{ $table->dropColumn('{col.get('Name')}'); }});")

    for ch in diff.findall('ChangeColumn'):
        new_col = ch.find('NewColumn')
        old_col = ch.find('OldColumn')
        up.append(f"Schema::table('{table}', function (Blueprint $table) {{ $table->renameColumn('{old_col.get('Name')}', '{new_col.get('Name')}'); }});")
        down.append(f"Schema::table('{table}', function (Blueprint $table) {{ $table->renameColumn('{new_col.get('Name')}', '{old_col.get('Name')}'); }});")

    if unable_to_revert:
        down.append("return false;")

def handle_foreign_key_inline(fk):
    name = fk.get('Name')
    from_col = fk.get('FromColumn')
    to_col = fk.get('ToColumn')
    to_table = fk.get('ToTable')
    on_delete = fk.get('OnDelete')
    on_update = fk.get('OnUpdate')

    fk_stmt = f"$table->foreign('{from_col}')->references('{to_col}')->on('{to_table}')"
    if on_delete:
        fk_stmt += f"->onDelete('{on_delete}')"
    if on_update:
        fk_stmt += f"->onUpdate('{on_update}')"
    fk_stmt += f"->name('{name}');"

    return fk_stmt

def get_filename():
    now = datetime.now()
    suffix = actions[0] if len(actions) == 1 else 'migration'
    return now.strftime(f"%Y_%m_%d_%H%M%S_{suffix}.php")

def generate_class():
    lines = [
        "