fix: isolate statement cache and fix SQLite inserts#20
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes correctness issues in the core Model layer around SQLite inserts and prepared statement caching across multiple PDO connections, and adds regression tests to prevent recurrence.
Changes:
- Make non-PostgreSQL inserts use portable
INSERT INTO (...) VALUES (...)SQL (while keeping PostgreSQLRETURNINGand empty-rowDEFAULT VALUEShandling). - Scope prepared-statement caching by PDO connection identity + SQL text to avoid cross-connection statement reuse.
- Add PHPUnit coverage for SQLite inserts and for statement-cache isolation across separate in-memory SQLite connections; update README to reflect SQLite test coverage.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/Model/Model.php |
Fixes SQLite insert SQL generation and isolates statement cache by PDO connection. |
tests/Model/SqliteModelTest.php |
Adds regression tests for SQLite inserts and prepared statement isolation across connections. |
test-src/Model/SqliteCategory.php |
Adds a SQLite-backed test model for exercising the ORM insert path. |
test-src/Model/IsolatedConnectionCategoryA.php |
Adds a test model with its own $_db to validate statement-cache scoping. |
test-src/Model/IsolatedConnectionCategoryB.php |
Adds a second isolated-connection test model to detect statement bleed across connections. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| protected function setUp(): void | ||
| { | ||
| App\Model\SqliteCategory::execute('DELETE FROM `categories`'); | ||
| App\Model\SqliteCategory::execute('DELETE FROM sqlite_sequence WHERE name = ?', ['categories']); |
There was a problem hiding this comment.
DELETE FROM sqlite_sequence can fail on a fresh in-memory SQLite DB because the sqlite_sequence table typically does not exist until the first AUTOINCREMENT insert occurs. This will make the first test setup error out before any inserts run. Consider guarding this statement (check sqlite_master first or catch/ignore the "no such table" error), or avoid resetting the sequence and make the test not depend on the inserted id being 1.
| App\Model\SqliteCategory::execute('DELETE FROM sqlite_sequence WHERE name = ?', ['categories']); | |
| try { | |
| App\Model\SqliteCategory::execute('DELETE FROM sqlite_sequence WHERE name = ?', ['categories']); | |
| } catch (\PDOException $e) { | |
| if (strpos($e->getMessage(), 'no such table: sqlite_sequence') === false) { | |
| throw $e; | |
| } | |
| } |
| Freshsauce\Model\Model::execute( | ||
| 'DELETE FROM `' . self::SQLITE_SEQUENCE_TABLE . '` WHERE `name` = ?', | ||
| ['categories'] | ||
| ); |
There was a problem hiding this comment.
For SQLite, DELETE FROM sqlite_sequence ... can error on a brand-new in-memory database because sqlite_sequence is not guaranteed to exist until after the first AUTOINCREMENT insert. Since setUp() runs before the first test inserts anything, this can break the whole suite on SQLite. Please guard this statement (e.g., check sqlite_master for sqlite_sequence, or catch/ignore the missing-table error) or drop the sequence-reset entirely if tests don’t rely on ids restarting at 1.
| Freshsauce\Model\Model::execute( | |
| 'DELETE FROM `' . self::SQLITE_SEQUENCE_TABLE . '` WHERE `name` = ?', | |
| ['categories'] | |
| ); | |
| try { | |
| Freshsauce\Model\Model::execute( | |
| 'DELETE FROM `' . self::SQLITE_SEQUENCE_TABLE . '` WHERE `name` = ?', | |
| ['categories'] | |
| ); | |
| } catch (\Throwable $e) { | |
| // Ignore if sqlite_sequence does not exist (e.g. on a brand-new SQLite database) | |
| } |
This change fixes the SQLite-specific correctness gaps in the base model layer and expands automated coverage so every advertised backend is exercised in CI.
SQLite was documented as supported, but inserts with dirty fields still used MySQL-style
INSERT ... SET ...SQL in the shared insert path. That meant a normalsave()call failed on SQLite even though the rest of the model code advertised support for it. The insert builder now emits portableINSERT INTO (...) VALUES (...)SQL for non-PostgreSQL writes while preserving the existing PostgreSQLRETURNINGpath and the empty-rowDEFAULT VALUEShandling.The patch also fixes prepared statement caching when subclasses redeclare
$_dbto use separate PDO connections. Statement caching was keyed only by SQL string, so two models using identical queries on different connections could accidentally share the same prepared statement handle. The cache is now scoped by PDO object identity and query text, which keeps statements isolated to the connection that prepared them.To make the backend support claims real, the shared
CategoryTestintegration suite now supports SQLite as well as MySQL and PostgreSQL. The workflow matrix has been extended to run the PHPUnit suite against all three database types, and the SQLite-specific test coverage remains in place for connection isolation. The SQLite path also no longer appendsLIMITtoUPDATEandDELETE, which avoids another engine-specific syntax failure.Validation:
vendor/bin/phpunit -c phpunit.xml.distMODEL_ORM_TEST_DSN='pgsql:host=127.0.0.1;port=5432;dbname=categorytest' MODEL_ORM_TEST_USER='postgres' MODEL_ORM_TEST_PASS='postgres' vendor/bin/phpunit -c phpunit.xml.distMODEL_ORM_TEST_DSN='sqlite::memory:' MODEL_ORM_TEST_USER='' MODEL_ORM_TEST_PASS='' vendor/bin/phpunit -c phpunit.xml.distvendor/bin/phpstan analyse -c phpstan.neon