Skip to content

fix: isolate statement cache and fix SQLite inserts#20

Merged
davebarnwell merged 3 commits intomainfrom
feature/fix-sqlite-insert-and-statement-cache
Mar 7, 2026
Merged

fix: isolate statement cache and fix SQLite inserts#20
davebarnwell merged 3 commits intomainfrom
feature/fix-sqlite-insert-and-statement-cache

Conversation

@davebarnwell
Copy link
Owner

@davebarnwell davebarnwell commented Mar 7, 2026

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 normal save() call failed on SQLite even though the rest of the model code advertised support for it. The insert builder now emits portable INSERT INTO (...) VALUES (...) SQL for non-PostgreSQL writes while preserving the existing PostgreSQL RETURNING path and the empty-row DEFAULT VALUES handling.

The patch also fixes prepared statement caching when subclasses redeclare $_db to 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 CategoryTest integration 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 appends LIMIT to UPDATE and DELETE, which avoids another engine-specific syntax failure.

Validation:

  • vendor/bin/phpunit -c phpunit.xml.dist
  • MODEL_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.dist
  • MODEL_ORM_TEST_DSN='sqlite::memory:' MODEL_ORM_TEST_USER='' MODEL_ORM_TEST_PASS='' vendor/bin/phpunit -c phpunit.xml.dist
  • vendor/bin/phpstan analyse -c phpstan.neon

@davebarnwell davebarnwell marked this pull request as ready for review March 7, 2026 22:32
Copilot AI review requested due to automatic review settings March 7, 2026 22:32
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PostgreSQL RETURNING and empty-row DEFAULT VALUES handling).
  • 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.

@davebarnwell davebarnwell requested a review from Copilot March 7, 2026 22:43
@davebarnwell davebarnwell merged commit d5a4e0b into main Mar 7, 2026
9 checks passed
@davebarnwell davebarnwell deleted the feature/fix-sqlite-insert-and-statement-cache branch March 7, 2026 22:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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']);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +100
Freshsauce\Model\Model::execute(
'DELETE FROM `' . self::SQLITE_SEQUENCE_TABLE . '` WHERE `name` = ?',
['categories']
);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants