Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
/docker/ export-ignore
/phpstan.neon export-ignore
/phpunit.xml export-ignore
/harness/ export-ignore
/scripts/ export-ignore
/tests/ export-ignore
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,27 @@ jobs:
test -z "$(docker ps -aq --filter 'label=com.docker.compose.project')"
test -z "$(docker volume ls -q --filter 'label=com.docker.compose.project')"

consumer-verification:
name: Consumer verification and coexistence
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
# actions/checkout v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: ./.github/actions/setup-php-composer
with:
php-version: "8.4"
- name: Install and verify external consumer twice on fresh MySQL
run: composer test:consumer
- name: No Compose resources remain
if: always()
run: |
set -euo pipefail
test -z "$(docker ps -aq --filter 'label=com.docker.compose.project')"
test -z "$(docker volume ls -q --filter 'label=com.docker.compose.project')"

exploratory:
name: Exploratory MySQL Innovation (not required)
if: github.event_name == 'workflow_dispatch' && inputs.exploratory
Expand All @@ -225,7 +246,7 @@ jobs:
ci-gate:
name: CI gate
if: always()
needs: [workflow-lint, static, compat, integration, examples-smoke]
needs: [workflow-lint, static, compat, integration, examples-smoke, consumer-verification]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ composer test:unit # no Docker needed
composer test:regression # no Docker needed
composer test:integration # needs Docker; starts a disposable MySQL/MariaDB service
composer examples:smoke # needs Docker; smoke-executes examples/ against a real engine
composer test:consumer # needs Docker; two fresh external Composer verification runs
actionlint .github/workflows/*.yml
```

Expand All @@ -56,7 +57,7 @@ This package does not commit `composer.lock`. CI resolves both `latest` and `low

- Keep a pull request to one coherent change; include its own tests and any directly affected documentation in the same change rather than splitting them across separate pull requests.
- Describe the change, its rationale, and which local commands above you ran.
- All applicable CI gates (workflow lint, static checks, compatibility matrix, Integration, and example smoke) must pass before a merge is considered; CI gating and Git/merge permissions follow [AGENTS.md](AGENTS.md) and the pinned `AI_COLLABORATION_WORKFLOW_AR.md`/`GITHUB_PHASE_STACK_WORKFLOW_AR.md` — in particular, merges require explicit Owner authorization.
- All applicable CI gates (workflow lint, static checks, compatibility matrix, Integration, example smoke, and consumer verification) must pass before a merge is considered; CI gating and Git/merge permissions follow [AGENTS.md](AGENTS.md) and the pinned `AI_COLLABORATION_WORKFLOW_AR.md`/`GITHUB_PHASE_STACK_WORKFLOW_AR.md` — in particular, merges require explicit Owner authorization.

## Reporting a security issue

Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"php-cs-fixer fix --dry-run --diff",
"@php scripts/check-per-cs-3-1.php"
],
"syntax": "find src tests scripts examples .php-cs-fixer.php -name '*.php' -print0 | xargs -0 -n1 php -l > /dev/null",
"syntax": "find src tests scripts examples harness .php-cs-fixer.php -name '*.php' -print0 | xargs -0 -n1 php -l > /dev/null",
"whitespace": "git diff --check 4b825dc642cb6eb9a060e54bf8d69288fbee4904 HEAD -- . ':!docs/php-engineering-standards'",
"test": [
"@test:unit",
Expand All @@ -61,7 +61,8 @@
"test:unit": "phpunit --testsuite unit",
"test:regression": "phpunit --testsuite regression",
"test:integration": "@php scripts/integration.php",
"examples:smoke": "@php scripts/examples-smoke.php"
"examples:smoke": "@php scripts/examples-smoke.php",
"test:consumer": "@php scripts/consumer-verification.php"
},
"config": {
"optimize-autoloader": true,
Expand Down
5 changes: 4 additions & 1 deletion docs/architecture/CI_AND_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ Every CI gate runs a repository-owned command that a developer can run locally w
| Regression suite | `composer test:regression` | Compat |
| Integration suite (real database) | `composer test:integration` | Integration |
| Standalone examples (real database) | `composer examples:smoke` | Examples smoke |
| Consumer verification and coexistence | `composer test:consumer` | Consumer verification and coexistence |
| Workflow lint | `actionlint` over `.github/workflows` | Workflow lint |

Unit and Regression need no Docker. `composer test` runs Unit, Regression, and Integration locally. The required `CI gate` aggregates Workflow lint, Static checks, Compat, Integration, and Examples smoke. The exploratory cell is optional.
Unit and Regression need no Docker. `composer test` runs Unit, Regression, and Integration locally. The required `CI gate` aggregates Workflow lint, Static checks, Compat, Integration, Examples smoke, and Consumer verification and coexistence. The exploratory cell is optional.

## 2. Integration lifecycle

Expand Down Expand Up @@ -78,6 +79,8 @@ PP_DB_CELL=mariadb-11.8.9 composer examples:smoke

CI requires both `mysql-8.4.13` and `mariadb-11.8.9` smoke jobs on PHP 8.4. This smoke matrix is separate from the capability Integration matrix below.

[Consumer verification](../guides/CONSUMER_VERIFICATION.md) uses two fresh MySQL lifecycles and separate production Composer roots.

## 3. Test matrix

One parameterized Compose definition (`compose.integration.yaml`) serves every cell. The data is in `docker/integration-matrix.json`; images come from the Docker Official Images `mysql` and `mariadb` repositories on Docker Hub.
Expand Down
27 changes: 27 additions & 0 deletions docs/guides/CONSUMER_VERIFICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Consumer verification and coexistence

From a checkout of the committed candidate, run `composer test:consumer` with PHP 8.4 or newer (PDO MySQL enabled), Composer 2.10, Git, cURL, tar, and Docker Compose. Internet access is needed for Composer dependencies, the pinned legacy source archive, and the Integration database image. The candidate is archived from the committed Git HEAD and its commit and tree are printed. On pull-request CI, this is the commit that the workflow checked out.

The required CI job provisions two separate MySQL 8.4.13 services through the existing Integration lifecycle. Each service runs the consumer workload in two independently generated Composer roots outside the repository. Every root starts without a lock, vendor, or project state and installs both packages through Composer with `--no-dev`. It runs the repository's dependency policy, uses only the production autoloader, and verifies the installed package identities. The candidate is mirrored from the Git archive. The consumer project owns its schema and the input catalog fixtures. Each lifecycle verifies zero tables and open transactions, removes the consumer directories, and verifies Compose teardown.

| Scenario | Legacy identity | Evidence |
|---|---|---|
| Coexistence | Published `maatify/persistence:1.4.0` at `fc1459b49fe5af5ead17814eeb3ada7a3fa81791` | Dist reference checked in Composer lock. Both savepoint runners resolve to separate types. On one PDO connection, each package takes its turn as the inner savepoint runner. Rolling back the inner operation preserves earlier caller and outer-runner writes. The caller can still roll back both packages' work. The candidate also executes the catalog workflow. |
| Differential | [Unpublished legacy source](https://github.com/Maatify/persistence/tree/1f6a7aafe601090b7e55f60d83794364afdefe67) | Exact source archive installed in its own consumer root alongside the candidate. The same fixture, assertions, and observable pagination results apply to both packages. |

## Behavior evidence map

The [behavior contract](../architecture/BEHAVIOR_CONTRACT.md) labels published legacy as A, unpublished legacy as B, and this package as C.

| Behavior | Evidence and limits |
|---|---|
| B-01, B-02, B-03, B-06, B-07 | Differential catalog exercises scoped/nullable allocation, a locking allocation followed by insert in the same transaction, a clamped move, no-op and missing moves, and soft-delete/row visibility. B-02 and the B-03 locking-snapshot change use B as the source baseline. For both packages, a second connection commits a new maximum after the caller establishes its InnoDB repeatable-read snapshot. The plain read retains the old maximum and the locked move clamps to the newly committed maximum. Fine-grained lock footprint stays covered by the repository's Integration suite. |
| B-04 | Published and source coexistence scenarios prove nested savepoint isolation through transaction runners. The differential catalog also triggers a CHECK constraint failure after a range shift and proves local rollback preserves earlier caller work against B. The remaining Ordering-specific lazy rollback branches stay covered by `ScopedOrderingManagerLazySavepointTest` and `ScopedOrderingManagerSavepointWiringTest`; it is a documented delta from A. |
| B-05, B-10, B-13 | The catalog checks owned commit/rollback, move return values and callback exception identity against B. Fail-closed PDO control failure behavior is an approved delta from A and remains exercised by the named repository tests. Exception FQCNs and the package marker differ. |
| B-08, B-08a, B-09 | Differential catalog compares pagination normalization, filtering, ordering and the result envelope. A second real connection commits between the two count queries without timing sleeps. Autocommit can observe inconsistent counts and caller-owned InnoDB repeatable read gives a single snapshot. The PHP DTO name changes while the observable array and JSON payloads agree. |
| B-11, B-12 | Nested savepoints are checked in both directions against both legacy identities. A separate differential fixture uses the two Gen2 runner forms in each package with one configured retry. A bounded pipe handshake and two conflicting InnoDB row locks produce a real `40001/1213` victim. Each form replays once inside a fresh transaction and commits only the second attempt. Retry is a delta from A. Retry exhaustion and fail-closed paths remain covered by `DeadlockRetryIntegrationTest` and `DeadlockRetryFailClosedTest`. |
| B-14, B-15, B-16 | Harness SQL and transactions execute on the named MySQL cell. Existing Integration and compatibility jobs remain required. Engine evidence, Composer constraints, and archive scope remain distinct. Exact release artifact verification belongs to WU-11. |

The orchestrator is checked by the package's PHPStan configuration. The independent consumer fixture is syntax-checked and validated through execution in the two production Composer roots, where its legacy imports resolve.

The harness is additional to Unit, Regression, Integration and Examples Smoke. This evidence concerns an unpublished candidate, with no production host migration or support-range claim. DEC-009 remains deferred, and DEC-013 records test versions.
Loading