diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c77336..fb6901d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ First standalone release, planned as 1.0.0, for PRADO 4.4. SqlMap moves out of t ### Added +- `TSqlMapManager::VERSION` holds the package version and `TSqlMapManager::getVersion()` (the `Version` property) returns the version that built the manager, in the form of `Prado::getVersion()`. The version is serialized with the manager. `TSqlMapConfig` discards a cached manager whose version is not `VERSION` and rebuilds it, so the application cache does not need to be cleared after an upgrade. - An ``, ``, or `` statement with a `cacheModel` attribute flushes that cache model when it executes, the same as a `` entry. The attribute was previously accepted and ignored (#5). - `composer.json` declares `extra.prado` for PRADO 4.4: `bootstrap` (`TSqlMapConfig`), `error-messages` (`src/Data/SqlMap/DataMapper/messages.txt`), and `class-map` (`config/classes.json`). The legacy top-level `extra.bootstrap` remains for PRADO 4.3. - `config/classes.json` maps the 55 SqlMap short class names (`TSqlMapConfig`, `TSqlMapFifoCache`, ...) to their FQNs. It replaces the SqlMap entries of the framework's `classes.php`. diff --git a/CLAUDE.md b/CLAUDE.md index f7778f3..5c0d5f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -165,6 +165,7 @@ tests/ - **`if` statements** always use a block (`{}`), never a single-line body. - **Backward compatibility** — all changes must be backward compatible for point releases. - **`@since` tag** — use the next release version when adding new methods or classes; omit the method tag when it matches the class tag. Check prado's `CLAUDE.md` for the current next-release version number. +- **Package version** — `TSqlMapManager::VERSION` is the package version. Bump it with each release; `TSqlMapConfig` discards cached managers built by another version. - **Uniform Access Principal / Self Encapsulation** required — access own properties via getters/setters, not raw `$this->_prop`. - **Error codes** for PRADO exceptions are defined in `src/Data/SqlMap/DataMapper/messages.txt`. - **`@author` tags** — only Wei Zhuo, Brad Anderson, and Fabio Bas are listed as extension authors. diff --git a/README.md b/README.md index 87cfb8d..bf98add 100644 --- a/README.md +++ b/README.md @@ -904,7 +904,7 @@ Clinton Begin started iBATIS in 2001, and its SQL Maps component (2002) mapped S ## Versioning -The package has its own version line, starting at 1.0.0, independent of the PRADO framework version. The first release pairs with PRADO 4.4, the release that removes SqlMap from the framework. Point releases stay backward compatible. Changes are listed in the [changelog](CHANGELOG.md). +The package has its own version line, starting at 1.0.0, independent of the PRADO framework version. The first release pairs with PRADO 4.4, the release that removes SqlMap from the framework. Point releases stay backward compatible. Changes are listed in the [changelog](CHANGELOG.md). The version is available at runtime as `TSqlMapManager::VERSION`, and `TSqlMapManager::getVersion()` reports the version that built a manager instance. ## License diff --git a/docs/integration.md b/docs/integration.md index 02a450e..1b0c6f3 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -76,7 +76,7 @@ return [ `TSqlMapConfig` bootstraps the extension. On first access of its `Client` property it: -1. Checks the application cache for a previously serialized [`TSqlMapManager`](#tsqlmapmanager) (when `EnableCache` is `true`). +1. Checks the application cache for a previously serialized [`TSqlMapManager`](#tsqlmapmanager) (when `EnableCache` is `true`). A cached manager whose `Version` is not `TSqlMapManager::VERSION` was written by another release of this package and is ignored. 2. If not cached, instantiates a new `TSqlMapManager`, calls `configureXml()` with the resolved config file path, and stores the result in the cache. 3. Calls `getSqlmapGateway()` on the manager and returns the resulting [`TSqlMapGateway`](#tsqlmapgateway). @@ -108,6 +108,7 @@ When `ConnectionID` is set and the manager is loaded from cache, `TSqlMapConfig` | `getDbConnection` | `getDbConnection(): TDbConnection` | Returns the active database connection. | | `setDbConnection` | `setDbConnection(TDbConnection $conn): void` | Replaces the database connection (used by `TSqlMapConfig` after a cache hit). | | `flushCacheModels` | `flushCacheModels(): void` | Flushes all statement-level cache models registered with this manager. | +| `getVersion` | `getVersion(): ?string` | Returns the package version that built the manager, in the form of `Prado::getVersion()`. A new manager returns `TSqlMapManager::VERSION`; a manager unserialized from a cache written by another release returns that release's version, or `null` when none was recorded. | The manager is also the target for direct (non-PRADO) use: diff --git a/docs/migration.md b/docs/migration.md index 99ffd43..71a9592 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -56,13 +56,12 @@ Statements with a `cacheModel` build their cache key from the SQL text. Cached s ### Serialized managers -With `EnableCache="true"`, `TSqlMapConfig` stores the parsed `TSqlMapManager` in the application cache. The serialized form of several classes differs from the framework copy. Clear the application cache once after upgrading so the manager is rebuilt. +With `EnableCache="true"`, `TSqlMapConfig` stores the parsed `TSqlMapManager` in the application cache. The serialized form of several classes differs from the framework copy. `TSqlMapConfig` detects this: a cached manager that reports a `Version` other than `TSqlMapManager::VERSION` (the framework copy reports none) is discarded and the manager is rebuilt from the config files on the next request. No cache clearing is needed. ## Verifying the move After installing the package: 1. Confirm `class="TSqlMapConfig"` module definitions still resolve. -2. Clear the application cache, or delete the cache files under `protected/runtime`. -3. Replace any comparison of `TSqlMapException::getErrorCode()` with message text. -4. Run the application's own tests against its mapped statements. +2. Replace any comparison of `TSqlMapException::getErrorCode()` with message text. +3. Run the application's own tests against its mapped statements. diff --git a/phpunit.xml b/phpunit.xml index ac0ad3c..8e69823 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,6 +10,7 @@ ./tests/unit/Data/SqlMap/SqlMapSleepTest.php ./tests/unit/Data/SqlMap/StatementExtendsTest.php ./tests/unit/Data/SqlMap/Ticket589Test.php + ./tests/unit/Data/SqlMap/TSqlMapManagerVersionTest.php diff --git a/src/Data/SqlMap/TSqlMapConfig.php b/src/Data/SqlMap/TSqlMapConfig.php index 899b7cc..ccd813b 100644 --- a/src/Data/SqlMap/TSqlMapConfig.php +++ b/src/Data/SqlMap/TSqlMapConfig.php @@ -79,7 +79,9 @@ * config file path) so that XML parsing is skipped on subsequent requests. * Cache entries are invalidated automatically when the config files change * unless the application is running in {@see \Prado\TApplicationMode::Performance} - * mode. + * mode. A cached manager whose {@see TSqlMapManager::getVersion() Version} is not + * {@see TSqlMapManager::VERSION} was written by another release of this package; + * it is discarded and the manager is rebuilt from the config files. * * @author Wei Zhuo * @since 3.1 @@ -157,8 +159,9 @@ protected function cacheSqlMapManager($manager) } /** - * Loads SqlMap manager from cache. - * @return ?TSqlMapManager SqlMap manager intance if load was successful, null otherwise. + * Loads SqlMap manager from cache. A cached manager built by another version + * of this package is ignored, so the caller rebuilds it and replaces the entry. + * @return ?TSqlMapManager SqlMap manager instance if load was successful, null otherwise. */ protected function loadCachedSqlMapManager() { @@ -166,7 +169,7 @@ protected function loadCachedSqlMapManager() $cache = $this->getApplication()->getCache(); if ($cache !== null) { $manager = $cache->get($this->getCacheKey()); - if ($manager instanceof TSqlMapManager) { + if ($manager instanceof TSqlMapManager && $manager->getVersion() === TSqlMapManager::VERSION) { return $manager; } } diff --git a/src/Data/SqlMap/TSqlMapManager.php b/src/Data/SqlMap/TSqlMapManager.php index 84cd621..911e12b 100644 --- a/src/Data/SqlMap/TSqlMapManager.php +++ b/src/Data/SqlMap/TSqlMapManager.php @@ -38,11 +38,21 @@ * $result = $sqlmap->queryForObject('Products'); * ``` * + * {@see getVersion Version} is the package version that built the manager. It is + * serialized with the manager, so a manager restored from the application cache + * reports the version that wrote the cache entry. + * * @author Wei Zhuo * @since 3.1 */ class TSqlMapManager extends \Prado\TComponent { + /** + * Version of this package. Equal to {@see getVersion} for a new manager. + * @since 4.4.0 + */ + public const VERSION = '1.0.0'; + private $_mappedStatements; private $_resultMaps; private $_parameterMaps; @@ -52,6 +62,11 @@ class TSqlMapManager extends \Prado\TComponent private $_connection; private $_gateway; private $_cacheDependencies; + /** + * @var ?string package version that built this manager. Null for a manager + * unserialized from a cache entry written before the version was recorded. + */ + private $_version; /** * Constructor, create a new SqlMap manager. @@ -60,6 +75,7 @@ class TSqlMapManager extends \Prado\TComponent public function __construct($connection = null) { $this->_connection = $connection; + $this->_version = self::VERSION; $this->_mappedStatements = new TMap(); $this->_resultMaps = new TMap(); @@ -68,6 +84,21 @@ public function __construct($connection = null) parent::__construct(); } + /** + * The version of this package that built the manager, in the form of + * {@see \Prado\Prado::getVersion()}. A new manager reports {@see VERSION}. + * A manager unserialized from the application cache reports the version of + * the package that wrote the entry, or null when that package recorded no + * version. {@see TSqlMapConfig} discards a cached manager whose version is + * not {@see VERSION}. + * @return ?string package version that built this manager. + * @since 4.4.0 + */ + public function getVersion(): ?string + { + return $this->_version; + } + /** * @param \Prado\Data\TDbConnection $conn default database connection */ diff --git a/tests/unit/Data/SqlMap/TSqlMapManagerVersionTest.php b/tests/unit/Data/SqlMap/TSqlMapManagerVersionTest.php new file mode 100644 index 0000000..85788d6 --- /dev/null +++ b/tests/unit/Data/SqlMap/TSqlMapManagerVersionTest.php @@ -0,0 +1,98 @@ +appCache = PradoUnit::getProp(Prado::getApplication(), '_cache'); + } + + protected function tearDown(): void + { + PradoUnit::setProp(Prado::getApplication(), '_cache', $this->appCache); + } + + private function cachingConfig(): TSqlMapConfig + { + $cache = new TMemoryCache(); + $cache->setPrimaryCache(false); + $cache->init(null); + PradoUnit::setProp(Prado::getApplication(), '_cache', $cache); + + $config = new TSqlMapConfig(); + $config->setConfigFile(__DIR__ . '/maps/tests.xml'); + $config->setEnableCache(true); + return $config; + } + + private function cacheKey(TSqlMapConfig $config): string + { + return $config->getID() . $config->getConfigFile(); + } + + public function testVersionIsSemantic() + { + $this->assertMatchesRegularExpression('/^\d+\.\d+\.\d+$/', TSqlMapManager::VERSION); + } + + public function testNewManagerReportsPackageVersion() + { + $manager = new TSqlMapManager(); + $this->assertSame(TSqlMapManager::VERSION, $manager->getVersion()); + $this->assertSame(TSqlMapManager::VERSION, $manager->Version); + } + + public function testVersionIsSerialized() + { + $manager = unserialize(serialize(new TSqlMapManager())); + $this->assertSame(TSqlMapManager::VERSION, $manager->getVersion()); + } + + public function testManagerSerializedWithoutVersionReportsNull() + { + $class = TSqlMapManager::class; + $manager = unserialize(sprintf('O:%d:"%s":0:{}', strlen($class), $class)); + $this->assertInstanceOf(TSqlMapManager::class, $manager); + $this->assertNull($manager->getVersion()); + } + + public function testConfigReusesCachedManagerOfSameVersion() + { + $config = $this->cachingConfig(); + $first = $config->getSqlMapManager(); + $this->assertSame(TSqlMapManager::VERSION, $first->getVersion()); + + $cached = Prado::getApplication()->getCache()->get($this->cacheKey($config)); + $this->assertInstanceOf(TSqlMapManager::class, $cached); + $this->assertSame(TSqlMapManager::VERSION, $cached->getVersion()); + + $second = $config->getSqlMapManager(); + $this->assertSame(TSqlMapManager::VERSION, $second->getVersion()); + $this->assertTrue($second->getMappedStatements()->contains('test')); + } + + public function testConfigDiscardsCachedManagerOfAnotherVersion() + { + $config = $this->cachingConfig(); + $cache = Prado::getApplication()->getCache(); + + foreach (['0.9.0', null] as $version) { + $stale = new TSqlMapManager(); + PradoUnit::setProp($stale, '_version', $version); + $cache->set($this->cacheKey($config), $stale); + + $manager = $config->getSqlMapManager(); + $this->assertSame(TSqlMapManager::VERSION, $manager->getVersion()); + $this->assertTrue($manager->getMappedStatements()->contains('test')); + $this->assertSame(TSqlMapManager::VERSION, $cache->get($this->cacheKey($config))->getVersion()); + } + } +}