diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index a065b589bff..5963083840a 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -106,10 +106,11 @@ jobs:
oracle-version:
- "18"
- "21"
+ - "23"
services:
oracle:
- image: gvenzl/oracle-xe:${{ matrix.oracle-version }}
+ image: gvenzl/oracle-${{ matrix.oracle-version < 23 && 'xe' || 'free' }}:${{ matrix.oracle-version }}
env:
ORACLE_PASSWORD: oracle
ports:
@@ -140,7 +141,7 @@ jobs:
composer-options: "--ignore-platform-req=php+"
- name: "Run PHPUnit"
- run: "vendor/bin/phpunit -c ci/github/phpunit/oci8.xml --coverage-clover=coverage.xml"
+ run: "vendor/bin/phpunit -c ci/github/phpunit/oci8${{ matrix.oracle-version < 23 && '-21' || '' }}.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
@@ -162,10 +163,11 @@ jobs:
oracle-version:
- "18"
- "21"
+ - "23"
services:
oracle:
- image: gvenzl/oracle-xe:${{ matrix.oracle-version }}
+ image: gvenzl/oracle-${{ matrix.oracle-version < 23 && 'xe' || 'free' }}:${{ matrix.oracle-version }}
env:
ORACLE_PASSWORD: oracle
ports:
@@ -196,7 +198,7 @@ jobs:
composer-options: "--ignore-platform-req=php+"
- name: "Run PHPUnit"
- run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_oci.xml --coverage-clover=coverage.xml"
+ run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_oci${{ matrix.oracle-version < 23 && '-21' || '' }}.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
diff --git a/UPGRADE.md b/UPGRADE.md
index 6662e215a47..8fb7172b20a 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -921,6 +921,18 @@ The following methods have been removed.
# Upgrade to 3.8
+## Deprecated lock-related `AbstractPlatform` methods
+
+The usage of `AbstractPlatform::getReadLockSQL()`, `::getWriteLockSQL()` and `::getForUpdateSQL` is deprecated as this
+API is not portable. Use `QueryBuilder::forUpdate()` as a replacement for the latter.
+
+## Deprecated `AbstractMySQLPlatform` methods
+
+* `AbstractMySQLPlatform::getColumnTypeSQLSnippets()` has been deprecated
+ in favor of `AbstractMySQLPlatform::getColumnTypeSQLSnippet()`.
+* `AbstractMySQLPlatform::getDatabaseNameSQL()` has been deprecated without replacement.
+* Not passing a database name to `AbstractMySQLPlatform::getColumnTypeSQLSnippet()` has been deprecated.
+
## Deprecated reset methods from `QueryBuilder`
`QueryBuilder::resetQueryParts()` has been deprecated.
diff --git a/ci/github/phpunit/oci8-21.xml b/ci/github/phpunit/oci8-21.xml
new file mode 100644
index 00000000000..d8e1d99fbf7
--- /dev/null
+++ b/ci/github/phpunit/oci8-21.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ../../../tests
+
+
+
+
+
+ ../../../src
+
+
+
diff --git a/ci/github/phpunit/oci8.xml b/ci/github/phpunit/oci8.xml
index f333e9d30a8..c13b34d055b 100644
--- a/ci/github/phpunit/oci8.xml
+++ b/ci/github/phpunit/oci8.xml
@@ -14,14 +14,14 @@
-
+
-
+
diff --git a/ci/github/phpunit/pdo_oci-21.xml b/ci/github/phpunit/pdo_oci-21.xml
new file mode 100644
index 00000000000..ef1e272e5e0
--- /dev/null
+++ b/ci/github/phpunit/pdo_oci-21.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ../../../tests
+
+
+
+
+
+ ../../../src
+
+
+
diff --git a/ci/github/phpunit/pdo_oci.xml b/ci/github/phpunit/pdo_oci.xml
index 7a9fb1ee900..3a83d2e4d03 100644
--- a/ci/github/phpunit/pdo_oci.xml
+++ b/ci/github/phpunit/pdo_oci.xml
@@ -14,14 +14,14 @@
-
+
-
+
diff --git a/psalm.xml.dist b/psalm.xml.dist
index fa949b53ddd..c15dddaf5b7 100644
--- a/psalm.xml.dist
+++ b/psalm.xml.dist
@@ -44,8 +44,19 @@
+
+
+
+
+
+
+
+
diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php
index ce58f9aa36b..8d81b6c0e16 100644
--- a/src/Driver/AbstractMySQLDriver.php
+++ b/src/Driver/AbstractMySQLDriver.php
@@ -9,6 +9,7 @@
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\Exception\InvalidPlatformVersion;
use Doctrine\DBAL\Platforms\MariaDB1052Platform;
+use Doctrine\DBAL\Platforms\MariaDB1060Platform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
@@ -33,6 +34,10 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs
$version = $versionProvider->getServerVersion();
if (stripos($version, 'mariadb') !== false) {
$mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version);
+ if (version_compare($mariaDbVersion, '10.6.0', '>=')) {
+ return new MariaDB1060Platform();
+ }
+
if (version_compare($mariaDbVersion, '10.5.2', '>=')) {
return new MariaDB1052Platform();
}
diff --git a/src/Driver/OCI8/Driver.php b/src/Driver/OCI8/Driver.php
index 698cf359a42..d519cc94c75 100644
--- a/src/Driver/OCI8/Driver.php
+++ b/src/Driver/OCI8/Driver.php
@@ -10,6 +10,7 @@
use SensitiveParameter;
use function oci_connect;
+use function oci_new_connect;
use function oci_pconnect;
use const OCI_NO_AUTO_COMMIT;
diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php
index d3f411a83d6..0a0ad9ad6a7 100644
--- a/src/Platforms/AbstractMySQLPlatform.php
+++ b/src/Platforms/AbstractMySQLPlatform.php
@@ -14,13 +14,17 @@
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\MySQLSchemaManager;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types\Types;
+use Doctrine\Deprecations\Deprecation;
use function array_merge;
use function array_unique;
use function array_values;
use function count;
+use function func_get_args;
use function implode;
use function in_array;
use function is_numeric;
@@ -216,6 +220,8 @@ public function supportsColumnCollation(): bool
}
/**
+ * @deprecated Use {@see getColumnTypeSQLSnippet()} instead.
+ *
* The SQL snippets required to elucidate a column type
*
* Returns an array of the form [column type SELECT snippet, additional JOIN statement snippet]
@@ -224,7 +230,24 @@ public function supportsColumnCollation(): bool
*/
public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array
{
- return [$tableAlias . '.COLUMN_TYPE', ''];
+ Deprecation::triggerIfCalledFromOutside(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/6202',
+ 'AbstractMySQLPlatform::getColumnTypeSQLSnippets() is deprecated. '
+ . 'Use AbstractMySQLPlatform::getColumnTypeSQLSnippet() instead.',
+ );
+
+ return [$this->getColumnTypeSQLSnippet(...func_get_args()), ''];
+ }
+
+ /**
+ * The SQL snippet required to elucidate a column type
+ *
+ * Returns a column type SELECT snippet string
+ */
+ public function getColumnTypeSQLSnippet(string $tableAlias, string $databaseName): string
+ {
+ return $tableAlias . '.COLUMN_TYPE';
}
/**
@@ -282,6 +305,11 @@ protected function _getCreateTableSQL(string $name, array $columns, array $optio
return $sql;
}
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null);
+ }
+
/**
* Build SQL for table options
*
diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php
index 51363f46bd9..6dcf3ba0ede 100644
--- a/src/Platforms/AbstractPlatform.php
+++ b/src/Platforms/AbstractPlatform.php
@@ -26,11 +26,14 @@
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Schema\UniqueConstraint;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\SQL\Parser;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
use Doctrine\DBAL\Types\Exception\TypeNotFound;
use Doctrine\DBAL\Types\Type;
+use Doctrine\Deprecations\Deprecation;
use function addcslashes;
use function array_map;
@@ -700,9 +703,18 @@ abstract public function getCurrentDatabaseExpression(): string;
/**
* Returns the FOR UPDATE expression.
+ *
+ * @deprecated This API is not portable. Use {@link QueryBuilder::forUpdate()}` instead.
*/
public function getForUpdateSQL(): string
{
+ Deprecation::triggerIfCalledFromOutside(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/6191',
+ '%s is deprecated as non-portable.',
+ __METHOD__,
+ );
+
return 'FOR UPDATE';
}
@@ -722,9 +734,18 @@ public function appendLockHint(string $fromClause, LockMode $lockMode): string
*
* This defaults to the ANSI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database
* vendors allow to lighten this constraint up to be a real read lock.
+ *
+ * @deprecated This API is not portable.
*/
public function getReadLockSQL(): string
{
+ Deprecation::trigger(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/6191',
+ '%s is deprecated as non-portable.',
+ __METHOD__,
+ );
+
return $this->getForUpdateSQL();
}
@@ -732,9 +753,18 @@ public function getReadLockSQL(): string
* Returns the SQL snippet to append to any SELECT statement which obtains an exclusive lock on the rows.
*
* The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ANSI SQL standard.
+ *
+ * @deprecated This API is not portable.
*/
public function getWriteLockSQL(): string
{
+ Deprecation::trigger(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/6191',
+ '%s is deprecated as non-portable.',
+ __METHOD__,
+ );
+
return $this->getForUpdateSQL();
}
@@ -799,6 +829,11 @@ public function getCreateTableSQL(Table $table): array
return $this->buildCreateTableSQL($table, true);
}
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED');
+ }
+
/**
* @internal
*
diff --git a/src/Platforms/DB2Platform.php b/src/Platforms/DB2Platform.php
index 6e894b5e59e..31cdfb677b7 100644
--- a/src/Platforms/DB2Platform.php
+++ b/src/Platforms/DB2Platform.php
@@ -13,6 +13,8 @@
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types\Types;
@@ -557,6 +559,12 @@ public function supportsIdentityColumns(): bool
return true;
}
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null);
+ }
+
+ /** @deprecated This API is not portable. */
public function getForUpdateSQL(): string
{
return ' WITH RR USE AND KEEP UPDATE LOCKS';
diff --git a/src/Platforms/MariaDB1060Platform.php b/src/Platforms/MariaDB1060Platform.php
new file mode 100644
index 00000000000..6f45f1021fa
--- /dev/null
+++ b/src/Platforms/MariaDB1060Platform.php
@@ -0,0 +1,18 @@
+quoteStringLiteral($databaseName);
+
+ // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues
+ return <<isDistinct()) {
+ $parts[] = 'DISTINCT';
+ }
+
+ $parts[] = implode(', ', $query->getColumns());
+
+ $from = $query->getFrom();
+
+ if (count($from) > 0) {
+ $parts[] = 'FROM ' . implode(', ', $from);
+ }
+
+ $forUpdate = $query->getForUpdate();
+
+ if ($forUpdate !== null) {
+ $with = ['UPDLOCK', 'ROWLOCK'];
+
+ if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) {
+ $with[] = 'READPAST';
+ }
+
+ $parts[] = 'WITH (' . implode(', ', $with) . ')';
+ }
+
+ $where = $query->getWhere();
+
+ if ($where !== null) {
+ $parts[] = 'WHERE ' . $where;
+ }
+
+ $groupBy = $query->getGroupBy();
+
+ if (count($groupBy) > 0) {
+ $parts[] = 'GROUP BY ' . implode(', ', $groupBy);
+ }
+
+ $having = $query->getHaving();
+
+ if ($having !== null) {
+ $parts[] = 'HAVING ' . $having;
+ }
+
+ $orderBy = $query->getOrderBy();
+
+ if (count($orderBy) > 0) {
+ $parts[] = 'ORDER BY ' . implode(', ', $orderBy);
+ }
+
+ $sql = implode(' ', $parts);
+ $limit = $query->getLimit();
+
+ if ($limit->isDefined()) {
+ $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult());
+ }
+
+ return $sql;
+ }
+}
diff --git a/src/Platforms/SQLServerPlatform.php b/src/Platforms/SQLServerPlatform.php
index ee4d217b99d..6078fe7baad 100644
--- a/src/Platforms/SQLServerPlatform.php
+++ b/src/Platforms/SQLServerPlatform.php
@@ -9,6 +9,7 @@
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
use Doctrine\DBAL\Platforms\Keywords\SQLServerKeywords;
+use Doctrine\DBAL\Platforms\SQLServer\SQL\Builder\SQLServerSelectSQLBuilder;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\Identifier;
@@ -16,6 +17,7 @@
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\SQLServerSchemaManager;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types\Types;
use InvalidArgumentException;
@@ -47,6 +49,11 @@ class SQLServerPlatform extends AbstractPlatform
/** @internal Should be used only from within the {@see AbstractSchemaManager} class hierarchy. */
public const OPTION_DEFAULT_CONSTRAINT_NAME = 'default_constraint_name';
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new SQLServerSelectSQLBuilder($this);
+ }
+
public function getCurrentDateSQL(): string
{
return $this->getConvertExpression('date', 'GETDATE()');
@@ -1089,6 +1096,7 @@ public function appendLockHint(string $fromClause, LockMode $lockMode): string
};
}
+ /** @deprecated This API is not portable. */
public function getForUpdateSQL(): string
{
return ' ';
diff --git a/src/Platforms/SQLitePlatform.php b/src/Platforms/SQLitePlatform.php
index f2aa46a0485..0cca9c67eb6 100644
--- a/src/Platforms/SQLitePlatform.php
+++ b/src/Platforms/SQLitePlatform.php
@@ -17,6 +17,8 @@
use Doctrine\DBAL\Schema\SQLiteSchemaManager;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
@@ -147,6 +149,12 @@ public function getCurrentDatabaseExpression(): string
return "'main'";
}
+ /** @link https://www2.sqlite.org/cvstrac/wiki?p=UnsupportedSql */
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new DefaultSelectSQLBuilder($this, null, null);
+ }
+
protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string
{
return match ($level) {
diff --git a/src/Query/ForUpdate.php b/src/Query/ForUpdate.php
new file mode 100644
index 00000000000..f511f6404be
--- /dev/null
+++ b/src/Query/ForUpdate.php
@@ -0,0 +1,19 @@
+conflictResolutionMode;
+ }
+}
diff --git a/src/Query/ForUpdate/ConflictResolutionMode.php b/src/Query/ForUpdate/ConflictResolutionMode.php
new file mode 100644
index 00000000000..f968f7b941d
--- /dev/null
+++ b/src/Query/ForUpdate/ConflictResolutionMode.php
@@ -0,0 +1,27 @@
+maxResults !== null || $this->firstResult !== 0;
+ }
+
+ public function getMaxResults(): ?int
+ {
+ return $this->maxResults;
+ }
+
+ public function getFirstResult(): int
+ {
+ return $this->firstResult;
+ }
+}
diff --git a/src/Query/QueryBuilder.php b/src/Query/QueryBuilder.php
index 435c861c6c7..a2710592f52 100644
--- a/src/Query/QueryBuilder.php
+++ b/src/Query/QueryBuilder.php
@@ -13,6 +13,7 @@
use Doctrine\DBAL\Query\Exception\UnknownAlias;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
+use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Statement;
use Doctrine\DBAL\Types\Type;
@@ -143,6 +144,8 @@ class QueryBuilder
*/
private array $orderBy = [];
+ private ?ForUpdate $forUpdate = null;
+
/**
* The values of an INSERT query.
*
@@ -484,6 +487,20 @@ public function getMaxResults(): ?int
return $this->maxResults;
}
+ /**
+ * Locks the queried rows for a subsequent update.
+ *
+ * @return $this
+ */
+ public function forUpdate(int $conflictResolutionMode = ConflictResolutionMode::ORDINARY): self
+ {
+ $this->forUpdate = new ForUpdate($conflictResolutionMode);
+
+ $this->sql = null;
+
+ return $this;
+ }
+
/**
* Specifies an item that is to be returned in the query result.
* Replaces any previously specified selections, if any.
@@ -503,10 +520,6 @@ public function select(string ...$expressions): self
{
$this->type = QueryType::SELECT;
- if (count($expressions) < 1) {
- return $this;
- }
-
$this->select = $expressions;
$this->sql = null;
@@ -1190,50 +1203,28 @@ public function resetOrderBy(): self
return $this;
}
- /** @throws QueryException */
+ /** @throws Exception */
private function getSQLForSelect(): string
{
if (count($this->select) === 0) {
throw new QueryException('No SELECT expressions given. Please use select() or addSelect().');
}
- $query = 'SELECT';
-
- if ($this->distinct) {
- $query .= ' DISTINCT';
- }
-
- $query .= ' ' . implode(', ', $this->select);
-
- if (count($this->from) !== 0) {
- $query .= ' FROM ' . implode(', ', $this->getFromClauses());
- }
-
- if ($this->where !== null) {
- $query .= ' WHERE ' . $this->where;
- }
-
- if (count($this->groupBy) !== 0) {
- $query .= ' GROUP BY ' . implode(', ', $this->groupBy);
- }
-
- if ($this->having !== null) {
- $query .= ' HAVING ' . $this->having;
- }
-
- if (count($this->orderBy) !== 0) {
- $query .= ' ORDER BY ' . implode(', ', $this->orderBy);
- }
-
- if ($this->isLimitQuery()) {
- return $this->connection->getDatabasePlatform()->modifyLimitQuery(
- $query,
- $this->maxResults,
- $this->firstResult,
+ return $this->connection->getDatabasePlatform()
+ ->createSelectSQLBuilder()
+ ->buildSQL(
+ new SelectQuery(
+ $this->distinct,
+ $this->select,
+ $this->getFromClauses(),
+ $this->where !== null ? (string) $this->where : null,
+ $this->groupBy,
+ $this->having !== null ? (string) $this->having : null,
+ $this->orderBy,
+ new Limit($this->maxResults, $this->firstResult),
+ $this->forUpdate,
+ ),
);
- }
-
- return $query;
}
/**
@@ -1279,11 +1270,6 @@ private function verifyAllAliasesAreKnown(array $knownAliases): void
}
}
- private function isLimitQuery(): bool
- {
- return $this->maxResults !== null || $this->firstResult !== 0;
- }
-
/**
* Converts this instance into an INSERT string in SQL.
*/
diff --git a/src/Query/SelectQuery.php b/src/Query/SelectQuery.php
new file mode 100644
index 00000000000..e6ef9f26c24
--- /dev/null
+++ b/src/Query/SelectQuery.php
@@ -0,0 +1,78 @@
+distinct;
+ }
+
+ /** @return string[] */
+ public function getColumns(): array
+ {
+ return $this->columns;
+ }
+
+ /** @return string[] */
+ public function getFrom(): array
+ {
+ return $this->from;
+ }
+
+ public function getWhere(): ?string
+ {
+ return $this->where;
+ }
+
+ /** @return string[] */
+ public function getGroupBy(): array
+ {
+ return $this->groupBy;
+ }
+
+ public function getHaving(): ?string
+ {
+ return $this->having;
+ }
+
+ /** @return string[] */
+ public function getOrderBy(): array
+ {
+ return $this->orderBy;
+ }
+
+ public function getLimit(): Limit
+ {
+ return $this->limit;
+ }
+
+ public function getForUpdate(): ?ForUpdate
+ {
+ return $this->forUpdate;
+ }
+}
diff --git a/src/SQL/Builder/DefaultSelectSQLBuilder.php b/src/SQL/Builder/DefaultSelectSQLBuilder.php
new file mode 100644
index 00000000000..a30120e5eb2
--- /dev/null
+++ b/src/SQL/Builder/DefaultSelectSQLBuilder.php
@@ -0,0 +1,94 @@
+isDistinct()) {
+ $parts[] = 'DISTINCT';
+ }
+
+ $parts[] = implode(', ', $query->getColumns());
+
+ $from = $query->getFrom();
+
+ if (count($from) > 0) {
+ $parts[] = 'FROM ' . implode(', ', $from);
+ }
+
+ $where = $query->getWhere();
+
+ if ($where !== null) {
+ $parts[] = 'WHERE ' . $where;
+ }
+
+ $groupBy = $query->getGroupBy();
+
+ if (count($groupBy) > 0) {
+ $parts[] = 'GROUP BY ' . implode(', ', $groupBy);
+ }
+
+ $having = $query->getHaving();
+
+ if ($having !== null) {
+ $parts[] = 'HAVING ' . $having;
+ }
+
+ $orderBy = $query->getOrderBy();
+
+ if (count($orderBy) > 0) {
+ $parts[] = 'ORDER BY ' . implode(', ', $orderBy);
+ }
+
+ $sql = implode(' ', $parts);
+ $limit = $query->getLimit();
+
+ if ($limit->isDefined()) {
+ $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult());
+ }
+
+ $forUpdate = $query->getForUpdate();
+
+ if ($forUpdate !== null) {
+ if ($this->forUpdateSQL === null) {
+ throw NotSupported::new('FOR UPDATE');
+ }
+
+ $sql .= ' ' . $this->forUpdateSQL;
+
+ if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) {
+ if ($this->skipLockedSQL === null) {
+ throw NotSupported::new('SKIP LOCKED');
+ }
+
+ $sql .= ' ' . $this->skipLockedSQL;
+ }
+ }
+
+ return $sql;
+ }
+}
diff --git a/src/SQL/Builder/SelectSQLBuilder.php b/src/SQL/Builder/SelectSQLBuilder.php
new file mode 100644
index 00000000000..c013f96a8bd
--- /dev/null
+++ b/src/SQL/Builder/SelectSQLBuilder.php
@@ -0,0 +1,14 @@
+platform->getColumnTypeSQLSnippets();
+ // @todo 4.0 - call getColumnTypeSQLSnippet() instead
+ [$columnTypeSQL, $joinCheckConstraintSQL] = $this->platform->getColumnTypeSQLSnippets('c', $databaseName);
$sql = 'SELECT';
diff --git a/tests/Driver/VersionAwarePlatformDriverTest.php b/tests/Driver/VersionAwarePlatformDriverTest.php
index 55c830bedd3..f6b8c142494 100644
--- a/tests/Driver/VersionAwarePlatformDriverTest.php
+++ b/tests/Driver/VersionAwarePlatformDriverTest.php
@@ -8,6 +8,7 @@
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MariaDB1052Platform;
+use Doctrine\DBAL\Platforms\MariaDB1060Platform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
@@ -39,7 +40,8 @@ public static function mySQLVersionProvider(): array
['10.2.8-MariaDB-10.2.8+maria~xenial-log', MariaDBPlatform::class],
['10.2.8-MariaDB-1~lenny-log', MariaDBPlatform::class],
['10.5.2-MariaDB-1~lenny-log', MariaDB1052Platform::class],
- ['11.0.2-MariaDB-1:11.0.2+maria~ubu2204', MariaDB1052Platform::class],
+ ['10.6.0-MariaDB-1~lenny-log', MariaDB1060Platform::class],
+ ['11.0.2-MariaDB-1:11.0.2+maria~ubu2204', MariaDB1060Platform::class],
];
}
diff --git a/tests/Functional/Query/QueryBuilderTest.php b/tests/Functional/Query/QueryBuilderTest.php
new file mode 100644
index 00000000000..cc047ac44bf
--- /dev/null
+++ b/tests/Functional/Query/QueryBuilderTest.php
@@ -0,0 +1,126 @@
+addColumn('id', Types::INTEGER);
+ $table->setPrimaryKey(['id']);
+
+ $this->dropAndCreateTable($table);
+
+ $this->connection->insert('for_update', ['id' => 1]);
+ $this->connection->insert('for_update', ['id' => 2]);
+ }
+
+ protected function tearDown(): void
+ {
+ if (! $this->connection->isTransactionActive()) {
+ return;
+ }
+
+ $this->connection->rollBack();
+ }
+
+ public function testForUpdateOrdinary(): void
+ {
+ $platform = $this->connection->getDatabasePlatform();
+
+ if ($platform instanceof SQLitePlatform) {
+ self::markTestSkipped('Skipping on SQLite');
+ }
+
+ $qb1 = $this->connection->createQueryBuilder();
+ $qb1->select('id')
+ ->from('for_update')
+ ->forUpdate();
+
+ self::assertEquals([1, 2], $qb1->fetchFirstColumn());
+ }
+
+ public function testForUpdateSkipLockedWhenSupported(): void
+ {
+ if (! $this->platformSupportsSkipLocked()) {
+ self::markTestSkipped('The database platform does not support SKIP LOCKED.');
+ }
+
+ $qb1 = $this->connection->createQueryBuilder();
+ $qb1->select('id')
+ ->from('for_update')
+ ->where('id = 1')
+ ->forUpdate();
+
+ $this->connection->beginTransaction();
+
+ self::assertEquals([1], $qb1->fetchFirstColumn());
+
+ $params = TestUtil::getConnectionParams();
+
+ if (TestUtil::isDriverOneOf('oci8')) {
+ $params['driverOptions']['exclusive'] = true;
+ }
+
+ $connection2 = DriverManager::getConnection($params);
+
+ $qb2 = $connection2->createQueryBuilder();
+ $qb2->select('id')
+ ->from('for_update')
+ ->orderBy('id')
+ ->forUpdate(ConflictResolutionMode::SKIP_LOCKED);
+
+ self::assertEquals([2], $qb2->fetchFirstColumn());
+ }
+
+ public function testForUpdateSkipLockedWhenNotSupported(): void
+ {
+ if ($this->platformSupportsSkipLocked()) {
+ self::markTestSkipped('The database platform supports SKIP LOCKED.');
+ }
+
+ $qb = $this->connection->createQueryBuilder();
+ $qb->select('id')
+ ->from('for_update')
+ ->forUpdate(ConflictResolutionMode::SKIP_LOCKED);
+
+ self::expectException(Exception::class);
+ $qb->executeQuery();
+ }
+
+ private function platformSupportsSkipLocked(): bool
+ {
+ $platform = $this->connection->getDatabasePlatform();
+
+ if ($platform instanceof DB2Platform) {
+ return false;
+ }
+
+ if ($platform instanceof MySQLPlatform) {
+ return $platform instanceof MySQL80Platform;
+ }
+
+ if ($platform instanceof MariaDBPlatform) {
+ return $platform instanceof MariaDB1060Platform;
+ }
+
+ return ! $platform instanceof SQLitePlatform;
+ }
+}
diff --git a/tests/Functional/Schema/MySQLSchemaManagerTest.php b/tests/Functional/Schema/MySQLSchemaManagerTest.php
index a167407aecd..2b17b6a25be 100644
--- a/tests/Functional/Schema/MySQLSchemaManagerTest.php
+++ b/tests/Functional/Schema/MySQLSchemaManagerTest.php
@@ -15,6 +15,7 @@
use Doctrine\DBAL\Tests\Functional\Schema\MySQL\PointType;
use Doctrine\DBAL\Tests\TestUtil;
use Doctrine\DBAL\Types\BlobType;
+use Doctrine\DBAL\Types\JsonType;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
@@ -387,6 +388,17 @@ public function testListFloatTypeColumns(): void
self::assertTrue($columns['col_unsigned']->getUnsigned());
}
+ public function testJsonColumnType(): void
+ {
+ $table = new Table('test_mysql_json');
+ $table->addColumn('col_json', Types::JSON);
+ $this->dropAndCreateTable($table);
+
+ $columns = $this->schemaManager->listTableColumns('test_mysql_json');
+
+ self::assertInstanceOf(JsonType::class, $columns['col_json']->getType());
+ }
+
public function testColumnDefaultCurrentTimestamp(): void
{
$platform = $this->connection->getDatabasePlatform();
diff --git a/tests/Query/QueryBuilderTest.php b/tests/Query/QueryBuilderTest.php
index 5e9f562259a..8e287b9ba45 100644
--- a/tests/Query/QueryBuilderTest.php
+++ b/tests/Query/QueryBuilderTest.php
@@ -8,10 +8,12 @@
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Query\QueryException;
use Doctrine\DBAL\Result;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
use Doctrine\DBAL\Types\Types;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -30,9 +32,15 @@ protected function setUp(): void
$expressionBuilder = new ExpressionBuilder($this->conn);
- $this->conn->expects(self::any())
- ->method('createExpressionBuilder')
- ->willReturn($expressionBuilder);
+ $this->conn->method('createExpressionBuilder')
+ ->willReturn($expressionBuilder);
+
+ $platform = $this->createMock(AbstractPlatform::class);
+ $platform->method('createSelectSQLBuilder')
+ ->willReturn(new DefaultSelectSQLBuilder($platform, null, null));
+
+ $this->conn->method('getDatabasePlatform')
+ ->willReturn($platform);
}
public function testSimpleSelectWithoutFrom(): void