diff --git a/.github/workflows/prado-sqlmap.yml b/.github/workflows/prado-sqlmap.yml
index 332f378..f26bdfd 100644
--- a/.github/workflows/prado-sqlmap.yml
+++ b/.github/workflows/prado-sqlmap.yml
@@ -15,11 +15,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-versions: ['8.1', '8.2', '8.3']
+ php-versions: ['8.2', '8.3', '8.4', '8.5']
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Setup PHP, with composer and extensions
uses: shivammathur/setup-php@v2
@@ -39,7 +39,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Setup PHP with SQLite extensions
uses: shivammathur/setup-php@v2
@@ -72,7 +72,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -89,7 +89,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Setup PHP with MySQL extensions
uses: shivammathur/setup-php@v2
@@ -102,7 +102,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -138,7 +138,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Setup PHP with PostgreSQL extensions
uses: shivammathur/setup-php@v2
@@ -151,7 +151,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -187,7 +187,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Setup PHP with Firebird extensions
uses: shivammathur/setup-php@v2
@@ -200,7 +200,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -213,6 +213,7 @@ jobs:
run: bash .github/scripts/setup-firebird.sh ${{ job.services.firebird.id }}
- name: Run Firebird tests
+ timeout-minutes: 6
run: vendor/bin/phpunit --testsuite db-firebird
db-sqlsrv:
@@ -233,7 +234,7 @@ jobs:
--health-retries 10
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Setup PHP with SQL Server extensions
uses: shivammathur/setup-php@v2
@@ -246,7 +247,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -280,7 +281,7 @@ jobs:
--health-retries 20
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Install Oracle Instant Client
run: bash .github/scripts/install-oracle-client.sh
@@ -296,7 +297,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -334,7 +335,7 @@ jobs:
--health-retries 20
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Setup PHP with IBM DB2 extensions
uses: shivammathur/setup-php@v2
@@ -347,7 +348,7 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
diff --git a/CLAUDE.md b/CLAUDE.md
index 263bd6f..ee1e915 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -197,7 +197,7 @@ The phpunit bootstrap (`tests/test_tools/phpunit_bootstrap.php`) loads `PradoUni
- Indentation: **tabs** (not spaces)
- Line endings: Unix (`\n`)
-- PHP minimum: **8.1** (CI tests 8.1, 8.2, 8.3)
+- PHP minimum: **8.1** (CI tests 8.2, 8.3, 8.4, 8.5; the `pradosoft/prado` dev dependency requires 8.2)
- PSR-12 enforced via php-cs-fixer
- Use `?` for single nullable types and in doc blocks
diff --git a/composer.json b/composer.json
index 8ef2000..3c850b5 100644
--- a/composer.json
+++ b/composer.json
@@ -28,6 +28,11 @@
"Prado\\": "src"
}
},
+ "autoload-dev": {
+ "psr-4": {
+ "Prado\\Test\\Unit\\": "vendor/pradosoft/prado/tests/unit/"
+ }
+ },
"repositories": [
{
"type": "composer",
@@ -47,7 +52,7 @@
},
"config": {
"platform": {
- "php": "8.1"
+ "php": "8.2"
},
"allow-plugins": {
"phpdocumentor/shim": true
diff --git a/src/Data/SqlMap/Statements/TMappedStatement.php b/src/Data/SqlMap/Statements/TMappedStatement.php
index 158d991..0a4e8cf 100644
--- a/src/Data/SqlMap/Statements/TMappedStatement.php
+++ b/src/Data/SqlMap/Statements/TMappedStatement.php
@@ -494,6 +494,8 @@ public function onExecuteQuery($sql)
/**
* Apply result mapping.
+ * The PRADO_ROWNUM column is removed from the row. TIbmCommandBuilder::applyLimitOffset()
+ * pages through a ROW_NUMBER() subquery, and its outer "SELECT *" returns that helper column.
* @param array $row a result set row retrieved from the database
* @param null|object $resultObject the result object, will create if necessary.
* @return object the result filled with data, null if not filled.
@@ -503,6 +505,7 @@ protected function applyResultMap($row, &$resultObject = null)
if ($row === false) {
return null;
}
+ unset($row['PRADO_ROWNUM'], $row['prado_rownum']);
$resultMapName = $this->_statement->getResultMap();
$resultClass = $this->_statement->getResultClass();
diff --git a/src/Data/SqlMap/Statements/TPreparedCommand.php b/src/Data/SqlMap/Statements/TPreparedCommand.php
index 262de05..af6db79 100644
--- a/src/Data/SqlMap/Statements/TPreparedCommand.php
+++ b/src/Data/SqlMap/Statements/TPreparedCommand.php
@@ -51,12 +51,22 @@ protected function applyParameterMap($manager, $command, $prepared, $statement,
//$parameters = $prepared->getParameterValues();
$registry = $manager->getTypeHandlers();
if ($properties) {
+ $driverName = $command->getConnection()->getDriverName();
for ($i = 0, $k = $properties->getCount(); $i < $k; $i++) {
$property = $statement->parameterMap()->getProperty($i);
$value = $statement->parameterMap()->getPropertyValue($registry, $property, $parameterObject);
+ if (is_bool($value) && $driverName === 'firebird') {
+ // pdo_firebird binds a PHP bool only to a BOOLEAN column; integer flag columns take 0 or 1.
+ $value = (int) $value;
+ }
$dbType = $property->getDbType();
if ($dbType == '') { //relies on PHP lax comparison
- $command->bindValue($i + 1, $value, TDbCommandBuilder::getPdoType($value));
+ $pdoType = TDbCommandBuilder::getPdoType($value);
+ if ($pdoType === \PDO::PARAM_NULL && $driverName === 'ibm') {
+ // pdo_ibm rejects PDO::PARAM_NULL; it binds a null PDO::PARAM_STR as SQL NULL.
+ $pdoType = \PDO::PARAM_STR;
+ }
+ $command->bindValue($i + 1, $value, $pdoType);
} elseif (strpos($dbType, 'PDO::') === 0) {
$command->bindValue($i + 1, $value, constant($property->getDbType()));
} //assumes PDO types, e.g. PDO::PARAM_INT
diff --git a/tests/test_tools/phpunit_bootstrap.php b/tests/test_tools/phpunit_bootstrap.php
index bb15e6a..714c89f 100644
--- a/tests/test_tools/phpunit_bootstrap.php
+++ b/tests/test_tools/phpunit_bootstrap.php
@@ -2,6 +2,5 @@
require_once(__DIR__ . '/../../vendor/autoload.php');
require_once(__DIR__ . '/../../vendor/pradosoft/prado/framework/Prado.php');
-require_once(__DIR__ . '/../../vendor/pradosoft/prado/tests/unit/PradoUnitRequires.php');
-$application = new \Prado\TTestApplication();
+$application = new \Prado\Test\Unit\Harness\TTestApplication();
diff --git a/tests/unit/Data/SqlMap/ActiveRecordSqlMapTestCase.php b/tests/unit/Data/SqlMap/ActiveRecordSqlMapTestCase.php
index 02125db..28f6f5d 100644
--- a/tests/unit/Data/SqlMap/ActiveRecordSqlMapTestCase.php
+++ b/tests/unit/Data/SqlMap/ActiveRecordSqlMapTestCase.php
@@ -18,9 +18,10 @@ class ActiveAccount extends TActiveRecord
const TABLE = 'accounts';
/**
- * Maps PostgreSQL lowercase column names to the mixed-case PHP property names.
- * On MySQL and SQLite the schema returns 'Account_Id' (matching the property),
- * so these entries are simply never looked up on those drivers.
+ * Maps folded column names to the mixed-case PHP property names.
+ * - PostgreSQL and IBM DB2 metadata report lowercase names ('account_id').
+ * - Firebird metadata reports uppercase names ('ACCOUNT_ID').
+ * - MySQL and SQLite report 'Account_Id', which matches the property directly.
*/
public static $COLUMN_MAPPING = [
'account_id' => 'Account_Id',
@@ -29,6 +30,12 @@ class ActiveAccount extends TActiveRecord
'account_email' => 'Account_Email',
'account_banner_option' => 'Account_Banner_Option',
'account_cart_option' => 'Account_Cart_Option',
+ 'ACCOUNT_ID' => 'Account_Id',
+ 'ACCOUNT_FIRSTNAME' => 'Account_FirstName',
+ 'ACCOUNT_LASTNAME' => 'Account_LastName',
+ 'ACCOUNT_EMAIL' => 'Account_Email',
+ 'ACCOUNT_BANNER_OPTION' => 'Account_Banner_Option',
+ 'ACCOUNT_CART_OPTION' => 'Account_Cart_Option',
];
public static function finder($className = __CLASS__)
@@ -104,6 +111,13 @@ public function testLoadWithSqlMap_SaveWithActiveRecord()
$record->markLoaded();
$record->Account_FirstName = "Testing 123";
+ // The record has a NULL Account_Email. TDbCommandBuilder binds NULL as
+ // PDO::PARAM_NULL, which pdo_ibm rejects with "Unknown parameter type".
+ // Prado's TDbDriverCapabilities::requiresUntypedParameters() resolves it.
+ if (static::$dbDriver === 'ibm' && !class_exists('\Prado\Data\TDbDriverCapabilities')) {
+ $this->markTestSkipped('ActiveRecord NULL binding on pdo_ibm requires Prado\Data\TDbDriverCapabilities.');
+ }
+
$this->assertTrue($record->save());
$check1 = self::$sqlmap->queryForObject('GetActiveRecordAccounts');
diff --git a/tests/unit/Data/SqlMap/BaseCase.php b/tests/unit/Data/SqlMap/BaseCase.php
index c50fbf3..eff53fe 100644
--- a/tests/unit/Data/SqlMap/BaseCase.php
+++ b/tests/unit/Data/SqlMap/BaseCase.php
@@ -4,6 +4,7 @@
use Prado\Data\SqlMap\DataMapper\TSqlMapTypeHandler;
use Prado\Data\SqlMap\TSqlMapManager;
+use Prado\Test\Unit\PradoUnit;
abstract class BaseCase extends PHPUnit\Framework\TestCase
{
@@ -68,13 +69,7 @@ public static function setUpBeforeClass(): void
// PHP static properties declared on a base class are shared across all
// subclasses — without this reset, the MySQL TDbConnection left behind by
// MysqlStatementTest would be picked up by PgsqlStatementTest.
- if (static::$connection !== null) {
- try {
- static::$connection->setActive(false);
- } catch (\Throwable $ignored) {
- }
- static::$connection = null;
- }
+ static::closeConnections();
static::$sqlmap = null;
static::$mapper = null;
static::$config = null;
@@ -133,7 +128,7 @@ protected static function initSchema(): void
// Schema initialisation failed — treat DB as unavailable.
static::$unavailableKey = static::$dbDriver;
static::$unavailableCause = $e;
- static::$unavailableReason = 'Schema init failed (' . static::$dbDriver . ')';
+ static::$unavailableReason = 'Schema init failed (' . static::$dbDriver . '): ' . $e->getMessage();
static::$config = null;
}
return;
@@ -148,13 +143,32 @@ public static function tearDownAfterClass(): void
}
// Close the connection so the next class (possibly a different driver) starts
// fresh. See the setUpBeforeClass() comment above.
- if (static::$connection !== null) {
+ static::closeConnections();
+ }
+
+ /**
+ * Closes the shared connection and the current config's connection.
+ * The config connection is closed even when $connection was never assigned,
+ * because TActiveRecordManager may still hold it. An open pdo_firebird
+ * connection keeps an implicit transaction alive, and its table locks block
+ * the next class's DROP TABLE indefinitely.
+ */
+ protected static function closeConnections(): void
+ {
+ $connections = [static::$connection];
+ if (static::$config !== null) {
+ $connections[] = static::$config->getConnection();
+ }
+ foreach ($connections as $connection) {
+ if ($connection === null) {
+ continue;
+ }
try {
- static::$connection->setActive(false);
+ $connection->setActive(false);
} catch (\Throwable $ignored) {
}
- static::$connection = null;
}
+ static::$connection = null;
}
public static function getConnection()
diff --git a/tests/unit/Data/SqlMap/DbSpecific/Ibm/IbmBaseTestConfig.php b/tests/unit/Data/SqlMap/DbSpecific/Ibm/IbmBaseTestConfig.php
index b17d9b8..3c4f695 100644
--- a/tests/unit/Data/SqlMap/DbSpecific/Ibm/IbmBaseTestConfig.php
+++ b/tests/unit/Data/SqlMap/DbSpecific/Ibm/IbmBaseTestConfig.php
@@ -13,6 +13,9 @@ public function __construct()
$dbname = getenv('DB2_DATABASE') ?: 'pradount';
$dsn = 'ibm:DRIVER={IBM DB2 ODBC DRIVER};DATABASE=' . $dbname . ';HOSTNAME=localhost;PORT=50000;PROTOCOL=TCPIP';
$this->_connection = new TDbConnection($dsn, $user, $password);
+ // pdo_ibm defaults PDO::ATTR_CASE to CASE_UPPER, which also folds quoted
+ // aliases such as "Id". CASE_NATURAL keeps the names DB2 reports.
+ $this->_connection->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
}
public function getScriptRunner()
diff --git a/tests/unit/Data/SqlMap/common.php b/tests/unit/Data/SqlMap/common.php
index 96b9cb7..2f60895 100644
--- a/tests/unit/Data/SqlMap/common.php
+++ b/tests/unit/Data/SqlMap/common.php
@@ -48,15 +48,17 @@ public function runScript($connection, $script)
}
/**
- * Script runner for IBM DB2. Silently ignores SQLSTATE 42704 ("undefined name")
- * errors so that DROP TABLE/SEQUENCE statements do not abort on a fresh database
- * where the objects do not yet exist.
+ * Script runner for IBM DB2. Strips full-line "--" comments before splitting on
+ * semicolons, so a semicolon inside a comment does not split a statement.
+ * Silently ignores SQLSTATE 42704 ("undefined name") errors so that DROP
+ * TABLE/SEQUENCE statements do not abort on a fresh database where the objects
+ * do not yet exist.
*/
class IbmScriptRunner extends DefaultScriptRunner
{
public function runScript($connection, $script)
{
- $sql = file_get_contents($script);
+ $sql = preg_replace('/^\s*--.*$/m', '', file_get_contents($script));
$lines = explode(';', $sql);
foreach ($lines as $line) {
$line = trim($line);
diff --git a/tests/unit/Data/SqlMap/maps/firebird/Account.xml b/tests/unit/Data/SqlMap/maps/firebird/Account.xml
index 5f86b3a..3297b89 100644
--- a/tests/unit/Data/SqlMap/maps/firebird/Account.xml
+++ b/tests/unit/Data/SqlMap/maps/firebird/Account.xml
@@ -176,10 +176,10 @@
parameterClass="int"
resultClass="array">
select
- Account_Id as Id,
- Account_FirstName as FirstName,
- Account_LastName as LastName,
- Account_Email as EmailAddress
+ Account_Id as "Id",
+ Account_FirstName as "FirstName",
+ Account_LastName as "LastName",
+ Account_Email as "EmailAddress"
from Accounts
where Account_Id = #value#
@@ -187,10 +187,10 @@
@@ -291,7 +291,7 @@
@@ -432,8 +432,8 @@
delete from Accounts
- where Account_Id = #Id#
- and Account_Id = #Id#
+ where Account_Id = CAST(#Id# AS INTEGER)
+ and Account_Id = CAST(#Id# AS INTEGER)
diff --git a/tests/unit/Data/SqlMap/maps/firebird/Category.xml b/tests/unit/Data/SqlMap/maps/firebird/Category.xml
index ca5e462..d38f0bd 100644
--- a/tests/unit/Data/SqlMap/maps/firebird/Category.xml
+++ b/tests/unit/Data/SqlMap/maps/firebird/Category.xml
@@ -23,7 +23,7 @@
@@ -32,7 +32,7 @@
- SELECT GEN_ID(categories_gen, 0) AS value FROM RDB$DATABASE
+ SELECT GEN_ID(categories_gen, 0) AS "value" FROM RDB$DATABASE
insert into Categories
(Category_Name, Category_Guid)
@@ -43,7 +43,7 @@
- SELECT GEN_ID(categories_gen, 0) AS value FROM RDB$DATABASE
+ SELECT GEN_ID(categories_gen, 0) AS "value" FROM RDB$DATABASE
insert into Categories
(Category_Name, Category_Guid)
@@ -54,7 +54,7 @@
- SELECT GEN_ID(categories_gen, 0) AS value FROM RDB$DATABASE
+ SELECT GEN_ID(categories_gen, 0) AS "value" FROM RDB$DATABASE
insert into Categories
(Category_Name, Category_Guid)
@@ -65,7 +65,7 @@
- SELECT GEN_ID(categories_gen, 0) AS value FROM RDB$DATABASE
+ SELECT GEN_ID(categories_gen, 0) AS "value" FROM RDB$DATABASE
insert into Categories
(Category_Name, Category_Guid)
@@ -87,7 +87,7 @@
- SELECT GEN_ID(categories_gen, 0) AS value FROM RDB$DATABASE
+ SELECT GEN_ID(categories_gen, 0) AS "value" FROM RDB$DATABASE
diff --git a/tests/unit/Data/SqlMap/maps/firebird/Enumeration.xml b/tests/unit/Data/SqlMap/maps/firebird/Enumeration.xml
index 58391c5..f82c76d 100644
--- a/tests/unit/Data/SqlMap/maps/firebird/Enumeration.xml
+++ b/tests/unit/Data/SqlMap/maps/firebird/Enumeration.xml
@@ -27,9 +27,9 @@
diff --git a/tests/unit/Data/SqlMap/maps/firebird/Order.xml b/tests/unit/Data/SqlMap/maps/firebird/Order.xml
index 17b45d3..51d1564 100644
--- a/tests/unit/Data/SqlMap/maps/firebird/Order.xml
+++ b/tests/unit/Data/SqlMap/maps/firebird/Order.xml
@@ -257,7 +257,7 @@
parameterClass="int"
resultClass="date">
select
- Order_Date as 'datetime'
+ Order_Date as "datetime"
from Orders where Order_Id = #value#
@@ -334,15 +334,15 @@
parameterClass="Integer"
resultClass="Order" >
select
- Orders.Order_Id as Id,
- Order_Date as Date,
- Order_CardExpiry as CardExpiry,
- Order_CardType as CardType,
- Order_CardNumber as CardNumber,
- Order_Street as Street,
- Order_City as City,
- Order_Province as Province,
- Order_PostalCode as PostalCode,
+ Orders.Order_Id as "Id",
+ Order_Date as "Date",
+ Order_CardExpiry as "CardExpiry",
+ Order_CardType as "CardType",
+ Order_CardNumber as "CardNumber",
+ Order_Street as "Street",
+ Order_City as "City",
+ Order_Province as "Province",
+ Order_PostalCode as "PostalCode",
LineItem_ID as "FavouriteLineItem.Id",
LineItem_Code as "FavouriteLineItem.Code",
LineItem_Quantity as "FavouriteLineItem.Quantity",
diff --git a/tests/unit/Data/SqlMap/maps/firebird/sqlmap.xml b/tests/unit/Data/SqlMap/maps/firebird/sqlmap.xml
index 43e9e32..451ae26 100644
--- a/tests/unit/Data/SqlMap/maps/firebird/sqlmap.xml
+++ b/tests/unit/Data/SqlMap/maps/firebird/sqlmap.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/tests/unit/Data/SqlMap/maps/mssql/Category.xml b/tests/unit/Data/SqlMap/maps/mssql/Category.xml
index f495837..1fc643c 100644
--- a/tests/unit/Data/SqlMap/maps/mssql/Category.xml
+++ b/tests/unit/Data/SqlMap/maps/mssql/Category.xml
@@ -12,7 +12,7 @@
select
Category_ID as Id,
Category_Name as Name,
- Category_Guid as Guid
+ Category_Guid as GuidString
from Categories
where Category_ID = #value#
@@ -73,13 +73,15 @@
select SCOPE_IDENTITY() as value
-
+
+
+ SELECT CAST(IDENT_CURRENT('Categories') AS INT) AS value
+
insert into Categories
(Category_Name, Category_Guid)
values
- (?,?);
- select SCOPE_IDENTITY() as value
-
+ (?,?)
+
update Categories set
@@ -144,7 +146,7 @@
-
+