diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 7856819..61a4dfa 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -15,6 +15,9 @@ concurrency: group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true +permissions: + contents: read + jobs: setup-phpunit-matrix: runs-on: ubuntu-latest @@ -203,3 +206,97 @@ jobs: run: | cd galette-core/galette/plugins/plugin-auto ../../vendor/bin/phpunit --test-suffix=.php --bootstrap tests/TestsBootstrap.php --no-coverage --process-isolation tests/GaletteAuto/ + + 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-auto + fetch-depth: 0 + + - name: Find previous release + run: | + cd galette-core/galette/plugins/plugin-auto + 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-auto 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-auto 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-auto | tee upgrade.log + grep -q 'Database for plugin "plugin-auto" upgraded' upgrade.log + + - name: Unit tests + run: | + cd galette-core/galette/plugins/plugin-auto + ../../vendor/bin/phpunit --test-suffix=.php --bootstrap tests/TestsBootstrap.php --no-coverage --process-isolation tests/GaletteAuto/ diff --git a/_define.php b/_define.php index 0efbf93..cf87de8 100644 --- a/_define.php +++ b/_define.php @@ -61,5 +61,5 @@ 'autoPreferences' => 'admin', 'storeAutoPreferences' => 'admin' ], - dbver: 1.00 + dbver: 1.1 ); diff --git a/lib/GaletteAuto/PluginEventProvider.php b/lib/GaletteAuto/PluginEventProvider.php new file mode 100644 index 0000000..0833716 --- /dev/null +++ b/lib/GaletteAuto/PluginEventProvider.php @@ -0,0 +1,55 @@ + + */ +class PluginEventProvider implements ListenerSubscriber +{ + /** + * Constructor + * + * Built while plugins are loaded: the vehicles repository is resolved + * only when an event is emitted. + * + * @param ContainerInterface $container Container + */ + public function __construct(private readonly ContainerInterface $container) + { + } + + /** + * Set up listeners + * + * @param ListenerRegistry $acceptor Listener + */ + public function subscribeListeners(ListenerRegistry $acceptor): void + { + $acceptor->subscribeTo( + 'member.before_remove', + function (GaletteEvent $event): void { + /** @var \ArrayObject $member */ + $member = $event->getObject(); + $this->container->get(Vehicles::class)->removeForMember((int)$member[Adherent::PK]); + } + ); + } +} diff --git a/lib/GaletteAuto/PluginGaletteAuto.php b/lib/GaletteAuto/PluginGaletteAuto.php index 9fb453a..c494c6b 100644 --- a/lib/GaletteAuto/PluginGaletteAuto.php +++ b/lib/GaletteAuto/PluginGaletteAuto.php @@ -21,6 +21,8 @@ use Galette\Core\Plugins\PublicPagesProviderInterface; use Galette\Entity\Adherent; use Galette\Core\GalettePlugin; +use Laminas\Db\Metadata\Object\ConstraintObject; +use Laminas\Db\Metadata\Source\Factory; /** * Galette Auto plugin main class @@ -270,4 +272,21 @@ public function isInstalled(): bool && $this->zdb->tableExists(AUTO_PREFIX . Transmission::TABLE) ; } + + /** + * Version of tables installed before plugins versions were recorded + * + * Vehicles of a removed member are removed along since 1.1. + */ + public function getLegacyDbVersion(): ?float + { + $metadata = Factory::createSourceFromAdapter($this->zdb->db); + /** @var ConstraintObject $constraint */ + foreach ($metadata->getConstraints(PREFIX_DB . AUTO_PREFIX . Auto::TABLE) as $constraint) { + if ($constraint->isForeignKey() && $constraint->getColumns() === [Adherent::PK]) { + return $constraint->getDeleteRule() === 'CASCADE' ? null : 1.0; + } + } + return null; + } } diff --git a/lib/GaletteAuto/Repository/Vehicles.php b/lib/GaletteAuto/Repository/Vehicles.php index 5b46109..d9e41da 100644 --- a/lib/GaletteAuto/Repository/Vehicles.php +++ b/lib/GaletteAuto/Repository/Vehicles.php @@ -294,6 +294,55 @@ public function remove(array $ids): void } } + /** + * Remove a member's vehicles, with their history and photos + * + * Member is also dropped from the history of vehicles they formerly + * owned: they are part of its primary key. + * + * @param int $id_adh Member ID + * + * @throws \Throwable + */ + public function removeForMember(int $id_adh): void + { + $transaction = !$this->zdb->inTransaction(); + + try { + if ($transaction) { + $this->zdb->beginTransaction(); + } + + $select = $this->zdb->select(AUTO_PREFIX . Auto::TABLE); + $select->columns([Auto::PK])->where([Adherent::PK => $id_adh]); + $ids = array_map( + fn($row) => (int)$row[Auto::PK], + $this->zdb->execute($select)->toArray() + ); + + if (count($ids) > 0) { + $this->remove($ids); + } + + $delete = $this->zdb->delete(AUTO_PREFIX . History::TABLE); + $delete->where([Adherent::PK => $id_adh]); + $this->zdb->execute($delete); + + if ($transaction) { + $this->zdb->commit(); + } + } catch (\Throwable $e) { + if ($transaction) { + $this->zdb->rollback(); + } + Analog::log( + '[' . static::class . '] Cannot remove vehicles of member #' . $id_adh . ' | ' . $e->getMessage(), + Analog::ERROR + ); + throw $e; + } + } + /** * Build vehicles select, joining their properties */ diff --git a/scripts/mysql.sql b/scripts/mysql.sql index 3b68ec9..8b4c9ff 100644 --- a/scripts/mysql.sql +++ b/scripts/mysql.sql @@ -9,65 +9,66 @@ SET FOREIGN_KEY_CHECKS=0; -- Table structure for table galette_auto_bodies DROP TABLE IF EXISTS galette_auto_bodies; CREATE TABLE galette_auto_bodies ( - id_body int(11) NOT NULL AUTO_INCREMENT, + id_body int(10) unsigned NOT NULL AUTO_INCREMENT, body varchar(50) NOT NULL, PRIMARY KEY (id_body) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- Table structure for table galette_auto_brands DROP TABLE IF EXISTS galette_auto_brands; CREATE TABLE galette_auto_brands ( - id_brand int(11) NOT NULL AUTO_INCREMENT, + id_brand int(10) unsigned NOT NULL AUTO_INCREMENT, brand varchar(50) NOT NULL, PRIMARY KEY (id_brand) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- Table structure for table galette_auto_colors DROP TABLE IF EXISTS galette_auto_colors; CREATE TABLE galette_auto_colors ( - id_color int(11) NOT NULL AUTO_INCREMENT, + id_color int(10) unsigned NOT NULL AUTO_INCREMENT, color varchar(50) NOT NULL, PRIMARY KEY (id_color) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- Table structure for table galette_auto_finitions DROP TABLE IF EXISTS galette_auto_finitions; CREATE TABLE galette_auto_finitions ( - id_finition int(11) NOT NULL AUTO_INCREMENT, + id_finition int(10) unsigned NOT NULL AUTO_INCREMENT, finition varchar(50) NOT NULL, PRIMARY KEY (id_finition) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- Table structure for table galette_auto_models DROP TABLE IF EXISTS galette_auto_models; CREATE TABLE galette_auto_models ( - id_model int(11) NOT NULL AUTO_INCREMENT, + id_model int(10) unsigned NOT NULL AUTO_INCREMENT, model varchar(50) NOT NULL, - id_brand int(11) NOT NULL, + id_brand int(10) unsigned NOT NULL, PRIMARY KEY (id_model), - FOREIGN KEY (id_brand) REFERENCES galette_auto_brands(id_brand) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + CONSTRAINT galette_auto_models_id_brand_fkey FOREIGN KEY (id_brand) + REFERENCES galette_auto_brands (id_brand) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- Table structure for table galette_auto_states DROP TABLE IF EXISTS galette_auto_states; CREATE TABLE galette_auto_states ( - id_state int(11) NOT NULL AUTO_INCREMENT, + id_state int(10) unsigned NOT NULL AUTO_INCREMENT, state varchar(50) NOT NULL, PRIMARY KEY (id_state) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- Table structure for table galette_auto_transmissions DROP TABLE IF EXISTS galette_auto_transmissions; CREATE TABLE galette_auto_transmissions ( - id_transmission int(11) NOT NULL AUTO_INCREMENT, + id_transmission int(10) unsigned NOT NULL AUTO_INCREMENT, transmission varchar(50) NOT NULL, PRIMARY KEY (id_transmission) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- Table structure for table galette_auto_cars DROP TABLE IF EXISTS galette_auto_cars; CREATE TABLE galette_auto_cars ( - id_car int(11) NOT NULL AUTO_INCREMENT, + id_car int(10) unsigned NOT NULL AUTO_INCREMENT, car_name varchar(50) NOT NULL, car_registration varchar(10) NOT NULL, car_first_registration_date date NOT NULL, @@ -80,47 +81,59 @@ CREATE TABLE galette_auto_cars ( car_horsepower int(4) DEFAULT NULL, car_engine_size int(11) DEFAULT NULL, car_fuel int(2) DEFAULT NULL, - id_color int(11) NOT NULL, - id_body int(11) NOT NULL, - id_state int(11) NOT NULL, - id_transmission int(11) NOT NULL, - id_finition int(11) NOT NULL, - id_model int(11) NOT NULL, + id_color int(10) unsigned NOT NULL, + id_body int(10) unsigned NOT NULL, + id_state int(10) unsigned NOT NULL, + id_transmission int(10) unsigned NOT NULL, + id_finition int(10) unsigned NOT NULL, + id_model int(10) unsigned NOT NULL, id_adh int(10) unsigned NOT NULL, PRIMARY KEY (id_car), - FOREIGN KEY(id_color) REFERENCES galette_auto_colors (id_color) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_body) REFERENCES galette_auto_bodies (id_body) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_state) REFERENCES galette_auto_states (id_state) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_transmission) REFERENCES galette_auto_transmissions (id_transmission) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_finition) REFERENCES galette_auto_finitions (id_finition) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_model) REFERENCES galette_auto_models (id_model) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_adh) REFERENCES galette_adherents (id_adh) ON DELETE NO ACTION ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + CONSTRAINT galette_auto_cars_id_color_fkey FOREIGN KEY (id_color) + REFERENCES galette_auto_colors (id_color) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_auto_cars_id_body_fkey FOREIGN KEY (id_body) + REFERENCES galette_auto_bodies (id_body) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_auto_cars_id_state_fkey FOREIGN KEY (id_state) + REFERENCES galette_auto_states (id_state) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_auto_cars_id_transmission_fkey FOREIGN KEY (id_transmission) + REFERENCES galette_auto_transmissions (id_transmission) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_auto_cars_id_finition_fkey FOREIGN KEY (id_finition) + REFERENCES galette_auto_finitions (id_finition) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_auto_cars_id_model_fkey FOREIGN KEY (id_model) + REFERENCES galette_auto_models (id_model) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_auto_cars_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; -- Table structure for table galette_auto_history DROP TABLE IF EXISTS galette_auto_history; CREATE TABLE galette_auto_history ( - id_car int(11) NOT NULL, + id_car int(10) unsigned NOT NULL, id_adh int(10) unsigned NOT NULL, history_date datetime NOT NULL, car_registration varchar(10) NOT NULL, - id_color int(11) NOT NULL, - id_state int(11) NOT NULL, + id_color int(10) unsigned NOT NULL, + id_state int(10) unsigned NOT NULL, PRIMARY KEY (id_car,id_adh,history_date), - FOREIGN KEY (id_car) REFERENCES galette_auto_cars (id_car) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_adh) REFERENCES galette_adherents (id_adh) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_color) REFERENCES galette_auto_colors (id_color) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY (id_state) REFERENCES galette_auto_states (id_state) ON DELETE NO ACTION ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + CONSTRAINT galette_auto_history_id_car_fkey FOREIGN KEY (id_car) + REFERENCES galette_auto_cars (id_car) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT galette_auto_history_id_adh_fkey FOREIGN KEY (id_adh) + REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT galette_auto_history_id_color_fkey FOREIGN KEY (id_color) + REFERENCES galette_auto_colors (id_color) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_auto_history_id_state_fkey FOREIGN KEY (id_state) + REFERENCES galette_auto_states (id_state) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; -- Table structure for table galette_auto_pictures DROP TABLE IF EXISTS galette_auto_pictures; CREATE TABLE galette_auto_pictures ( - id_car int(11) NOT NULL, + id_car int(10) unsigned NOT NULL, picture mediumblob NOT NULL, format varchar(10) NOT NULL DEFAULT '', PRIMARY KEY (id_car), - FOREIGN KEY (id_car) REFERENCES galette_auto_cars (id_car) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + CONSTRAINT galette_auto_pictures_id_car_fkey FOREIGN KEY (id_car) + REFERENCES galette_auto_cars (id_car) 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 40245cf..75e1547 100644 --- a/scripts/pgsql.sql +++ b/scripts/pgsql.sql @@ -105,7 +105,8 @@ DROP TABLE IF EXISTS galette_auto_models CASCADE; CREATE TABLE galette_auto_models ( id_model integer DEFAULT nextval('galette_auto_models_id_seq'::text) NOT NULL, model character varying(50) NOT NULL, - id_brand integer NOT NULL REFERENCES galette_auto_brands (id_brand) ON DELETE NO ACTION ON UPDATE NO ACTION, + id_brand integer NOT NULL + CONSTRAINT galette_auto_models_id_brand_fkey REFERENCES galette_auto_brands (id_brand) ON DELETE RESTRICT ON UPDATE CASCADE, PRIMARY KEY (id_model) ); @@ -141,33 +142,51 @@ CREATE TABLE galette_auto_cars ( car_horsepower integer DEFAULT NULL, car_engine_size integer DEFAULT NULL, car_fuel integer DEFAULT NULL, - id_color integer NOT NULL REFERENCES galette_auto_colors (id_color) ON DELETE NO ACTION ON UPDATE NO ACTION, - id_body integer NOT NULL REFERENCES galette_auto_bodies (id_body) ON DELETE NO ACTION ON UPDATE NO ACTION, - id_state integer NOT NULL REFERENCES galette_auto_states (id_state) ON DELETE NO ACTION ON UPDATE NO ACTION, - id_transmission integer NOT NULL REFERENCES galette_auto_transmissions (id_transmission) ON DELETE NO ACTION ON UPDATE NO ACTION, - id_finition integer NOT NULL REFERENCES galette_auto_finitions (id_finition) ON DELETE NO ACTION ON UPDATE NO ACTION, - id_model integer NOT NULL REFERENCES galette_auto_models (id_model) ON DELETE NO ACTION ON UPDATE NO ACTION, - id_adh integer NOT NULL REFERENCES galette_adherents (id_adh) ON DELETE NO ACTION ON UPDATE NO ACTION, + id_color integer NOT NULL + CONSTRAINT galette_auto_cars_id_color_fkey REFERENCES galette_auto_colors (id_color) ON DELETE RESTRICT ON UPDATE CASCADE, + id_body integer NOT NULL + CONSTRAINT galette_auto_cars_id_body_fkey REFERENCES galette_auto_bodies (id_body) ON DELETE RESTRICT ON UPDATE CASCADE, + id_state integer NOT NULL + CONSTRAINT galette_auto_cars_id_state_fkey REFERENCES galette_auto_states (id_state) ON DELETE RESTRICT ON UPDATE CASCADE, + id_transmission integer NOT NULL + CONSTRAINT galette_auto_cars_id_transmission_fkey REFERENCES galette_auto_transmissions (id_transmission) ON DELETE RESTRICT ON UPDATE CASCADE, + id_finition integer NOT NULL + CONSTRAINT galette_auto_cars_id_finition_fkey REFERENCES galette_auto_finitions (id_finition) ON DELETE RESTRICT ON UPDATE CASCADE, + id_model integer NOT NULL + CONSTRAINT galette_auto_cars_id_model_fkey REFERENCES galette_auto_models (id_model) ON DELETE RESTRICT ON UPDATE CASCADE, + id_adh integer NOT NULL + CONSTRAINT galette_auto_cars_id_adh_fkey REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (id_car) ); -- Table structure for table galette_auto_history -DROP TABLE IF EXISTS galette_auto_history; +DROP TABLE IF EXISTS galette_auto_history CASCADE; CREATE TABLE galette_auto_history ( - id_car integer NOT NULL REFERENCES galette_auto_cars (id_car) ON DELETE NO ACTION ON UPDATE NO ACTION, - id_adh integer NOT NULL REFERENCES galette_adherents (id_adh) ON DELETE NO ACTION ON UPDATE NO ACTION, + id_car integer NOT NULL + CONSTRAINT galette_auto_history_id_car_fkey REFERENCES galette_auto_cars (id_car) ON DELETE CASCADE ON UPDATE CASCADE, + id_adh integer NOT NULL + CONSTRAINT galette_auto_history_id_adh_fkey REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, history_date timestamp NOT NULL, car_registration character varying(10) NOT NULL, - id_color integer NOT NULL REFERENCES galette_auto_colors (id_color) ON DELETE NO ACTION ON UPDATE NO ACTION, - id_state integer NOT NULL REFERENCES galette_auto_states (id_state) ON DELETE NO ACTION ON UPDATE NO ACTION, + id_color integer NOT NULL + CONSTRAINT galette_auto_history_id_color_fkey REFERENCES galette_auto_colors (id_color) ON DELETE RESTRICT ON UPDATE CASCADE, + id_state integer NOT NULL + CONSTRAINT galette_auto_history_id_state_fkey REFERENCES galette_auto_states (id_state) ON DELETE RESTRICT ON UPDATE CASCADE, PRIMARY KEY (id_car,id_adh,history_date) ); -- Table structure for table galette_auto_pictures DROP TABLE IF EXISTS galette_auto_pictures CASCADE; CREATE TABLE galette_auto_pictures ( - id_car integer NOT NULL REFERENCES galette_auto_cars (id_car) ON DELETE CASCADE ON UPDATE CASCADE, + id_car integer NOT NULL + CONSTRAINT galette_auto_pictures_id_car_fkey REFERENCES galette_auto_cars (id_car) ON DELETE CASCADE ON UPDATE CASCADE, picture bytea NOT NULL, format character varying(10) NOT NULL DEFAULT '', PRIMARY KEY (id_car) ); + +-- Foreign keys are not indexed by PostgreSQL +CREATE INDEX galette_auto_models_id_brand_idx ON galette_auto_models (id_brand); +CREATE INDEX galette_auto_cars_id_model_idx ON galette_auto_cars (id_model); +CREATE INDEX galette_auto_cars_id_adh_idx ON galette_auto_cars (id_adh); +CREATE INDEX galette_auto_history_id_adh_idx ON galette_auto_history (id_adh); diff --git a/scripts/upgrade-to-1.1-mysql.sql b/scripts/upgrade-to-1.1-mysql.sql new file mode 100644 index 0000000..6d0544e --- /dev/null +++ b/scripts/upgrade-to-1.1-mysql.sql @@ -0,0 +1,188 @@ +-- +-- This file is part of Galette Auto plugin (https://galette.eu). +-- SPDX-FileCopyrightText: Copyright © 2009-2026 The Galette Team +-- SPDX-License-Identifier: GPL-3.0-or-later +-- + +-- Align schema with PostgreSQL one: utf8mb4, unsigned identifiers, same +-- foreign keys on both engines. A member removal removes their vehicles. +-- Foreign keys names depend on the MySQL version that created them, and +-- MySQL cannot drop them conditionally: tables are rebuilt. Foreign keys +-- are added once every identifier has its new type, with checks enabled: +-- without them, MariaDB records ON DELETE RESTRICT as NO ACTION. +SET FOREIGN_KEY_CHECKS=0; + +CREATE TABLE galette_auto_bodies_new ( + id_body int(10) unsigned NOT NULL AUTO_INCREMENT, + body varchar(50) NOT NULL, + PRIMARY KEY (id_body) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_bodies_new (id_body, body) +SELECT id_body, body +FROM galette_auto_bodies; + +CREATE TABLE galette_auto_brands_new ( + id_brand int(10) unsigned NOT NULL AUTO_INCREMENT, + brand varchar(50) NOT NULL, + PRIMARY KEY (id_brand) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_brands_new (id_brand, brand) +SELECT id_brand, brand +FROM galette_auto_brands; + +CREATE TABLE galette_auto_colors_new ( + id_color int(10) unsigned NOT NULL AUTO_INCREMENT, + color varchar(50) NOT NULL, + PRIMARY KEY (id_color) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_colors_new (id_color, color) +SELECT id_color, color +FROM galette_auto_colors; + +CREATE TABLE galette_auto_finitions_new ( + id_finition int(10) unsigned NOT NULL AUTO_INCREMENT, + finition varchar(50) NOT NULL, + PRIMARY KEY (id_finition) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_finitions_new (id_finition, finition) +SELECT id_finition, finition +FROM galette_auto_finitions; + +CREATE TABLE galette_auto_models_new ( + id_model int(10) unsigned NOT NULL AUTO_INCREMENT, + model varchar(50) NOT NULL, + id_brand int(10) unsigned NOT NULL, + PRIMARY KEY (id_model) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_models_new (id_model, model, id_brand) +SELECT id_model, model, id_brand +FROM galette_auto_models; + +CREATE TABLE galette_auto_states_new ( + id_state int(10) unsigned NOT NULL AUTO_INCREMENT, + state varchar(50) NOT NULL, + PRIMARY KEY (id_state) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_states_new (id_state, state) +SELECT id_state, state +FROM galette_auto_states; + +CREATE TABLE galette_auto_transmissions_new ( + id_transmission int(10) unsigned NOT NULL AUTO_INCREMENT, + transmission varchar(50) NOT NULL, + PRIMARY KEY (id_transmission) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_transmissions_new (id_transmission, transmission) +SELECT id_transmission, transmission +FROM galette_auto_transmissions; + +CREATE TABLE galette_auto_cars_new ( + id_car int(10) unsigned NOT NULL AUTO_INCREMENT, + car_name varchar(50) NOT NULL, + car_registration varchar(10) NOT NULL, + car_first_registration_date date NOT NULL, + car_first_circulation_date date NOT NULL, + car_mileage int(10) DEFAULT NULL, + car_comment text, + car_creation_date date NOT NULL, + car_chassis_number varchar(50) DEFAULT NULL, + car_seats int(1) DEFAULT NULL, + car_horsepower int(4) DEFAULT NULL, + car_engine_size int(11) DEFAULT NULL, + car_fuel int(2) DEFAULT NULL, + id_color int(10) unsigned NOT NULL, + id_body int(10) unsigned NOT NULL, + id_state int(10) unsigned NOT NULL, + id_transmission int(10) unsigned NOT NULL, + id_finition int(10) unsigned NOT NULL, + id_model int(10) unsigned NOT NULL, + id_adh int(10) unsigned NOT NULL, + PRIMARY KEY (id_car) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_cars_new (id_car, car_name, car_registration, car_first_registration_date, car_first_circulation_date, car_mileage, car_comment, car_creation_date, car_chassis_number, car_seats, car_horsepower, car_engine_size, car_fuel, id_color, id_body, id_state, id_transmission, id_finition, id_model, id_adh) +SELECT id_car, car_name, car_registration, car_first_registration_date, car_first_circulation_date, car_mileage, car_comment, car_creation_date, car_chassis_number, car_seats, car_horsepower, car_engine_size, car_fuel, id_color, id_body, id_state, id_transmission, id_finition, id_model, id_adh +FROM galette_auto_cars; + +CREATE TABLE galette_auto_history_new ( + id_car int(10) unsigned NOT NULL, + id_adh int(10) unsigned NOT NULL, + history_date datetime NOT NULL, + car_registration varchar(10) NOT NULL, + id_color int(10) unsigned NOT NULL, + id_state int(10) unsigned NOT NULL, + PRIMARY KEY (id_car,id_adh,history_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_history_new (id_car, id_adh, history_date, car_registration, id_color, id_state) +SELECT id_car, id_adh, history_date, car_registration, id_color, id_state +FROM galette_auto_history; + +CREATE TABLE galette_auto_pictures_new ( + id_car int(10) unsigned NOT NULL, + picture mediumblob NOT NULL, + format varchar(10) NOT NULL DEFAULT '', + PRIMARY KEY (id_car) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_auto_pictures_new (id_car, picture, format) +SELECT id_car, picture, format +FROM galette_auto_pictures; + +DROP TABLE galette_auto_bodies, galette_auto_brands, galette_auto_colors, galette_auto_finitions, + galette_auto_models, galette_auto_states, galette_auto_transmissions, galette_auto_cars, + galette_auto_history, galette_auto_pictures; + +RENAME TABLE galette_auto_bodies_new TO galette_auto_bodies, + galette_auto_brands_new TO galette_auto_brands, + galette_auto_colors_new TO galette_auto_colors, + galette_auto_finitions_new TO galette_auto_finitions, + galette_auto_models_new TO galette_auto_models, + galette_auto_states_new TO galette_auto_states, + galette_auto_transmissions_new TO galette_auto_transmissions, + galette_auto_cars_new TO galette_auto_cars, + galette_auto_history_new TO galette_auto_history, + galette_auto_pictures_new TO galette_auto_pictures; + +SET FOREIGN_KEY_CHECKS=1; + +ALTER TABLE galette_auto_models + ADD CONSTRAINT galette_auto_models_id_brand_fkey FOREIGN KEY (id_brand) + REFERENCES galette_auto_brands (id_brand) ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE galette_auto_cars + ADD CONSTRAINT galette_auto_cars_id_color_fkey FOREIGN KEY (id_color) + REFERENCES galette_auto_colors (id_color) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_body_fkey FOREIGN KEY (id_body) + REFERENCES galette_auto_bodies (id_body) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_state_fkey FOREIGN KEY (id_state) + REFERENCES galette_auto_states (id_state) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_transmission_fkey FOREIGN KEY (id_transmission) + REFERENCES galette_auto_transmissions (id_transmission) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_finition_fkey FOREIGN KEY (id_finition) + REFERENCES galette_auto_finitions (id_finition) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_model_fkey FOREIGN KEY (id_model) + REFERENCES galette_auto_models (id_model) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_adh_fkey FOREIGN KEY (id_adh) + REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE galette_auto_history + ADD CONSTRAINT galette_auto_history_id_car_fkey FOREIGN KEY (id_car) + REFERENCES galette_auto_cars (id_car) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_history_id_adh_fkey FOREIGN KEY (id_adh) + REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_history_id_color_fkey FOREIGN KEY (id_color) + REFERENCES galette_auto_colors (id_color) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_history_id_state_fkey FOREIGN KEY (id_state) + REFERENCES galette_auto_states (id_state) ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE galette_auto_pictures + ADD CONSTRAINT galette_auto_pictures_id_car_fkey FOREIGN KEY (id_car) + REFERENCES galette_auto_cars (id_car) 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..d39eb5d --- /dev/null +++ b/scripts/upgrade-to-1.1-pgsql.sql @@ -0,0 +1,59 @@ +-- +-- This file is part of Galette Auto plugin (https://galette.eu). +-- SPDX-FileCopyrightText: Copyright © 2009-2026 The Galette Team +-- SPDX-License-Identifier: GPL-3.0-or-later +-- + +-- Same foreign keys as MySQL: a member removal removes their vehicles +ALTER TABLE galette_auto_models + DROP CONSTRAINT IF EXISTS galette_auto_models_id_brand_fkey, + ADD CONSTRAINT galette_auto_models_id_brand_fkey FOREIGN KEY (id_brand) + REFERENCES galette_auto_brands (id_brand) ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE galette_auto_cars + DROP CONSTRAINT IF EXISTS galette_auto_cars_id_color_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_cars_id_body_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_cars_id_state_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_cars_id_transmission_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_cars_id_finition_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_cars_id_model_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_cars_id_adh_fkey, + ADD CONSTRAINT galette_auto_cars_id_color_fkey FOREIGN KEY (id_color) + REFERENCES galette_auto_colors (id_color) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_body_fkey FOREIGN KEY (id_body) + REFERENCES galette_auto_bodies (id_body) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_state_fkey FOREIGN KEY (id_state) + REFERENCES galette_auto_states (id_state) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_transmission_fkey FOREIGN KEY (id_transmission) + REFERENCES galette_auto_transmissions (id_transmission) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_finition_fkey FOREIGN KEY (id_finition) + REFERENCES galette_auto_finitions (id_finition) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_model_fkey FOREIGN KEY (id_model) + REFERENCES galette_auto_models (id_model) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_cars_id_adh_fkey FOREIGN KEY (id_adh) + REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE galette_auto_history + DROP CONSTRAINT IF EXISTS galette_auto_history_id_car_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_history_id_adh_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_history_id_color_fkey, + DROP CONSTRAINT IF EXISTS galette_auto_history_id_state_fkey, + ADD CONSTRAINT galette_auto_history_id_car_fkey FOREIGN KEY (id_car) + REFERENCES galette_auto_cars (id_car) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_history_id_adh_fkey FOREIGN KEY (id_adh) + REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_history_id_color_fkey FOREIGN KEY (id_color) + REFERENCES galette_auto_colors (id_color) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_auto_history_id_state_fkey FOREIGN KEY (id_state) + REFERENCES galette_auto_states (id_state) ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE galette_auto_pictures + DROP CONSTRAINT IF EXISTS galette_auto_pictures_id_car_fkey, + ADD CONSTRAINT galette_auto_pictures_id_car_fkey FOREIGN KEY (id_car) + REFERENCES galette_auto_cars (id_car) ON DELETE CASCADE ON UPDATE CASCADE; + +-- Foreign keys are not indexed by PostgreSQL +CREATE INDEX IF NOT EXISTS galette_auto_models_id_brand_idx ON galette_auto_models (id_brand); +CREATE INDEX IF NOT EXISTS galette_auto_cars_id_model_idx ON galette_auto_cars (id_model); +CREATE INDEX IF NOT EXISTS galette_auto_cars_id_adh_idx ON galette_auto_cars (id_adh); +CREATE INDEX IF NOT EXISTS galette_auto_history_id_adh_idx ON galette_auto_history (id_adh); diff --git a/tests/GaletteAuto/tests/units/PluginEventProvider.php b/tests/GaletteAuto/tests/units/PluginEventProvider.php new file mode 100644 index 0000000..fff0293 --- /dev/null +++ b/tests/GaletteAuto/tests/units/PluginEventProvider.php @@ -0,0 +1,164 @@ + + */ +class PluginEventProvider extends GaletteTestCase +{ + protected int $seed = 20260926203512; + protected bool $load_plugins = true; + + /** @var array */ + private array $props = []; + + /** + * Set up tests + */ + public function setUp(): void + { + parent::setUp(); + + $values = [ + 'body' => 'Berline', + 'color' => 'Grey', + 'finition' => 'Standard', + 'state' => 'Correct', + 'transmission' => 'Manual', + 'brand' => 'Peugeot', + ]; + foreach ($values as $property => $value) { + $class = '\GaletteAuto\\' . ucfirst($property); + $object = new $class($this->zdb); + $object->setValue($value); + $object->store(true); + $this->props[$property] = $object->getId(); + } + + $model = new \GaletteAuto\Model($this->zdb); + $this->assertTrue($model->check(['model' => '307', 'brand' => $this->props['brand']])); + $model->store(true); + $this->props['model'] = $model->getId(); + } + + /** + * Create a vehicle and its history entry, bypassing repository + * + * @param int $id_adh Owner ID + * @param string $name Vehicle name + */ + private function createVehicle(int $id_adh, string $name): int + { + $insert = $this->zdb->insert(AUTO_PREFIX . Auto::TABLE); + $insert->values([ + 'car_name' => $name, + 'car_registration' => 'GA-123-TE', + 'car_first_registration_date' => '2001-02-12', + 'car_first_circulation_date' => '2001-02-13', + 'car_creation_date' => date('Y-m-d'), + 'car_fuel' => Auto::FUEL_DIESEL, + \GaletteAuto\Color::PK => $this->props['color'], + \GaletteAuto\Body::PK => $this->props['body'], + \GaletteAuto\State::PK => $this->props['state'], + \GaletteAuto\Transmission::PK => $this->props['transmission'], + \GaletteAuto\Finition::PK => $this->props['finition'], + \GaletteAuto\Model::PK => $this->props['model'], + Adherent::PK => $id_adh, + ]); + $this->zdb->execute($insert); + + $select = $this->zdb->select(AUTO_PREFIX . Auto::TABLE); + $select->columns([Auto::PK])->where(['car_name' => $name])->order(Auto::PK . ' DESC')->limit(1); + $car_id = (int)$this->zdb->execute($select)->current()[Auto::PK]; + + $this->addHistory($car_id, $id_adh, '2020-01-01 10:00:00'); + return $car_id; + } + + /** + * Add a vehicle history entry + * + * @param int $car_id Vehicle ID + * @param int $id_adh Owner ID + * @param string $date Entry date + */ + private function addHistory(int $car_id, int $id_adh, string $date): void + { + $insert = $this->zdb->insert(AUTO_PREFIX . History::TABLE); + $insert->values([ + Auto::PK => $car_id, + Adherent::PK => $id_adh, + 'history_date' => $date, + 'car_registration' => 'GA-123-TE', + \GaletteAuto\Color::PK => $this->props['color'], + \GaletteAuto\State::PK => $this->props['state'], + ]); + $this->zdb->execute($insert); + } + + /** + * Get owners of a vehicle history entries, oldest first + * + * @param int $car_id Vehicle ID + * + * @return array + */ + private function getHistoryOwners(int $car_id): array + { + $select = $this->zdb->select(AUTO_PREFIX . History::TABLE); + $select->columns([Adherent::PK])->where([Auto::PK => $car_id])->order('history_date'); + return array_map( + fn($row) => (int)$row[Adherent::PK], + $this->zdb->execute($select)->toArray() + ); + } + + /** + * Removing a member removes their vehicles, and drops them from other vehicles history + */ + public function testMemberRemoval(): void + { + $member = $this->getMemberOne(); + $other = $this->getMemberTwo(); + + $own_car = $this->createVehicle($member->id, 'Own car'); + $sold_car = $this->createVehicle($member->id, 'Sold car'); + //sold car now belongs to the other member + $update = $this->zdb->update(AUTO_PREFIX . Auto::TABLE); + $update->set([Adherent::PK => $other->id])->where([Auto::PK => $sold_car]); + $this->zdb->execute($update); + $this->addHistory($sold_car, $other->id, '2021-01-01 10:00:00'); + $other_car = $this->createVehicle($other->id, 'Other car'); + + $this->assertSame([$member->id, $other->id], $this->getHistoryOwners($sold_car)); + + $this->logSuperAdmin(); + $members = new \Galette\Repository\Members(); + $this->assertTrue($members->removeMembers($member->id)); + + $vehicles = $this->container->get(\GaletteAuto\Repository\Vehicles::class); + $this->assertSame( + [$sold_car => $other->id, $other_car => $other->id], + $vehicles->getOwners([$own_car, $sold_car, $other_car]) + ); + $this->assertSame([], $this->getHistoryOwners($own_car)); + $this->assertSame([$other->id], $this->getHistoryOwners($sold_car)); + $this->assertSame([$other->id], $this->getHistoryOwners($other_car)); + } +} diff --git a/tests/GaletteAuto/tests/units/PluginGaletteAuto.php b/tests/GaletteAuto/tests/units/PluginGaletteAuto.php index f30c8f6..a178d2c 100644 --- a/tests/GaletteAuto/tests/units/PluginGaletteAuto.php +++ b/tests/GaletteAuto/tests/units/PluginGaletteAuto.php @@ -117,4 +117,14 @@ public function testPublicPage(): void $this->setRawPreference($name, \Galette\Enums\PublicPageVisibility::Inherit->value); $this->assertSame([], $plugin->getPublicMenuItems()); } + + /** + * Tables where vehicles outlive their owner come from before 1.1; the + * CI upgrade job covers them, schema changes cannot be rolled back on MySQL + */ + public function testLegacyDbVersion(): void + { + $plugin = $this->container->get(\GaletteAuto\PluginGaletteAuto::class); + $this->assertNull($plugin->getLegacyDbVersion()); + } }