Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,70 @@ jobs:
- name: Validate Doctrine schema
run: APP_ENV=prod php bin/console doctrine:schema:validate

validate-doctrine-schema-postgres:
runs-on: ubuntu-latest
env:
DATABASE_URL: postgresql://db:db@127.0.0.1:5432/db?serverVersion=16&charset=utf8
strategy:
fail-fast: false
matrix:
php: ["8.3"]
name: Validate Doctrine Schema on Postgres (PHP ${{ matrix.php }})
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: db
POSTGRES_PASSWORD: db
POSTGRES_DB: db
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U db -d db"
--health-interval=5s
--health-timeout=3s
--health-retries=10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP, with composer and extensions
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php}}
# pgsql/pdo_pgsql added on top of the standard extension list so
# Doctrine can connect to Postgres for the portability gate.
extensions: apcu, ctype, iconv, imagick, json, pdo_pgsql, pgsql, redis, soap, xmlreader, zip
coverage: none

- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ matrix.php }}-composer-

- name: 'Composer install with exported .env variables'
run: |
set -a && source .env && set +a
APP_ENV=prod composer install --no-dev -o

# The 2.x historical migrations use raw MariaDB SQL and can't run on
# Postgres — that's the whole reason this PR exists. We instead apply
# the entity layer directly via schema:update, which tests the
# load-bearing claim that the *metadata* (after the rename) is
# platform-portable. doctrine:schema:validate then catches any drift
# between Postgres' generated DDL and what Doctrine expects.
- name: Apply entity metadata to Postgres (schema:update)
run: APP_ENV=prod php bin/console doctrine:schema:update --force --complete --no-interaction

- name: Validate Doctrine schema (Postgres)
run: APP_ENV=prod php bin/console doctrine:schema:validate

php-cs-fixer:
runs-on: ubuntu-latest
strategy:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.

## [2.7.0] - 2026-05-01

