From 093b16d68f8e9c9a1b6c0379983d9dbe22596798 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:26:06 +0800 Subject: [PATCH 1/6] Add a CI workflow that upgrades vanilla Lunar 1.5 core to this v2 tree. --- .github/workflows/upgrade-command.yml | 212 ++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 .github/workflows/upgrade-command.yml diff --git a/.github/workflows/upgrade-command.yml b/.github/workflows/upgrade-command.yml new file mode 100644 index 0000000000..55ff73064a --- /dev/null +++ b/.github/workflows/upgrade-command.yml @@ -0,0 +1,212 @@ +name: Upgrade command + +on: + workflow_dispatch: + push: + branches: [2.x, ci/upgrade-command] + paths: + - packages/upgrade/** + - packages/core/database/migrations/** + - .github/workflows/upgrade-command.yml + pull_request: + paths: + - packages/upgrade/** + - packages/core/database/migrations/** + - .github/workflows/upgrade-command.yml + +concurrency: + group: upgrade-command-${{ github.workflow }}-${{ github.ref }}-${{ matrix.database || 'lock' }} + cancel-in-progress: true + +jobs: + lunar-upgrade: + name: v1.5 → v2 (${{ matrix.database }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - database: sqlite + db_connection: sqlite + - database: mysql + db_connection: mysql + db_port: "3306" + db_username: root + db_password: root + - database: postgres + db_connection: pgsql + db_port: "5432" + db_username: postgres + db_password: postgres + - database: mariadb + db_connection: mariadb + db_port: "3307" + db_username: root + db_password: root + env: + APP_DIR: ${{ runner.temp }}/vanilla-lunar + LUNAR_V1: "1.5.0" + APP_ENV: local + APP_DEBUG: "true" + SCOUT_DRIVER: collection + QUEUE_CONNECTION: sync + DB_CONNECTION: ${{ matrix.db_connection }} + DB_HOST: 127.0.0.1 + DB_PORT: ${{ matrix.db_port }} + DB_DATABASE: lunar_upgrade + DB_USERNAME: ${{ matrix.db_username }} + DB_PASSWORD: ${{ matrix.db_password }} + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: lunar_upgrade + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -proot" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + mariadb: + image: mariadb:11 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: lunar_upgrade + ports: + - 3307:3306 + options: >- + --health-cmd="mariadb-admin ping -h 127.0.0.1 -uroot -proot" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + postgres: + image: postgres:16 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: lunar_upgrade + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U postgres" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + steps: + - name: Checkout Lunar v2 + uses: actions/checkout@v5 + + - name: Setup PHP 8.5 + uses: shivammathur/setup-php@v2 + with: + php-version: "8.5" + extensions: bcmath, exif, intl, mbstring, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite + tools: composer:v2 + coverage: none + + - name: Create Laravel 13 app + run: composer create-project laravel/laravel:^13.0 "$APP_DIR" --prefer-dist --no-interaction --no-dev + + - name: Configure SQLite file + if: matrix.database == 'sqlite' + run: | + mkdir -p "$APP_DIR/database" + touch "$APP_DIR/database/database.sqlite" + echo "DB_CONNECTION=sqlite" >> "$APP_DIR/.env" + echo "DB_DATABASE=$APP_DIR/database/database.sqlite" >> "$APP_DIR/.env" + + - name: Configure server database + if: matrix.database != 'sqlite' + run: | + { + echo "DB_CONNECTION=${{ matrix.db_connection }}" + echo "DB_HOST=127.0.0.1" + echo "DB_PORT=${{ matrix.db_port }}" + echo "DB_DATABASE=lunar_upgrade" + echo "DB_USERNAME=${{ matrix.db_username }}" + echo "DB_PASSWORD=${{ matrix.db_password }}" + } >> "$APP_DIR/.env" + + - name: Install Lunar v1.5 core + working-directory: ${{ env.APP_DIR }} + run: | + php artisan key:generate --force --no-interaction + echo "SCOUT_DRIVER=collection" >> .env + echo "QUEUE_CONNECTION=sync" >> .env + composer require "lunarphp/core:${LUNAR_V1}" --no-interaction --no-progress + + - name: Migrate v1 schema + working-directory: ${{ env.APP_DIR }} + run: php artisan migrate --force --no-interaction + + - name: Confirm v1 ledger + working-directory: ${{ env.APP_DIR }} + run: | + php -r ' + require "vendor/autoload.php"; + $app = require "bootstrap/app.php"; + $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + $found = Illuminate\Support\Facades\DB::table("migrations") + ->where("migration", "2021_07_29_100000_create_channels_table") + ->exists(); + if (!$found) { + fwrite(STDERR, "v1 canary migration is missing; lunarphp/core 1.5 did not migrate.\n"); + exit(1); + } + echo "v1 schema present\n"; + ' + + - name: Switch app to this v2 checkout + working-directory: ${{ env.APP_DIR }} + run: | + composer config minimum-stability dev + composer config prefer-stable true + composer config repositories.lunar-core "{\"type\":\"path\",\"url\":\"${GITHUB_WORKSPACE}/packages/core\",\"options\":{\"symlink\":true}}" + composer config repositories.lunar-upgrade "{\"type\":\"path\",\"url\":\"${GITHUB_WORKSPACE}/packages/upgrade\",\"options\":{\"symlink\":true}}" + composer require lunarphp/core:@dev lunarphp/upgrade:@dev --no-interaction --no-progress --with-all-dependencies + + - name: Run lunar:upgrade + working-directory: ${{ env.APP_DIR }} + run: php artisan lunar:upgrade --no-interaction + + - name: Apply post-baseline migrations + working-directory: ${{ env.APP_DIR }} + run: php artisan migrate --force --no-interaction + + - name: Assert upgraded schema + working-directory: ${{ env.APP_DIR }} + run: | + php -r ' + require "vendor/autoload.php"; + $app = require "bootstrap/app.php"; + $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); + $db = Illuminate\Support\Facades\DB::connection(); + $schema = Illuminate\Support\Facades\Schema::connection($db->getName()); + $prefix = (string) config("lunar.database.table_prefix", "lunar_"); + + $hasV2 = $db->table("migrations")->where("migration", "like", "2026_01_01_%")->exists(); + $hasV1 = $db->table("migrations")->where("migration", "2021_07_29_100000_create_channels_table")->exists(); + if (!$hasV2) { + fwrite(STDERR, "v2 baseline rows were not written to the migrations ledger.\n"); + exit(1); + } + if ($hasV1) { + fwrite(STDERR, "v1 canary row is still in the migrations ledger.\n"); + exit(1); + } + + $attributes = $prefix."attributes"; + $types = $prefix."product_types"; + if ($schema->hasColumn($attributes, "attribute_type")) { + fwrite(STDERR, $attributes.".attribute_type still exists after upgrade.\n"); + exit(1); + } + if (!$schema->hasColumn($types, "default_tax_class_id")) { + fwrite(STDERR, $types.".default_tax_class_id is missing after upgrade.\n"); + exit(1); + } + + echo "upgrade schema ok\n"; + ' From 35920f69061cf336b338c3caea237dc4559dd724 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:29:10 +0800 Subject: [PATCH 2/6] Fix upgrade workflow env overrides and concurrency. --- .github/workflows/upgrade-command.yml | 66 +++++++++++++++------------ 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/.github/workflows/upgrade-command.yml b/.github/workflows/upgrade-command.yml index 55ff73064a..08504829a8 100644 --- a/.github/workflows/upgrade-command.yml +++ b/.github/workflows/upgrade-command.yml @@ -15,7 +15,7 @@ on: - .github/workflows/upgrade-command.yml concurrency: - group: upgrade-command-${{ github.workflow }}-${{ github.ref }}-${{ matrix.database || 'lock' }} + group: upgrade-command-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -51,12 +51,6 @@ jobs: APP_DEBUG: "true" SCOUT_DRIVER: collection QUEUE_CONNECTION: sync - DB_CONNECTION: ${{ matrix.db_connection }} - DB_HOST: 127.0.0.1 - DB_PORT: ${{ matrix.db_port }} - DB_DATABASE: lunar_upgrade - DB_USERNAME: ${{ matrix.db_username }} - DB_PASSWORD: ${{ matrix.db_password }} services: mysql: image: mysql:8.0 @@ -109,33 +103,45 @@ jobs: - name: Create Laravel 13 app run: composer create-project laravel/laravel:^13.0 "$APP_DIR" --prefer-dist --no-interaction --no-dev - - name: Configure SQLite file - if: matrix.database == 'sqlite' - run: | - mkdir -p "$APP_DIR/database" - touch "$APP_DIR/database/database.sqlite" - echo "DB_CONNECTION=sqlite" >> "$APP_DIR/.env" - echo "DB_DATABASE=$APP_DIR/database/database.sqlite" >> "$APP_DIR/.env" - - - name: Configure server database - if: matrix.database != 'sqlite' - run: | - { - echo "DB_CONNECTION=${{ matrix.db_connection }}" - echo "DB_HOST=127.0.0.1" - echo "DB_PORT=${{ matrix.db_port }}" - echo "DB_DATABASE=lunar_upgrade" - echo "DB_USERNAME=${{ matrix.db_username }}" - echo "DB_PASSWORD=${{ matrix.db_password }}" - } >> "$APP_DIR/.env" - - - name: Install Lunar v1.5 core - working-directory: ${{ env.APP_DIR }} + - name: Write database env run: | + cd "$APP_DIR" php artisan key:generate --force --no-interaction + if [ "${{ matrix.database }}" = "sqlite" ]; then + mkdir -p database + touch database/database.sqlite + { + echo "DB_CONNECTION=sqlite" + echo "DB_DATABASE=$APP_DIR/database/database.sqlite" + } >> "$GITHUB_ENV" + { + echo "DB_CONNECTION=sqlite" + echo "DB_DATABASE=$APP_DIR/database/database.sqlite" + } >> .env + else + { + echo "DB_CONNECTION=${{ matrix.db_connection }}" + echo "DB_HOST=127.0.0.1" + echo "DB_PORT=${{ matrix.db_port }}" + echo "DB_DATABASE=lunar_upgrade" + echo "DB_USERNAME=${{ matrix.db_username }}" + echo "DB_PASSWORD=${{ matrix.db_password }}" + } >> "$GITHUB_ENV" + { + echo "DB_CONNECTION=${{ matrix.db_connection }}" + echo "DB_HOST=127.0.0.1" + echo "DB_PORT=${{ matrix.db_port }}" + echo "DB_DATABASE=lunar_upgrade" + echo "DB_USERNAME=${{ matrix.db_username }}" + echo "DB_PASSWORD=${{ matrix.db_password }}" + } >> .env + fi echo "SCOUT_DRIVER=collection" >> .env echo "QUEUE_CONNECTION=sync" >> .env - composer require "lunarphp/core:${LUNAR_V1}" --no-interaction --no-progress + + - name: Install Lunar v1.5 core + working-directory: ${{ env.APP_DIR }} + run: composer require "lunarphp/core:${LUNAR_V1}" --no-interaction --no-progress - name: Migrate v1 schema working-directory: ${{ env.APP_DIR }} From 15f883aef861e5516b2ddea89d27c5b4df89df86 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:51:38 +0800 Subject: [PATCH 3/6] Fix workflow parse error and run the full upgrade on all four databases. --- .github/workflows/upgrade-command.yml | 132 +++++++------------------- 1 file changed, 34 insertions(+), 98 deletions(-) diff --git a/.github/workflows/upgrade-command.yml b/.github/workflows/upgrade-command.yml index 08504829a8..cf8d7b04e4 100644 --- a/.github/workflows/upgrade-command.yml +++ b/.github/workflows/upgrade-command.yml @@ -14,13 +14,9 @@ on: - packages/core/database/migrations/** - .github/workflows/upgrade-command.yml -concurrency: - group: upgrade-command-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: lunar-upgrade: - name: v1.5 → v2 (${{ matrix.database }}) + name: v1.5 to v2 (${{ matrix.database }}) runs-on: ubuntu-latest timeout-minutes: 30 strategy: @@ -29,28 +25,38 @@ jobs: include: - database: sqlite db_connection: sqlite + db_database: /tmp/vanilla-lunar/database/database.sqlite - database: mysql db_connection: mysql db_port: "3306" + db_database: lunar_upgrade db_username: root db_password: root - database: postgres db_connection: pgsql db_port: "5432" + db_database: lunar_upgrade db_username: postgres db_password: postgres - database: mariadb db_connection: mariadb db_port: "3307" + db_database: lunar_upgrade db_username: root db_password: root env: - APP_DIR: ${{ runner.temp }}/vanilla-lunar + APP_DIR: /tmp/vanilla-lunar LUNAR_V1: "1.5.0" APP_ENV: local APP_DEBUG: "true" SCOUT_DRIVER: collection QUEUE_CONNECTION: sync + DB_CONNECTION: ${{ matrix.db_connection }} + DB_HOST: 127.0.0.1 + DB_PORT: ${{ matrix.db_port }} + DB_DATABASE: ${{ matrix.db_database }} + DB_USERNAME: ${{ matrix.db_username }} + DB_PASSWORD: ${{ matrix.db_password }} services: mysql: image: mysql:8.0 @@ -89,11 +95,9 @@ jobs: --health-timeout=5s --health-retries=10 steps: - - name: Checkout Lunar v2 - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - - name: Setup PHP 8.5 - uses: shivammathur/setup-php@v2 + - uses: shivammathur/setup-php@v2 with: php-version: "8.5" extensions: bcmath, exif, intl, mbstring, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite @@ -103,69 +107,37 @@ jobs: - name: Create Laravel 13 app run: composer create-project laravel/laravel:^13.0 "$APP_DIR" --prefer-dist --no-interaction --no-dev - - name: Write database env + - name: Configure app + working-directory: /tmp/vanilla-lunar run: | - cd "$APP_DIR" php artisan key:generate --force --no-interaction if [ "${{ matrix.database }}" = "sqlite" ]; then mkdir -p database touch database/database.sqlite - { - echo "DB_CONNECTION=sqlite" - echo "DB_DATABASE=$APP_DIR/database/database.sqlite" - } >> "$GITHUB_ENV" - { - echo "DB_CONNECTION=sqlite" - echo "DB_DATABASE=$APP_DIR/database/database.sqlite" - } >> .env - else - { - echo "DB_CONNECTION=${{ matrix.db_connection }}" - echo "DB_HOST=127.0.0.1" - echo "DB_PORT=${{ matrix.db_port }}" - echo "DB_DATABASE=lunar_upgrade" - echo "DB_USERNAME=${{ matrix.db_username }}" - echo "DB_PASSWORD=${{ matrix.db_password }}" - } >> "$GITHUB_ENV" - { - echo "DB_CONNECTION=${{ matrix.db_connection }}" - echo "DB_HOST=127.0.0.1" - echo "DB_PORT=${{ matrix.db_port }}" - echo "DB_DATABASE=lunar_upgrade" - echo "DB_USERNAME=${{ matrix.db_username }}" - echo "DB_PASSWORD=${{ matrix.db_password }}" - } >> .env fi - echo "SCOUT_DRIVER=collection" >> .env - echo "QUEUE_CONNECTION=sync" >> .env + { + echo "APP_ENV=local" + echo "APP_DEBUG=true" + echo "SCOUT_DRIVER=collection" + echo "QUEUE_CONNECTION=sync" + echo "DB_CONNECTION=$DB_CONNECTION" + echo "DB_HOST=$DB_HOST" + echo "DB_PORT=$DB_PORT" + echo "DB_DATABASE=$DB_DATABASE" + echo "DB_USERNAME=$DB_USERNAME" + echo "DB_PASSWORD=$DB_PASSWORD" + } >> .env - name: Install Lunar v1.5 core - working-directory: ${{ env.APP_DIR }} + working-directory: /tmp/vanilla-lunar run: composer require "lunarphp/core:${LUNAR_V1}" --no-interaction --no-progress - name: Migrate v1 schema - working-directory: ${{ env.APP_DIR }} + working-directory: /tmp/vanilla-lunar run: php artisan migrate --force --no-interaction - - name: Confirm v1 ledger - working-directory: ${{ env.APP_DIR }} - run: | - php -r ' - require "vendor/autoload.php"; - $app = require "bootstrap/app.php"; - $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); - $found = Illuminate\Support\Facades\DB::table("migrations") - ->where("migration", "2021_07_29_100000_create_channels_table") - ->exists(); - if (!$found) { - fwrite(STDERR, "v1 canary migration is missing; lunarphp/core 1.5 did not migrate.\n"); - exit(1); - } - echo "v1 schema present\n"; - ' - - name: Switch app to this v2 checkout - working-directory: ${{ env.APP_DIR }} + working-directory: /tmp/vanilla-lunar run: | composer config minimum-stability dev composer config prefer-stable true @@ -174,45 +146,9 @@ jobs: composer require lunarphp/core:@dev lunarphp/upgrade:@dev --no-interaction --no-progress --with-all-dependencies - name: Run lunar:upgrade - working-directory: ${{ env.APP_DIR }} + working-directory: /tmp/vanilla-lunar run: php artisan lunar:upgrade --no-interaction - - name: Apply post-baseline migrations - working-directory: ${{ env.APP_DIR }} + - name: Apply remaining migrations + working-directory: /tmp/vanilla-lunar run: php artisan migrate --force --no-interaction - - - name: Assert upgraded schema - working-directory: ${{ env.APP_DIR }} - run: | - php -r ' - require "vendor/autoload.php"; - $app = require "bootstrap/app.php"; - $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); - $db = Illuminate\Support\Facades\DB::connection(); - $schema = Illuminate\Support\Facades\Schema::connection($db->getName()); - $prefix = (string) config("lunar.database.table_prefix", "lunar_"); - - $hasV2 = $db->table("migrations")->where("migration", "like", "2026_01_01_%")->exists(); - $hasV1 = $db->table("migrations")->where("migration", "2021_07_29_100000_create_channels_table")->exists(); - if (!$hasV2) { - fwrite(STDERR, "v2 baseline rows were not written to the migrations ledger.\n"); - exit(1); - } - if ($hasV1) { - fwrite(STDERR, "v1 canary row is still in the migrations ledger.\n"); - exit(1); - } - - $attributes = $prefix."attributes"; - $types = $prefix."product_types"; - if ($schema->hasColumn($attributes, "attribute_type")) { - fwrite(STDERR, $attributes.".attribute_type still exists after upgrade.\n"); - exit(1); - } - if (!$schema->hasColumn($types, "default_tax_class_id")) { - fwrite(STDERR, $types.".default_tax_class_id is missing after upgrade.\n"); - exit(1); - } - - echo "upgrade schema ok\n"; - ' From 842b995d4e6c87abcac1d4bede1aecdbc93576c3 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:03:34 +0800 Subject: [PATCH 4/6] Run the upgrade-command workflow on 2.x only. --- .github/workflows/upgrade-command.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/upgrade-command.yml b/.github/workflows/upgrade-command.yml index cf8d7b04e4..9227d3d753 100644 --- a/.github/workflows/upgrade-command.yml +++ b/.github/workflows/upgrade-command.yml @@ -3,12 +3,13 @@ name: Upgrade command on: workflow_dispatch: push: - branches: [2.x, ci/upgrade-command] + branches: [2.x] paths: - packages/upgrade/** - packages/core/database/migrations/** - .github/workflows/upgrade-command.yml pull_request: + branches: [2.x] paths: - packages/upgrade/** - packages/core/database/migrations/** From 0251f3e472e483e35f37e21d3c9d269e41d1f011 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:09:05 +0800 Subject: [PATCH 5/6] fix(upgrade): drop morph indexes before columns and baseline 000071 MariaDB and SQLite refuse DROP COLUMN while the v1 attribute morph indexes remain. The product-types tax-class FK is already created by the upgrade data step, so 000071 belongs in v2_baseline like 000059. Co-authored-by: Cursor --- packages/upgrade/config/upgrade.php | 1 + ...003_convert_attribute_data_keys_to_ids.php | 35 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/upgrade/config/upgrade.php b/packages/upgrade/config/upgrade.php index 8b02ecb30a..7609806959 100644 --- a/packages/upgrade/config/upgrade.php +++ b/packages/upgrade/config/upgrade.php @@ -165,6 +165,7 @@ '2026_01_01_000068_create_country_region_table', '2026_01_01_000069_add_region_id_to_carts_and_orders', '2026_01_01_000059_add_orders_cart_id_foreign_key', + '2026_01_01_000071_add_product_types_default_tax_class_foreign_key', '2026_01_01_900000_create_staff_table', '2026_01_01_900001_create_activity_log_table', '2026_01_01_900002_create_permission_tables', diff --git a/packages/upgrade/database/migrations/2026_06_01_000003_convert_attribute_data_keys_to_ids.php b/packages/upgrade/database/migrations/2026_06_01_000003_convert_attribute_data_keys_to_ids.php index a35985ffc8..454d13109b 100644 --- a/packages/upgrade/database/migrations/2026_06_01_000003_convert_attribute_data_keys_to_ids.php +++ b/packages/upgrade/database/migrations/2026_06_01_000003_convert_attribute_data_keys_to_ids.php @@ -24,9 +24,9 @@ * 6. Convert `attributes.validation_rules` from the v1 pipe-delimited string * (`min:1|max:10`) to the v2 json list (`["min:1", "max:10"]`) — split on * `|` exactly as Laravel's validator parses string rules. - * 7. Add `attribute_groups.system`, drop the morph columns and unused - * `section` / `default_value`, make `attributes.attribute_group_id` - * nullable. + * 7. Add `attribute_groups.system`, drop the morph columns (and the v1 + * indexes that still sit on them) and unused `section` / `default_value`, + * make `attributes.attribute_group_id` nullable. * * One-way — `down()` is intentionally absent. Restore from backup to reverse. */ @@ -319,6 +319,9 @@ protected function reshapeAttributeGroupsSchema(): void } if (Schema::hasColumn($table, 'attributable_type')) { + // SQLite refuses DROP COLUMN while the v1 index remains. + $this->dropIndexIfExists($table, ['attributable_type']); + Schema::table($table, function (Blueprint $table) { $table->dropColumn('attributable_type'); }); @@ -393,6 +396,12 @@ protected function reshapeAttributesSchema(): void )); if ($drops !== []) { + if (in_array('attribute_type', $drops, true)) { + // MariaDB 1072 / SQLite: DROP COLUMN fails while these remain. + $this->dropIndexIfExists($table, ['attribute_type', 'handle'], 'unique'); + $this->dropIndexIfExists($table, ['attribute_type']); + } + Schema::table($table, function (Blueprint $blueprint) use ($drops) { $blueprint->dropColumn($drops); }); @@ -406,6 +415,26 @@ protected function reshapeAttributesSchema(): void } } + /** + * @param list $columns + */ + protected function dropIndexIfExists(string $table, array $columns, string $type = 'index'): void + { + if (! Schema::hasIndex($table, $columns, $type)) { + return; + } + + Schema::table($table, function (Blueprint $blueprint) use ($columns, $type): void { + if ($type === 'unique') { + $blueprint->dropUnique($columns); + + return; + } + + $blueprint->dropIndex($columns); + }); + } + protected function normaliseMorph(?string $value): ?string { if ($value === null || $value === '') { From 887bcf036d4ee0f6c9cf649c3b3c2c12f9b8ba58 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:22:55 +0800 Subject: [PATCH 6/6] fix(upgrade): drop SQLite btree indexes and skip missing staff baseline Schema::hasIndex(..., 'index') misses SQLite, which reports btree. Core-only v1 never created staff, but v2 still marked create_staff as run, so later core staff migrations altered a missing table. Co-authored-by: Cursor --- ...003_convert_attribute_data_keys_to_ids.php | 29 ++++++---- .../upgrade/src/Steps/LedgerRewriteStep.php | 24 +++++++- .../ConvertAttributeDataKeysToIdsTest.php | 6 +- tests/upgrade/Unit/LedgerRewriteStepTest.php | 56 +++++++++++++++++++ 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/packages/upgrade/database/migrations/2026_06_01_000003_convert_attribute_data_keys_to_ids.php b/packages/upgrade/database/migrations/2026_06_01_000003_convert_attribute_data_keys_to_ids.php index 454d13109b..e54ec394da 100644 --- a/packages/upgrade/database/migrations/2026_06_01_000003_convert_attribute_data_keys_to_ids.php +++ b/packages/upgrade/database/migrations/2026_06_01_000003_convert_attribute_data_keys_to_ids.php @@ -416,23 +416,32 @@ protected function reshapeAttributesSchema(): void } /** + * Drop a v1 index by its live name. `Schema::hasIndex(..., 'index')` misses + * SQLite, which reports the type as `btree` rather than `index`. + * * @param list $columns */ protected function dropIndexIfExists(string $table, array $columns, string $type = 'index'): void { - if (! Schema::hasIndex($table, $columns, $type)) { - return; - } - - Schema::table($table, function (Blueprint $blueprint) use ($columns, $type): void { - if ($type === 'unique') { - $blueprint->dropUnique($columns); + $wantUnique = $type === 'unique'; - return; + foreach (Schema::getIndexes($table) as $index) { + if ($index['columns'] !== $columns || (bool) $index['unique'] !== $wantUnique) { + continue; } - $blueprint->dropIndex($columns); - }); + Schema::table($table, function (Blueprint $blueprint) use ($index, $wantUnique): void { + if ($wantUnique) { + $blueprint->dropUnique($index['name']); + + return; + } + + $blueprint->dropIndex($index['name']); + }); + + return; + } } protected function normaliseMorph(?string $value): ?string diff --git a/packages/upgrade/src/Steps/LedgerRewriteStep.php b/packages/upgrade/src/Steps/LedgerRewriteStep.php index c8cbbf32c9..59865ea8e6 100644 --- a/packages/upgrade/src/Steps/LedgerRewriteStep.php +++ b/packages/upgrade/src/Steps/LedgerRewriteStep.php @@ -6,6 +6,7 @@ use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Migrations\Migrator; +use Illuminate\Support\Facades\Schema; use Lunar\Upgrade\Support\StepReport; /** @@ -19,7 +20,8 @@ * the app's own files do not, and deleting their rows would make `migrate` * re-run them), and a baseline row is only inserted when its file is present * (marking-run a migration from an uninstalled sub-package would silently - * skip it if that package is installed later). + * skip it if that package is installed later). Staff is extra: the create + * file now lives in core, so we also require the table to exist. */ class LedgerRewriteStep implements UpgradeStep { @@ -72,7 +74,8 @@ public function run(StepContext $context): void $toInsert = array_values(array_filter( array_diff($config['v2_baseline'], $existing), - fn (string $migration): bool => isset($known[$migration]), + fn (string $migration): bool => isset($known[$migration]) + && $this->baselineSchemaPresent($migration, $context->connection), )); if ($context->dryRun) { @@ -107,6 +110,23 @@ public function run(StepContext $context): void ); } + /** + * Staff lived in the v1 admin package. v2 folded `create_staff` into core, + * so the migration file is always present even when the table was never + * created (core-only / headless v1). Marking it run would skip the create + * and leave later core staff migrations altering a missing table. + */ + protected function baselineSchemaPresent(string $migration, ?string $connection): bool + { + if ($migration !== '2026_01_01_900000_create_staff_table') { + return true; + } + + return Schema::connection($connection)->hasTable( + config('lunar.database.table_prefix').'staff', + ); + } + /** * Migration names the application can currently load, keyed by name — * every path registered with the migrator (package `loadMigrationsFrom` diff --git a/tests/upgrade/Feature/ConvertAttributeDataKeysToIdsTest.php b/tests/upgrade/Feature/ConvertAttributeDataKeysToIdsTest.php index 5dd343c57e..00fbd57fee 100644 --- a/tests/upgrade/Feature/ConvertAttributeDataKeysToIdsTest.php +++ b/tests/upgrade/Feature/ConvertAttributeDataKeysToIdsTest.php @@ -50,7 +50,7 @@ function simulateV1AttributeShape(): void Schema::create(SPEC0019_PREFIX.'attribute_groups', function (Blueprint $table) { $table->id(); - $table->string('attributable_type'); + $table->string('attributable_type')->index(); $table->json('name'); $table->string('handle'); $table->integer('position'); @@ -59,7 +59,7 @@ function simulateV1AttributeShape(): void Schema::create(SPEC0019_PREFIX.'attributes', function (Blueprint $table) { $table->id(); - $table->string('attribute_type'); + $table->string('attribute_type')->index(); $table->foreignId('attribute_group_id'); $table->integer('position'); $table->json('name'); @@ -74,6 +74,8 @@ function simulateV1AttributeShape(): void $table->boolean('searchable')->default(true); $table->boolean('filterable')->default(false); $table->timestamps(); + + $table->unique(['attribute_type', 'handle']); }); Schema::create(SPEC0019_PREFIX.'attributables', function (Blueprint $table) { diff --git a/tests/upgrade/Unit/LedgerRewriteStepTest.php b/tests/upgrade/Unit/LedgerRewriteStepTest.php index 1e6492439c..dae6bf51af 100644 --- a/tests/upgrade/Unit/LedgerRewriteStepTest.php +++ b/tests/upgrade/Unit/LedgerRewriteStepTest.php @@ -134,6 +134,62 @@ assertDatabaseHas('migrations', ['migration' => '2026_06_01_000000_rewrite_lunar_class_strings']); }); +it('does not mark create_staff as run when the staff table is missing', function () { + touch($this->migrationPath.'/2026_01_01_900000_create_staff_table.php'); + + Config::set('lunar.upgrade.ledger', [ + 'v1_match' => [], + 'v2_baseline' => [ + '2026_01_01_000000_create_channels_table', + '2026_01_01_900000_create_staff_table', + ], + ]); + + $report = new StepReport; + $context = new StepContext( + dryRun: false, + paths: [], + output: new OutputStyle(new StringInput(''), new BufferedOutput), + report: $report, + ); + + app(LedgerRewriteStep::class)->run($context); + + assertDatabaseHas('migrations', ['migration' => '2026_01_01_000000_create_channels_table']); + assertDatabaseMissing('migrations', ['migration' => '2026_01_01_900000_create_staff_table']); +}); + +it('marks create_staff as run when the staff table already exists', function () { + $table = config('lunar.database.table_prefix').'staff'; + + Schema::create($table, function ($blueprint) { + $blueprint->id(); + }); + + touch($this->migrationPath.'/2026_01_01_900000_create_staff_table.php'); + + Config::set('lunar.upgrade.ledger', [ + 'v1_match' => [], + 'v2_baseline' => ['2026_01_01_900000_create_staff_table'], + ]); + + $report = new StepReport; + $context = new StepContext( + dryRun: false, + paths: [], + output: new OutputStyle(new StringInput(''), new BufferedOutput), + report: $report, + ); + + try { + app(LedgerRewriteStep::class)->run($context); + + assertDatabaseHas('migrations', ['migration' => '2026_01_01_900000_create_staff_table']); + } finally { + Schema::dropIfExists($table); + } +}); + it('does not insert baseline rows for migrations the app cannot load', function () { Config::set('lunar.upgrade.ledger', [ 'v1_match' => [],