Skip to content

Commit 7a6014c

Browse files
turegjorupclaude
andcommitted
feat: prep db identifiers for cross-platform portability
- Rename 15 `changed_idx` indexes to `<table>_changed_idx` (Postgres scopes index names schema-wide; MariaDB scopes per-table — the shared name collides on Postgres). - Quote `user` table identifier in entity metadata (`#[ORM\Table(name: '`user`')]`). Doctrine emits the platform-native quote on every reference (`` `user` `` on MariaDB, `"user"` on Postgres), so no table rename is needed. - Add `validate-doctrine-schema-postgres` CI job that applies entity metadata to a Postgres 16 service container via `doctrine:schema:update --force --complete` and then runs `doctrine:schema:validate`. Gates against future entity-level Postgres regressions. Lands on 2.7 so the consolidated 3.0 migration (os2display#441 / os2display#442) can emit the portable shape from the start, keeping `migrations:rollup` honest for 2.x → 3.0 upgraders. Verified locally on MariaDB: migration applies cleanly, `doctrine:schema:validate` in sync, full PHPUnit suite green (133 tests / 516 assertions, identical to pristine release/2.7.0). Verified on Postgres 16: `doctrine:schema:update --force --complete` applies the metadata in 265 queries, `doctrine:schema:validate` in sync. down/up cycle of the new migration also verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4953015 commit 7a6014c

19 files changed

Lines changed: 143 additions & 15 deletions

.github/workflows/pr.yaml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,70 @@ jobs:
104104
- name: Validate Doctrine schema
105105
run: APP_ENV=prod php bin/console doctrine:schema:validate
106106

107+
validate-doctrine-schema-postgres:
108+
runs-on: ubuntu-latest
109+
env:
110+
DATABASE_URL: postgresql://db:db@127.0.0.1:5432/db?serverVersion=16&charset=utf8
111+
strategy:
112+
fail-fast: false
113+
matrix:
114+
php: ["8.3"]
115+
name: Validate Doctrine Schema on Postgres (PHP ${{ matrix.php }})
116+
services:
117+
postgres:
118+
image: postgres:16-alpine
119+
env:
120+
POSTGRES_USER: db
121+
POSTGRES_PASSWORD: db
122+
POSTGRES_DB: db
123+
ports:
124+
- 5432:5432
125+
options: >-
126+
--health-cmd "pg_isready -U db -d db"
127+
--health-interval=5s
128+
--health-timeout=3s
129+
--health-retries=10
130+
steps:
131+
- name: Checkout
132+
uses: actions/checkout@v4
133+
134+
- name: Setup PHP, with composer and extensions
135+
uses: shivammathur/setup-php@v2
136+
with:
137+
php-version: ${{ matrix.php}}
138+
# pgsql/pdo_pgsql added on top of the standard extension list so
139+
# Doctrine can connect to Postgres for the portability gate.
140+
extensions: apcu, ctype, iconv, imagick, json, pdo_pgsql, pgsql, redis, soap, xmlreader, zip
141+
coverage: none
142+
143+
- name: Get composer cache directory
144+
id: composer-cache
145+
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
146+
147+
- name: Cache composer dependencies
148+
uses: actions/cache@v4
149+
with:
150+
path: ${{ steps.composer-cache.outputs.dir }}
151+
key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
152+
restore-keys: ${{ matrix.php }}-composer-
153+
154+
- name: 'Composer install with exported .env variables'
155+
run: |
156+
set -a && source .env && set +a
157+
APP_ENV=prod composer install --no-dev -o
158+
159+
# The 2.x historical migrations use raw MariaDB SQL and can't run on
160+
# Postgres — that's the whole reason this PR exists. We instead apply
161+
# the entity layer directly via schema:update, which tests the
162+
# load-bearing claim that the *metadata* (after the rename) is
163+
# platform-portable. doctrine:schema:validate then catches any drift
164+
# between Postgres' generated DDL and what Doctrine expects.
165+
- name: Apply entity metadata to Postgres (schema:update)
166+
run: APP_ENV=prod php bin/console doctrine:schema:update --force --complete --no-interaction
167+
168+
- name: Validate Doctrine schema (Postgres)
169+
run: APP_ENV=prod php bin/console doctrine:schema:validate
170+
107171
php-cs-fixer:
108172
runs-on: ubuntu-latest
109173
strategy:

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.
66

77
## [2.7.0] - 2026-05-01
88

9+
- [#PR](https://github.com/os2display/display-api-service/pull/PR)
10+
- Renamed 15 `changed_idx` indexes to `<table>_changed_idx` for cross-platform portability (Postgres scopes index names schema-wide).
11+
- Quoted `user` table identifier in entity metadata so Doctrine emits the platform-native quote on every reference.
12+
- Added Postgres CI gate that runs `doctrine:schema:update --force --complete` + `doctrine:schema:validate` against a Postgres 16 service container.
913
- [#363](https://github.com/os2display/display-api-service/pull/363)
1014
- Added optional 'area' and 'facility' configuration fields
1115
- [#362](https://github.com/os2display/display-api-service/pull/362)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Portability prep: give the 15 `changed_idx` indexes table-scoped names so
12+
* they don't collide on platforms (such as Postgres) that scope index names
13+
* schema-wide rather than per-table.
14+
*
15+
* No-op for MariaDB at runtime. Landing on 2.7 means the consolidated 3.0
16+
* migration can emit the portable shape from the start, keeping
17+
* `migrations:rollup` honest for 2.x → 3.0 upgraders. The reserved-keyword
18+
* problem with the `user` table is handled separately by backtick-quoting
19+
* the identifier in the entity attribute (no rename needed).
20+
*/
21+
final class Version20260507120000 extends AbstractMigration
22+
{
23+
private const CHANGED_IDX_TABLES = [
24+
'feed',
25+
'feed_source',
26+
'media',
27+
'playlist',
28+
'playlist_screen_region',
29+
'playlist_slide',
30+
'screen',
31+
'screen_campaign',
32+
'screen_group',
33+
'screen_group_campaign',
34+
'screen_layout',
35+
'screen_layout_regions',
36+
'slide',
37+
'template',
38+
'theme',
39+
];
40+
41+
public function getDescription(): string
42+
{
43+
return 'Uniquify the 15 `changed_idx` indexes (table-scoped names) for cross-platform portability.';
44+
}
45+
46+
public function up(Schema $schema): void
47+
{
48+
foreach (self::CHANGED_IDX_TABLES as $table) {
49+
$this->addSql(sprintf('ALTER TABLE `%s` RENAME INDEX `changed_idx` TO `%s_changed_idx`', $table, $table));
50+
}
51+
}
52+
53+
public function down(Schema $schema): void
54+
{
55+
foreach (self::CHANGED_IDX_TABLES as $table) {
56+
$this->addSql(sprintf('ALTER TABLE `%s` RENAME INDEX `%s_changed_idx` TO `changed_idx`', $table, $table));
57+
}
58+
}
59+
}

src/Entity/ScreenLayout.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
#[ORM\Entity(repositoryClass: ScreenLayoutRepository::class)]
1919
#[ORM\EntityListeners([\App\EventListener\ScreenLayoutDoctrineEventListener::class])]
20-
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
20+
#[ORM\Index(fields: ['changed'], name: 'screen_layout_changed_idx')]
2121
class ScreenLayout extends AbstractBaseEntity implements MultiTenantInterface, RelationsChecksumInterface
2222
{
2323
use MultiTenantTrait;

src/Entity/ScreenLayoutRegions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
#[ORM\Entity(repositoryClass: ScreenLayoutRegionsRepository::class)]
1919
#[ORM\EntityListeners([\App\EventListener\ScreenLayoutRegionsDoctrineEventListener::class])]
20-
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
20+
#[ORM\Index(fields: ['changed'], name: 'screen_layout_regions_changed_idx')]
2121
class ScreenLayoutRegions extends AbstractBaseEntity implements MultiTenantInterface, RelationsChecksumInterface
2222
{
2323
use MultiTenantTrait;

src/Entity/Template.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
#[ORM\Entity(repositoryClass: TemplateRepository::class)]
1919
#[ORM\EntityListeners([\App\EventListener\TemplateDoctrineEventListener::class])]
20-
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
20+
#[ORM\Index(fields: ['changed'], name: 'template_changed_idx')]
2121
class Template extends AbstractBaseEntity implements MultiTenantInterface, RelationsChecksumInterface
2222
{
2323
use MultiTenantTrait;

src/Entity/Tenant/Feed.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
#[ORM\Entity(repositoryClass: FeedRepository::class)]
1313
#[ORM\EntityListeners([\App\EventListener\FeedDoctrineEventListener::class])]
14-
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
14+
#[ORM\Index(fields: ['changed'], name: 'feed_changed_idx')]
1515
class Feed extends AbstractTenantScopedEntity implements RelationsChecksumInterface
1616
{
1717
use RelationsChecksumTrait;

src/Entity/Tenant/FeedSource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
#[ORM\Entity(repositoryClass: FeedSourceRepository::class)]
1616
#[ORM\EntityListeners([\App\EventListener\FeedSourceDoctrineEventListener::class])]
17-
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
17+
#[ORM\Index(fields: ['changed'], name: 'feed_source_changed_idx')]
1818
class FeedSource extends AbstractTenantScopedEntity implements RelationsChecksumInterface
1919
{
2020
use EntityTitleDescriptionTrait;

src/Entity/Tenant/Media.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#[Vich\Uploadable]
1919
#[ORM\Entity(repositoryClass: MediaRepository::class)]
2020
#[ORM\EntityListeners([\App\EventListener\MediaDoctrineEventListener::class])]
21-
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
21+
#[ORM\Index(fields: ['changed'], name: 'media_changed_idx')]
2222
class Media extends AbstractTenantScopedEntity implements RelationsChecksumInterface
2323
{
2424
use EntityTitleDescriptionTrait;

src/Entity/Tenant/Playlist.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use Doctrine\ORM\Mapping as ORM;
1818

1919
#[ORM\Entity(repositoryClass: PlaylistRepository::class)]
20-
#[ORM\Index(fields: ['changed'], name: 'changed_idx')]
20+
#[ORM\Index(fields: ['changed'], name: 'playlist_changed_idx')]
2121
class Playlist extends AbstractTenantScopedEntity implements MultiTenantInterface, RelationsChecksumInterface
2222
{
2323
use EntityPublishedTrait;

0 commit comments

Comments
 (0)