- [#444](https://github.com/os2display/display-api-service/pull/444)
- Renamed 15 `changed_idx` indexes to `<table>_changed_idx` for cross-platform portability (Postgres scopes index names schema-wide).
- Quoted `user` table identifier in entity metadata so Doctrine emits the platform-native quote on every reference.
- Added Postgres CI gate that runs `doctrine:schema:update --force --complete` + `doctrine:schema:validate` against a Postgres 16 service container.
- [#363](https://github.com/os2display/display-api-service/pull/363)
- Added optional 'area' and 'facility' configuration fields
- [#362](https://github.com/os2display/display-api-service/pull/362)
Expand Down
59 changes: 59 additions & 0 deletions migrations/Version20260507120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Portability prep: give the 15 `changed_idx` indexes table-scoped names so
* they don't collide on platforms (such as Postgres) that scope index names
* schema-wide rather than per-table.
*
* No-op for MariaDB at runtime. Landing on 2.7 means the consolidated 3.0
* migration can emit the portable shape from the start, keeping
* `migrations:rollup` honest for 2.x → 3.0 upgraders. The reserved-keyword
* problem with the `user` table is handled separately by backtick-quoting
* the identifier in the entity attribute (no rename needed).
*/
final class Version20260507120000 extends AbstractMigration
{
private const CHANGED_IDX_TABLES = [
'feed',
'feed_source',
'media',
'playlist',
'playlist_screen_region',
'playlist_slide',
'screen',
'screen_campaign',
'screen_group',
'screen_group_campaign',
'screen_layout',
'screen_layout_regions',
'slide',
'template',
'theme',
];

public function getDescription(): string
{
return 'Uniquify the 15 `changed_idx` indexes (table-scoped names) for cross-platform portability.';
}

public function up(Schema $schema): void
{
foreach (self::CHANGED_IDX_TABLES as $table) {
$this->addSql(sprintf('ALTER TABLE `%s` RENAME INDEX `changed_idx` TO `%s_changed_idx`', $table, $table));
}
}

public function down(Schema $schema): void
{
foreach (self::CHANGED_IDX_TABLES as $table) {
$this->addSql(sprintf('ALTER TABLE `%s` RENAME INDEX `%s_changed_idx` TO `changed_idx`', $table, $table));
}
}
}
2 changes: 1 addition & 1 deletion src/Entity/ScreenLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

#[ORM\Entity(repositoryClass: ScreenLayoutRepository::class)]
#[ORM\EntityListeners([\App\EventListener\ScreenLayoutDoctrineEventListener::class])]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'screen_layout_changed_idx')]
class ScreenLayout extends AbstractBaseEntity implements MultiTenantInterface, RelationsChecksumInterface
{
use MultiTenantTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/ScreenLayoutRegions.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

#[ORM\Entity(repositoryClass: ScreenLayoutRegionsRepository::class)]
#[ORM\EntityListeners([\App\EventListener\ScreenLayoutRegionsDoctrineEventListener::class])]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'screen_layout_regions_changed_idx')]
class ScreenLayoutRegions extends AbstractBaseEntity implements MultiTenantInterface, RelationsChecksumInterface
{
use MultiTenantTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

#[ORM\Entity(repositoryClass: TemplateRepository::class)]
#[ORM\EntityListeners([\App\EventListener\TemplateDoctrineEventListener::class])]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'template_changed_idx')]
class Template extends AbstractBaseEntity implements MultiTenantInterface, RelationsChecksumInterface
{
use MultiTenantTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/Feed.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

#[ORM\Entity(repositoryClass: FeedRepository::class)]
#[ORM\EntityListeners([\App\EventListener\FeedDoctrineEventListener::class])]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'feed_changed_idx')]
class Feed extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use RelationsChecksumTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/FeedSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

#[ORM\Entity(repositoryClass: FeedSourceRepository::class)]
#[ORM\EntityListeners([\App\EventListener\FeedSourceDoctrineEventListener::class])]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'feed_source_changed_idx')]
class FeedSource extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use EntityTitleDescriptionTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/Media.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#[Vich\Uploadable]
#[ORM\Entity(repositoryClass: MediaRepository::class)]
#[ORM\EntityListeners([\App\EventListener\MediaDoctrineEventListener::class])]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'media_changed_idx')]
class Media extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use EntityTitleDescriptionTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/Playlist.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: PlaylistRepository::class)]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'playlist_changed_idx')]
class Playlist extends AbstractTenantScopedEntity implements MultiTenantInterface, RelationsChecksumInterface
{
use EntityPublishedTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/PlaylistScreenRegion.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

#[ORM\UniqueConstraint(name: 'unique_playlist_screen_region', columns: ['playlist_id', 'screen_id', 'region_id'])]
#[ORM\Entity(repositoryClass: PlaylistScreenRegionRepository::class)]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'playlist_screen_region_changed_idx')]
class PlaylistScreenRegion extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use RelationsChecksumTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/PlaylistSlide.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: PlaylistSlideRepository::class)]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'playlist_slide_changed_idx')]
class PlaylistSlide extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use RelationsChecksumTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/Screen.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ScreenRepository::class)]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'screen_changed_idx')]
class Screen extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use EntityTitleDescriptionTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/ScreenCampaign.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ScreenCampaignRepository::class)]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'screen_campaign_changed_idx')]
class ScreenCampaign extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use RelationsChecksumTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/ScreenGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ScreenGroupRepository::class)]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'screen_group_changed_idx')]
class ScreenGroup extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use EntityTitleDescriptionTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/ScreenGroupCampaign.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ScreenGroupCampaignRepository::class)]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'screen_group_campaign_changed_idx')]
class ScreenGroupCampaign extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use RelationsChecksumTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/Slide.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: SlideRepository::class)]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'slide_changed_idx')]
class Slide extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use EntityPublishedTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Tenant/Theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

#[ORM\Entity(repositoryClass: ThemeRepository::class)]
#[ORM\EntityListeners([\App\EventListener\ThemeDoctrineEventListener::class])]
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
#[ORM\Index(fields: ['changed'], name: 'theme_changed_idx')]
class Theme extends AbstractTenantScopedEntity implements RelationsChecksumInterface
{
use EntityTitleDescriptionTrait;
Expand Down
1 change: 1 addition & 0 deletions src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User extends AbstractBaseEntity implements UserInterface, PasswordAuthenticatedUserInterface, \JsonSerializable, TenantScopedUserInterface
{
#[Assert\NotBlank]
Expand Down
Loading