From 687e134e6d7605316f51cd71b7bac65cddba0cde Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sun, 27 Sep 2026 08:39:10 +0200 Subject: [PATCH 1/3] Align schema across engines: named foreign keys, group updated in cascade, no default amount, mandatory subscription columns --- _define.php | 2 +- .../PluginGaletteActivities.php | 25 +++++++ scripts/mysql.sql | 13 ++-- scripts/pgsql.sql | 6 +- scripts/upgrade-to-1.1-mysql.sql | 65 +++++++++++++++++++ scripts/upgrade-to-1.1-pgsql.sql | 15 +++++ .../tests/units/PluginGaletteActivities.php | 20 ++++++ 7 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 scripts/upgrade-to-1.1-mysql.sql create mode 100644 scripts/upgrade-to-1.1-pgsql.sql diff --git a/_define.php b/_define.php index 017e1d2..0e4a247 100644 --- a/_define.php +++ b/_define.php @@ -20,5 +20,5 @@ acls: [ //Permissions needed '/activities_.*/' => 'staff' ], - dbver: 1.00 + dbver: 1.1 ); diff --git a/lib/GaletteActivities/PluginGaletteActivities.php b/lib/GaletteActivities/PluginGaletteActivities.php index 64737e4..6792189 100644 --- a/lib/GaletteActivities/PluginGaletteActivities.php +++ b/lib/GaletteActivities/PluginGaletteActivities.php @@ -16,9 +16,12 @@ use Galette\Core\Plugins\MemberActionProviderInterface; use Galette\Core\Plugins\MenuProviderInterface; use Galette\Entity\Adherent; +use Galette\Entity\Group; use Galette\Core\GalettePlugin; use GaletteActivities\Entity\Activity; use GaletteActivities\Entity\Subscription; +use Laminas\Db\Metadata\Object\ConstraintObject; +use Laminas\Db\Metadata\Source\Factory; /** * Galette Activities plugin @@ -142,4 +145,26 @@ public function isInstalled(): bool $this->zdb->tableExists(ACTIVITIES_PREFIX . Activity::TABLE) && $this->zdb->tableExists(ACTIVITIES_PREFIX . Subscription::TABLE); } + + /** + * Database version of tables created before plugins versions tracking + * + * Before 1.1, the group foreign key was not updated in cascade on MySQL, + * and subscriptions columns accepted NULL on PostgreSQL. + */ + public function getLegacyDbVersion(): ?float + { + $metadata = Factory::createSourceFromAdapter($this->zdb->db); + /** @var ConstraintObject $constraint */ + foreach ($metadata->getConstraints(PREFIX_DB . ACTIVITIES_PREFIX . Activity::TABLE) as $constraint) { + if ($constraint->isForeignKey() && $constraint->getColumns() === [Group::PK]) { + if ($constraint->getUpdateRule() !== 'CASCADE') { + return 1.0; + } + } + } + + $column = $metadata->getColumn(Activity::PK, PREFIX_DB . ACTIVITIES_PREFIX . Subscription::TABLE); + return $column->isNullable() ? 1.0 : null; + } } diff --git a/scripts/mysql.sql b/scripts/mysql.sql index 4cb98fd..42e3a1b 100644 --- a/scripts/mysql.sql +++ b/scripts/mysql.sql @@ -20,7 +20,8 @@ CREATE TABLE galette_activities_activities ( creation_date date NOT NULL, comment text, PRIMARY KEY (id_activity), - FOREIGN KEY (id_group) REFERENCES galette_groups (id_group) ON DELETE RESTRICT ON UPDATE RESTRICT + CONSTRAINT galette_activities_activities_id_group_fkey FOREIGN KEY (id_group) + REFERENCES galette_groups (id_group) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- @@ -33,16 +34,18 @@ CREATE TABLE galette_activities_subscriptions ( id_activity int(10) NOT NULL, id_adh int(10) unsigned NOT NULL, is_paid tinyint(1) NOT NULL default 0, - payment_amount decimal(15, 2) default '0', + payment_amount decimal(15, 2) default NULL, payment_method tinyint(3) unsigned NOT NULL default '0', creation_date date NOT NULL, subscription_date date NOT NULL, end_date date NOT NULL, comment text, PRIMARY KEY (id_subscription), - UNIQUE KEY (id_activity, id_adh), - FOREIGN KEY (id_activity) REFERENCES galette_activities_activities (id_activity) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (id_adh) REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE + CONSTRAINT galette_activities_subscriptions_id_activity_id_adh_key UNIQUE (id_activity, id_adh), + CONSTRAINT galette_activities_subscriptions_id_activity_fkey FOREIGN KEY (id_activity) + REFERENCES galette_activities_activities (id_activity) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT galette_activities_subscriptions_id_adh_fkey FOREIGN KEY (id_adh) + REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; SET FOREIGN_KEY_CHECKS=1; diff --git a/scripts/pgsql.sql b/scripts/pgsql.sql index 4341c33..76fe829 100644 --- a/scripts/pgsql.sql +++ b/scripts/pgsql.sql @@ -39,9 +39,9 @@ CREATE SEQUENCE galette_activities_subscriptions_id_seq DROP TABLE IF EXISTS galette_activities_subscriptions CASCADE; CREATE TABLE galette_activities_subscriptions ( id_subscription integer DEFAULT nextval('galette_activities_subscriptions_id_seq'::text) NOT NULL, - id_activity integer REFERENCES galette_activities_activities (id_activity) ON DELETE CASCADE ON UPDATE CASCADE, - id_adh integer REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, - is_paid boolean default FALSE, + id_activity integer NOT NULL REFERENCES galette_activities_activities (id_activity) ON DELETE CASCADE ON UPDATE CASCADE, + id_adh integer NOT NULL REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, + is_paid boolean NOT NULL default FALSE, payment_amount decimal(15,2) NULL DEFAULT NULL, payment_method smallint default '0' NOT NULL, creation_date date NOT NULL, diff --git a/scripts/upgrade-to-1.1-mysql.sql b/scripts/upgrade-to-1.1-mysql.sql new file mode 100644 index 0000000..90b7139 --- /dev/null +++ b/scripts/upgrade-to-1.1-mysql.sql @@ -0,0 +1,65 @@ +-- +-- This file is part of Galette Activities plugin (https://galette.eu). +-- SPDX-FileCopyrightText: Copyright © 2024-2026 The Galette Team +-- SPDX-License-Identifier: GPL-3.0-or-later +-- + +-- Align schema with PostgreSQL one: same foreign keys names and actions, +-- no default amount. Foreign keys names depend on the MySQL version that +-- created them, and MySQL cannot drop them conditionally: tables are rebuilt. +-- Foreign keys are added with checks enabled: without them, MariaDB records +-- ON DELETE RESTRICT as NO ACTION. +SET FOREIGN_KEY_CHECKS=0; + +CREATE TABLE galette_activities_activities_new ( + id_activity int(10) NOT NULL auto_increment, + name varchar(150) NOT NULL, + type varchar(3) NOT NULL default '', + price decimal(15, 2) default NULL, + id_group int unsigned default NULL, + creation_date date NOT NULL, + comment text, + PRIMARY KEY (id_activity) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_activities_activities_new (id_activity, name, type, price, id_group, creation_date, comment) +SELECT id_activity, name, type, price, id_group, creation_date, comment +FROM galette_activities_activities; + +CREATE TABLE galette_activities_subscriptions_new ( + id_subscription int(10) NOT NULL auto_increment, + id_activity int(10) NOT NULL, + id_adh int(10) unsigned NOT NULL, + is_paid tinyint(1) NOT NULL default 0, + payment_amount decimal(15, 2) default NULL, + payment_method tinyint(3) unsigned NOT NULL default '0', + creation_date date NOT NULL, + subscription_date date NOT NULL, + end_date date NOT NULL, + comment text, + PRIMARY KEY (id_subscription), + CONSTRAINT galette_activities_subscriptions_id_activity_id_adh_key UNIQUE (id_activity, id_adh) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_activities_subscriptions_new (id_subscription, id_activity, id_adh, is_paid, payment_amount, + payment_method, creation_date, subscription_date, end_date, comment) +SELECT id_subscription, id_activity, id_adh, is_paid, payment_amount, + payment_method, creation_date, subscription_date, end_date, comment +FROM galette_activities_subscriptions; + +DROP TABLE galette_activities_subscriptions, galette_activities_activities; + +RENAME TABLE galette_activities_activities_new TO galette_activities_activities, + galette_activities_subscriptions_new TO galette_activities_subscriptions; + +SET FOREIGN_KEY_CHECKS=1; + +ALTER TABLE galette_activities_activities + ADD CONSTRAINT galette_activities_activities_id_group_fkey FOREIGN KEY (id_group) + REFERENCES galette_groups (id_group) ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE galette_activities_subscriptions + ADD CONSTRAINT galette_activities_subscriptions_id_activity_fkey FOREIGN KEY (id_activity) + REFERENCES galette_activities_activities (id_activity) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT galette_activities_subscriptions_id_adh_fkey FOREIGN KEY (id_adh) + REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/scripts/upgrade-to-1.1-pgsql.sql b/scripts/upgrade-to-1.1-pgsql.sql new file mode 100644 index 0000000..d25e590 --- /dev/null +++ b/scripts/upgrade-to-1.1-pgsql.sql @@ -0,0 +1,15 @@ +-- +-- This file is part of Galette Activities plugin (https://galette.eu). +-- SPDX-FileCopyrightText: Copyright © 2024-2026 The Galette Team +-- SPDX-License-Identifier: GPL-3.0-or-later +-- + +-- Align schema with MySQL one: subscriptions always have an activity, a member +-- and a payment status. +DELETE FROM galette_activities_subscriptions WHERE id_activity IS NULL OR id_adh IS NULL; +UPDATE galette_activities_subscriptions SET is_paid = FALSE WHERE is_paid IS NULL; + +ALTER TABLE galette_activities_subscriptions + ALTER COLUMN id_activity SET NOT NULL, + ALTER COLUMN id_adh SET NOT NULL, + ALTER COLUMN is_paid SET NOT NULL; diff --git a/tests/GaletteActivities/tests/units/PluginGaletteActivities.php b/tests/GaletteActivities/tests/units/PluginGaletteActivities.php index 7ae6f67..da957e1 100644 --- a/tests/GaletteActivities/tests/units/PluginGaletteActivities.php +++ b/tests/GaletteActivities/tests/units/PluginGaletteActivities.php @@ -125,4 +125,24 @@ public function testIsInstalled(): void { $this->assertTrue($this->getPlugin()->isInstalled()); } + + /** + * Tables created before 1.1 are detected; on MySQL, schema changes cannot be rolled back, + * the CI upgrade job covers them + */ + public function testLegacyDbVersion(): void + { + $plugin = $this->getPlugin(); + $this->assertNull($plugin->getLegacyDbVersion()); + + if ($this->zdb->isPostgres()) { + //rolled back with the test transaction + $this->zdb->db->query( + 'ALTER TABLE ' . PREFIX_DB . ACTIVITIES_PREFIX . \GaletteActivities\Entity\Subscription::TABLE + . ' ALTER COLUMN ' . \GaletteActivities\Entity\Activity::PK . ' DROP NOT NULL', + \Laminas\Db\Adapter\Adapter::QUERY_MODE_EXECUTE + ); + $this->assertSame(1.0, $plugin->getLegacyDbVersion()); + } + } } From 16ce6cc1456541f95bc2d9f747c1a1956adc2b67 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sun, 27 Sep 2026 08:39:10 +0200 Subject: [PATCH 2/3] Check upgrade from previous release in CI --- .github/workflows/ci-linux.yml | 94 ++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index b1c0e4e..13ffd83 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -204,3 +204,97 @@ jobs: run: | cd galette-core/galette/plugins/plugin-activities ../../vendor/bin/phpunit --test-suffix=.php --bootstrap tests/TestsBootstrap.php --no-coverage --process-isolation tests/GaletteActivities/ + + upgrade: + runs-on: ubuntu-latest + + strategy: + matrix: + db-image: ['mysql:8.4', 'mariadb:11', 'postgres:17'] + fail-fast: false + + env: + DB: ${{ matrix.db-image }} + + services: + # Label used to access the service container + db: + # Docker Hub image + image: ${{ matrix.db-image }} + # Provide env variables for both mysql and pgsql + env: + POSTGRES_USER: galette_tests + POSTGRES_PASSWORD: g@l3tte + POSTGRES_DB: galette_tests + MYSQL_USER: galette_tests + MYSQL_PASSWORD: g@l3tte + MYSQL_ROOT_PASSWORD: g@l3tte + MYSQL_DATABASE: galette_tests + # Open network ports for both mysql and pgsql + ports: + - 3306:3306 + - 5432:5432 + # Set health checks to wait until postgres has started + options: >- + --health-cmd="bash -c 'if [[ -n $(command -v pg_isready) ]]; then pg_isready; else if [[ -n $(command -v mysqladmin) ]]; then mysqladmin ping; else mariadb-admin ping; fi fi'" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + + name: Upgrade from previous release on ${{ matrix.db-image }} + + steps: + - name: PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + tools: composer, pecl + coverage: none + extensions: apcu + ini-values: apc.enable_cli=1 + + - name: Build Galette + uses: galette/.github/actions/build-galette@main + with: + php-version: '8.4' + + - name: Checkout plugin + uses: actions/checkout@v7 + with: + path: galette-core/galette/plugins/plugin-activities + fetch-depth: 0 + + - name: Find previous release + run: | + cd galette-core/galette/plugins/plugin-activities + echo "PREVIOUS_RELEASE=$(git tag --list '[0-9]*' --sort=-v:refname --no-contains HEAD | head -n1)" >> $GITHUB_ENV + + - name: Install previous release for PostgreSQL + env: + PGPASSWORD: g@l3tte + run: | + cd galette-core + bin/console galette:install -v --dbtype=pgsql --dbhost=localhost --dbname=galette_tests --dbuser=galette_tests --dbpass=g@l3tte --admin=admin --password=admin --no-interaction -w + git -C galette/plugins/plugin-activities show "$PREVIOUS_RELEASE:scripts/pgsql.sql" \ + | psql -v ON_ERROR_STOP=1 -h localhost -U galette_tests galette_tests + if: startsWith(matrix.db-image, 'postgres') + + - name: Install previous release for MariaDB + run: | + cd galette-core + mysql -e 'create database IF NOT EXISTS galette_tests;' -u galette_tests --password=g@l3tte -h 127.0.0.1 -P 3306 + bin/console galette:install -v --dbtype=mysql --dbhost=127.0.0.1 --dbname=galette_tests --dbuser=galette_tests --dbpass=g@l3tte --admin=admin --password=admin --no-interaction -w + git -C galette/plugins/plugin-activities show "$PREVIOUS_RELEASE:scripts/mysql.sql" \ + | mysql -u galette_tests --password=g@l3tte -h 127.0.0.1 -P 3306 galette_tests + if: startsWith(matrix.db-image, 'mysql') || startsWith(matrix.db-image, 'mariadb') + + - name: Upgrade + run: | + cd galette-core + bin/console galette:plugins:install-db --no-interaction plugin-activities | tee upgrade.log + grep -q 'Database for plugin "plugin-activities" upgraded' upgrade.log + + - name: Unit tests + run: | + cd galette-core/galette/plugins/plugin-activities + ../../vendor/bin/phpunit --test-suffix=.php --bootstrap tests/TestsBootstrap.php --no-coverage --process-isolation tests/GaletteActivities/ From 86deef1c93581ec3f681370295d43ca6957f6ddb Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sun, 27 Sep 2026 08:47:40 +0200 Subject: [PATCH 3/3] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/ci-linux.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 13ffd83..b8265f6 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -1,5 +1,8 @@ name: Linux +permissions: + contents: read + on: push: branches: