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
35 changes: 18 additions & 17 deletions .github/workflows/prado-sqlmap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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') }}
Expand All @@ -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
Expand All @@ -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') }}
Expand All @@ -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
Expand All @@ -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') }}
Expand Down Expand Up @@ -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
Expand All @@ -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') }}
Expand Down Expand Up @@ -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
Expand All @@ -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') }}
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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') }}
Expand Down Expand Up @@ -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
Expand All @@ -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') }}
Expand Down Expand Up @@ -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
Expand All @@ -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') }}
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
"Prado\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Prado\\Test\\Unit\\": "vendor/pradosoft/prado/tests/unit/"
}
},
"repositories": [
{
"type": "composer",
Expand All @@ -47,7 +52,7 @@
},
"config": {
"platform": {
"php": "8.1"
"php": "8.2"
},
"allow-plugins": {
"phpdocumentor/shim": true
Expand Down
3 changes: 3 additions & 0 deletions src/Data/SqlMap/Statements/TMappedStatement.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
Expand Down
12 changes: 11 additions & 1 deletion src/Data/SqlMap/Statements/TPreparedCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions tests/test_tools/phpunit_bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
20 changes: 17 additions & 3 deletions tests/unit/Data/SqlMap/ActiveRecordSqlMapTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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__)
Expand Down Expand Up @@ -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');
Expand Down
36 changes: 25 additions & 11 deletions tests/unit/Data/SqlMap/BaseCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/Data/SqlMap/DbSpecific/Ibm/IbmBaseTestConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 6 additions & 4 deletions tests/unit/Data/SqlMap/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading