Skip to content
Open
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
9 changes: 7 additions & 2 deletions .github/scripts/setup-sqlsrv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ set -euo pipefail

SQLCMD=/opt/mssql-tools18/bin/sqlcmd

curl -sSL -O https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
# msodbcsql18 (pulled in by mssql-tools18) is the ODBC driver pdo_sqlsrv needs.
# The repo package must match the runner's Ubuntu release; ubuntu-latest
# rolls between releases. Newer images preinstall packages-microsoft-prod
# with its keyring deleted, so the conffile flags reinstall it without a prompt.
. /etc/os-release
curl -sSL -O "https://packages.microsoft.com/config/ubuntu/${VERSION_ID}/packages-microsoft-prod.deb"
sudo dpkg -i --force-confnew --force-confmiss packages-microsoft-prod.deb
sudo apt-get update -qq
sudo ACCEPT_EULA=Y apt-get install -y mssql-tools18 unixodbc-dev

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Run `composer fix` before `composer fulltest` to auto-apply style fixes; `fullte

- Indentation: **tabs** (not spaces)
- Line endings: Unix `\n`
- PHP 8.1+ syntax; CI tests 8.1, 8.2, 8.3
- PHP 8.2+ syntax; CI tests 8.2, 8.3, 8.4, 8.5
- PSR-12 enforced via php-cs-fixer
- Uniform Access / Self Encapsulation: access own fields only through getters/setters, never `$this->_prop` directly in subclasses

Expand Down
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ First standalone release, planned as 1.0.0, for PRADO 4.4. SqlMap moves out of t

### Added

- `<insertOrIgnore>` and `<upsert>` mapping elements, from PRADO's Data layer renovation for 4.4 (pradosoft/prado#1088). They run through `TSqlMapGateway::insert()` as an `<insert>` does, with the driver's own conflict SQL. `<upsert>` takes `updateColumns` and `conflictColumns` attributes. Classes: `TSqlMapInsertOrIgnore`, `TSqlMapUpsert`, `TInsertOrIgnoreMappedStatement`, `TUpsertMappedStatement`. See [insertOrIgnore and upsert](docs/statements/upsert.md).
- `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 `<insert>`, `<update>`, or `<delete>` statement with a `cacheModel` attribute flushes that cache model when it executes, the same as a `<flushOnExecute>` 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`.
- `config/classes.json` maps the 59 SqlMap short class names (`TSqlMapConfig`, `TSqlMapFifoCache`, ...) to their FQNs. It replaces the SqlMap entries of the framework's `classes.php`.
- Message text for five codes that were thrown without a definition: `sqlmap_can_not_extend_select_key`, `sqlmap_configfile_invalid`, `sqlmap_must_enable_custom_paging`, `sqlmap_property_must_be_list`, `sqlmap_use_set_to_store_cache`.
- `TSqlMapException` accepts the `TException` argument forms: an optional integer error code first, and a trailing `Throwable` as the previous exception.

### Changed

- The package requires PRADO 4.4 (`"pradosoft/prado": "^4.4@dev"` until 4.4 is tagged) and therefore PHP 8.2.
- A parameter without a `dbType` binds with the type from `TDbCommand::getColumnTypeFromValue()`, which binds a null untyped on IBM DB2.
- Paging through a connection whose command builder is not an SQL builder throws `TSqlMapExecutionException` `sqlmap_limit_offset_unsupported`.
- The configuration and statement classes leave properties out of serialization through `_getZappableSleepProps()`, so `TComponent` also leaves out its default state.
- `TSqlMapException::getErrorCode()` returns the message code (for example `sqlmap_contains_no_statement`). It previously returned the translated message text.
- `messages.txt` entries are ICU MessageFormat patterns (pradosoft/prado#1437): `'{0}'` is `''{0}''`. Messages render as before. Under PRADO 4.3, `TSqlMapException::translateErrorMessage()` returns `''` as `'`.
- `messages.txt` entries are ICU MessageFormat patterns (pradosoft/prado#1437): `'{0}'` is `''{0}''`. Messages render as before.
- Active Record support is the `pradosoft/prado-activerecord` package (pradosoft/prado#1359). It is a suggested package and a development dependency for the Active Record SqlMap tests.

### Fixed
Expand All @@ -31,7 +36,7 @@ First standalone release, planned as 1.0.0, for PRADO 4.4. SqlMap moves out of t
- Firebird: a boolean parameter binds as `0` or `1`. `pdo_firebird` binds a PHP boolean only to a `BOOLEAN` column.
- `TCachingStatement` builds cache keys from the SQL text. It previously serialized the prepared command, which threw "Serialization of 'PDOStatement' is not allowed" for any cached statement with bound parameters.
- `TCachingStatement::executeInsert()` delegates to the wrapped statement. It previously called itself until the stack overflowed.
- `TParameterProperty::__sleep()` and `TResultProperty::__sleep()` name private properties by their namespaced class, so properties at their default values are left out of the serialized form. `TResultProperty` also misspelled `_propertyName`.
- `TParameterProperty` and `TResultProperty` name private properties by their namespaced class, so properties at their default values are left out of the serialized form. `TResultProperty` also misspelled `_propertyName`.
- `TResultProperty::getPropertyValueType()` no longer calls a method on `null` when no reflection class is found for the type.
- A `groupBy` result map no longer adds an element to a nested collection when every column of the nested result map is null, as in a LEFT JOIN row with no match. The collection stays empty. A parent row with no nested elements is still returned, in query order (#4).
- A nested result map with its own `groupBy` groups within its parent. The same `groupBy` value under two parents previously shared one tree key, so the second parent lost the element and the first parent's element was replaced.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,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.2, 8.3, 8.4, 8.5; the `pradosoft/prado` dev dependency requires 8.2)
- PHP minimum: **8.2**, as PRADO 4.4 requires (CI tests 8.2, 8.3, 8.4, 8.5)
- PSR-12 enforced via php-cs-fixer
- Use `?` for single nullable types and in doc blocks

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ SqlMap maps SQL statements to PHP objects using XML configuration files. Queries

## Requirements

- PHP 8.1 or later
- PRADO 4.3.3 or later (`pradosoft/prado`)
- PHP 8.2 or later
- PRADO 4.4 or later (`pradosoft/prado`)
- PDO extension and a PDO driver for your database

## Installation
Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@
}
],
"require": {
"php": ">=8.1.0",
"ext-pdo": "*"
"php": ">=8.2.0",
"ext-pdo": "*",
"pradosoft/prado": "^4.4@dev"
},
"require-dev": {
"pradosoft/prado": "dev-master",
"phpunit/phpunit": "^10",
"phpdocumentor/shim": "^3",
"friendsofphp/php-cs-fixer": "^3.94",
Expand Down
4 changes: 4 additions & 0 deletions config/classes.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"TSqlMapCacheTypes": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapCacheTypes",
"TSqlMapDelete": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapDelete",
"TSqlMapInsert": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapInsert",
"TSqlMapInsertOrIgnore": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapInsertOrIgnore",
"TSqlMapSelect": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapSelect",
"TSqlMapSelectKey": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapSelectKey",
"TSqlMapStatement": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapStatement",
"TSqlMapUpdate": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapUpdate",
"TSqlMapUpsert": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapUpsert",
"TSqlMapXmlConfigBuilder": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapXmlConfigBuilder",
"TSqlMapXmlConfiguration": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapXmlConfiguration",
"TSqlMapXmlMappingConfiguration": "Prado\\Data\\SqlMap\\Configuration\\TSqlMapXmlMappingConfiguration",
Expand All @@ -39,6 +41,7 @@
"TCachingStatement": "Prado\\Data\\SqlMap\\Statements\\TCachingStatement",
"TDeleteMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TDeleteMappedStatement",
"TInsertMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TInsertMappedStatement",
"TInsertOrIgnoreMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TInsertOrIgnoreMappedStatement",
"TMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TMappedStatement",
"TPostSelectBinding": "Prado\\Data\\SqlMap\\Statements\\TPostSelectBinding",
"TPreparedCommand": "Prado\\Data\\SqlMap\\Statements\\TPreparedCommand",
Expand All @@ -51,6 +54,7 @@
"TSqlMapObjectCollectionTree": "Prado\\Data\\SqlMap\\Statements\\TSqlMapObjectCollectionTree",
"TStaticSql": "Prado\\Data\\SqlMap\\Statements\\TStaticSql",
"TUpdateMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TUpdateMappedStatement",
"TUpsertMappedStatement": "Prado\\Data\\SqlMap\\Statements\\TUpsertMappedStatement",
"TSqlMapConfig": "Prado\\Data\\SqlMap\\TSqlMapConfig",
"TSqlMapGateway": "Prado\\Data\\SqlMap\\TSqlMapGateway",
"TSqlMapManager": "Prado\\Data\\SqlMap\\TSqlMapManager"
Expand Down
6 changes: 4 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SqlMap is a data-mapper layer that maps SQL statements defined in XML files to P
| [Prado Integration](integration.md) | Registering `TSqlMapConfig` as a PRADO application module, connection configuration, and caching the parsed manager |
| [Quick Start](quickstart.md) | Minimal working example: config file, mapping file, and PHP calls |
| [Configuration](configuration/README.md) | `sqlMapConfig` root file reference — properties, type aliases, type handlers, transaction manager, data source, and `sqlMaps` includes |
| [Statements](statements/README.md) | `select`, `insert`, `update`, `delete`, `statement`, and `procedure` tags; `selectKey`; `TSqlMapGateway` PHP API |
| [Statements](statements/README.md) | `select`, `insert`, `insertOrIgnore`, `upsert`, `update`, `delete`, `statement`, and `procedure` tags; `selectKey`; `TSqlMapGateway` PHP API |
| [Mappings](mappings/README.md) | `resultMap`, `parameterMap`, inline parameter syntax, discriminators, and sub-maps |
| [Dynamic SQL](dynamic-sql/README.md) | `dynamic`, conditional tags (`isNull`, `isEqual`, `iterate`, etc.), and inline substitution tokens |

Expand Down Expand Up @@ -43,7 +43,9 @@ SqlMap is a data-mapper layer that maps SQL statements defined in XML files to P
| `parameter` | `parameterMap` | Declares one parameter: property, db type, null value, and mode | [Mappings](mappings/README.md#parameter) |
| `select` | `sqlMap` | Mapped `SELECT` statement; returns one object or a list | [Statements](statements/README.md#select) |
| `insert` | `sqlMap` | Mapped `INSERT` statement; optionally returns a generated key | [Statements](statements/README.md#insert) |
| `selectKey` | `insert` | Executes a query to retrieve the generated primary key | [Statements](statements/README.md#selectkey) |
| `insertOrIgnore` | `sqlMap` | Mapped `INSERT` that skips a conflicting row | [insertOrIgnore and upsert](statements/upsert.md) |
| `upsert` | `sqlMap` | Mapped `INSERT` that updates a conflicting row | [insertOrIgnore and upsert](statements/upsert.md) |
| `selectKey` | `insert`, `insertOrIgnore`, `upsert` | Executes a query to retrieve the generated primary key | [Statements](statements/README.md#selectkey) |
| `update` | `sqlMap` | Mapped `UPDATE` statement; returns affected-row count | [Statements](statements/README.md#update) |
| `delete` | `sqlMap` | Mapped `DELETE` statement; returns affected-row count | [Statements](statements/README.md#delete) |
| `statement` | `sqlMap` | Generic mapped statement for any SQL that does not fit a specific DML tag | [Statements](statements/README.md#statement) |
Expand Down
4 changes: 3 additions & 1 deletion docs/statements/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Statements

SqlMap supports five statement types for executing SQL: [`select`](select.md), [`insert`](insert.md), [`update`](update.md), [`delete`](delete.md), and [`statement`](statement.md) (generic). A sixth element, [`selectKey`](insert.md#selectkey), appears only as a child of `insert` and retrieves generated key values before or after the insert executes.
SqlMap supports seven statement types for executing SQL: [`select`](select.md), [`insert`](insert.md), [`insertOrIgnore` and `upsert`](upsert.md), [`update`](update.md), [`delete`](delete.md), and [`statement`](statement.md) (generic). The [`selectKey`](insert.md#selectkey) element appears only as a child of `insert`, `insertOrIgnore`, or `upsert` and retrieves generated key values before or after the insert executes.

All statement elements share a common set of attributes, and all are defined inside a [mapping file](../mappings/README.md) as children of the root `<sqlMap>` element.

Expand Down Expand Up @@ -30,6 +30,8 @@ All statement elements share a common set of attributes, and all are defined ins
|---|---|---|
| [`select`](select.md) | `queryForObject`, `queryForList`, `queryForMap`, `queryForPagedList` | Reads rows; result mapping applied; supports caching. |
| [`insert`](insert.md) | `insert` | Writes one row; optional `<selectKey>` child returns the generated key. |
| [`insertOrIgnore`](upsert.md) | `insert` | Writes one row unless it conflicts with an existing key. |
| [`upsert`](upsert.md) | `insert` | Writes one row, or updates the row it conflicts with. |
| [`update`](update.md) | `update` | Modifies rows; returns affected-row count. |
| [`delete`](delete.md) | `delete` | Removes rows; returns affected-row count. |
| [`statement`](statement.md) | `queryForObject`, `queryForList`, `insert`, `update`, `delete` | Generic; use when the DML type is variable or unknown at mapping time. |
Expand Down
63 changes: 63 additions & 0 deletions docs/statements/upsert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[SqlMap Docs](../README.md) -> [Statements](README.md) -> insertOrIgnore and upsert

# insertOrIgnore and upsert

The `insertOrIgnore` element maps an `INSERT` that skips a row conflicting with an existing key. The `upsert` element maps an `INSERT` that updates the conflicting row. Both run through `TSqlMapGateway::insert()` and behave as [`insert`](insert.md): they take the same attributes, accept a [`selectKey`](selectKey.md) child, return the `selectKey` value or `null`, and flush the `cacheModel` they name.

The SQL in the element is the driver's own conflict syntax. SqlMap does not rewrite it.

| Driver | `insertOrIgnore` | `upsert` |
|---|---|---|
| SQLite | `INSERT OR IGNORE INTO ...` | `INSERT ... ON CONFLICT (col) DO UPDATE SET ...` |
| MySQL | `INSERT IGNORE INTO ...` | `INSERT ... ON DUPLICATE KEY UPDATE ...` |
| PostgreSQL | `INSERT ... ON CONFLICT DO NOTHING` | `INSERT ... ON CONFLICT (col) DO UPDATE SET ...` |
| SQL Server, Oracle, DB2, Firebird | `MERGE` without `WHEN MATCHED` | `MERGE ... WHEN MATCHED THEN UPDATE ...` |

---

## Syntax

```xml
<sqlMap>
<insertOrIgnore id="addTag" parameterClass="Application\Model\Tag">
INSERT OR IGNORE INTO tags (name) VALUES (#name#)
</insertOrIgnore>

<upsert id="saveScore" parameterClass="Application\Model\Score"
updateColumns="score, updated" conflictColumns="player">
INSERT INTO scores (player, score, updated) VALUES (#player#, #score#, #updated#)
ON CONFLICT (player) DO UPDATE SET score = excluded.score, updated = excluded.updated
</upsert>
</sqlMap>
```

```php
$gateway->insert('addTag', $tag);
$gateway->insert('saveScore', $score);
```

---

## Attributes

`insertOrIgnore` takes the attributes of [`insert`](insert.md#attributes). `upsert` adds two:

| Attribute | Type | Description |
|---|---|---|
| `updateColumns` | comma-separated list | Columns the statement updates on a conflict. Stored as `TSqlMapUpsert::getUpdateColumns()`; `null` when absent. |
| `conflictColumns` | comma-separated list | Columns that detect the conflict. Stored as `TSqlMapUpsert::getConflictColumns()`; `null` when absent. |

The two attributes describe the statement for code that reads the mapping. They do not change the SQL.

---

## Classes

| Element | Statement class | Mapped statement class |
|---|---|---|
| `insertOrIgnore` | `TSqlMapInsertOrIgnore` (a `TSqlMapInsert`) | `TInsertOrIgnoreMappedStatement` (a `TInsertMappedStatement`) |
| `upsert` | `TSqlMapUpsert` (a `TSqlMapInsert`) | `TUpsertMappedStatement` (a `TInsertMappedStatement`) |

---

[SqlMap Docs](../README.md) -> [Statements](README.md) -> insertOrIgnore and upsert
5 changes: 4 additions & 1 deletion src/Data/SqlMap/Configuration/TDiscriminator.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

/**
* TDiscriminator and TSubMap classes file.
* TDiscriminator classes file.
*
* @author Wei Zhuo <weizhuo[at]gmail[dot]com>
* @link https://github.com/pradosoft/prado
Expand All @@ -14,6 +14,8 @@
use Prado\Data\TSqlMapManager;

/**
* TDiscriminator class
*
* The TDiscriminator corresponds to the <discriminator> tag within a <resultMap>.
*
* TDiscriminator allows inheritance logic in SqlMap result mappings.
Expand All @@ -25,6 +27,7 @@
*
* @author Wei Zhuo <weizho[at]gmail[dot]com>
* @since 3.1
* @see TSubMap
*/
class TDiscriminator extends \Prado\TComponent
{
Expand Down
5 changes: 2 additions & 3 deletions src/Data/SqlMap/Configuration/TParameterProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ public function setNullValue($value)
$this->_nullValue = $value;
}

public function __sleep()
protected function _getZappableSleepProps(&$exprops)
{
$exprops = [];
parent::_getZappableSleepProps($exprops);
$cn = __CLASS__;
if ($this->_typeHandler === null) {
$exprops[] = "\0$cn\0_typeHandler";
Expand All @@ -155,6 +155,5 @@ public function __sleep()
if ($this->_nullValue === null) {
$exprops[] = "\0$cn\0_nullValue";
}
return array_diff(parent::__sleep(), $exprops);
}
}
5 changes: 2 additions & 3 deletions src/Data/SqlMap/Configuration/TResultProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,9 @@ public function instanceOfArrayType($target)
return $this->getPropertyValueType() == self::ARRAY_TYPE;
}

public function __sleep()
protected function _getZappableSleepProps(&$exprops)
{
$exprops = [];
parent::_getZappableSleepProps($exprops);
$cn = __CLASS__;
if ($this->_nullValue === null) {
$exprops[] = "\0$cn\0_nullValue";
Expand Down Expand Up @@ -380,6 +380,5 @@ public function __sleep()
if ($this->_select === null) {
$exprops[] = "\0$cn\0_select";
}
return array_diff(parent::__sleep(), $exprops);
}
}
24 changes: 24 additions & 0 deletions src/Data/SqlMap/Configuration/TSqlMapInsertOrIgnore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/**
* TSqlMapInsertOrIgnore class file.
*
* @author Brad Anderson <belisoful@icloud.com>
* @link https://github.com/pradosoft/prado
* @license https://github.com/pradosoft/prado/blob/master/LICENSE
*/

namespace Prado\Data\SqlMap\Configuration;

/**
* TSqlMapInsertOrIgnore corresponds to the <insertOrIgnore> element.
*
* Behaves identically to TSqlMapInsert but executes via insertOrIgnore(),
* silently ignoring duplicate key conflicts.
*
* @author Brad Anderson <belisoful@icloud.com>
* @since 4.4.0
*/
class TSqlMapInsertOrIgnore extends TSqlMapInsert
{
}
10 changes: 3 additions & 7 deletions src/Data/SqlMap/Configuration/TSqlMapStatement.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,11 @@ public function createInstanceOfResultClass($registry, $row)
}
}

public function __sleep()
protected function _getZappableSleepProps(&$exprops)
{
parent::_getZappableSleepProps($exprops);
$cn = __CLASS__;
$exprops = ["\0$cn\0_resultMap"];
$exprops[] = "\0$cn\0_resultMap";
if (!$this->_parameterMapName) {
$exprops[] = "\0$cn\0_parameterMapName";
}
Expand All @@ -316,9 +317,6 @@ public function __sleep()
if (!$this->_resultMapName) {
$exprops[] = "\0$cn\0_resultMapName";
}
if (!$this->_resultMap) {
$exprops[] = "\0$cn\0_resultMap";
}
if (!$this->_resultClassName) {
$exprops[] = "\0$cn\0_resultClassName";
}
Expand All @@ -337,7 +335,5 @@ public function __sleep()
if (!$this->_cache) {
$exprops[] = "\0$cn\0_cache";
}

return array_diff(parent::__sleep(), $exprops);
}
}
Loading
Loading