Merge pull request #56504 from nextcloud/bugfix/56497/fix-json-with-postgres

fix(db): Fix JSON handling in WHERE statements for postgres
pull/55511/merge
Joas Schilling 2025-11-18 17:42:39 +07:00 committed by GitHub
commit 93c7111708
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 125 additions and 2 deletions

@ -2073,6 +2073,11 @@
<code><![CDATA[new View('/' . $user)]]></code>
</InternalMethod>
</file>
<file src="apps/files_versions/lib/Db/VersionEntity.php">
<DeprecatedConstant>
<code><![CDATA[Types::JSON]]></code>
</DeprecatedConstant>
</file>
<file src="apps/files_versions/lib/Listener/FileEventsListener.php">
<InternalClass>
<code><![CDATA[new View($user . '/files')]]></code>
@ -2082,6 +2087,11 @@
<code><![CDATA[new View($user . '/files')]]></code>
</InternalMethod>
</file>
<file src="apps/files_versions/lib/Migration/Version1020Date20221114144058.php">
<DeprecatedConstant>
<code><![CDATA[Types::JSON]]></code>
</DeprecatedConstant>
</file>
<file src="apps/files_versions/lib/Sabre/RestoreFolder.php">
<InvalidNullableReturnType>
<code><![CDATA[getChild]]></code>
@ -3280,6 +3290,17 @@
<code><![CDATA[$this->request->server]]></code>
</NoInterfaceProperties>
</file>
<file src="core/Migrations/Version25000Date20220515204012.php">
<DeprecatedConstant>
<code><![CDATA[Types::JSON]]></code>
</DeprecatedConstant>
</file>
<file src="core/Migrations/Version33000Date20251106131209.php">
<DeprecatedConstant>
<code><![CDATA[IQueryBuilder::PARAM_JSON]]></code>
<code><![CDATA[IQueryBuilder::PARAM_JSON]]></code>
</DeprecatedConstant>
</file>
<file src="core/Service/LoginFlowV2Service.php">
<DeprecatedClass>
<code><![CDATA[IToken::DO_NOT_REMEMBER]]></code>

@ -21,12 +21,19 @@ class Version33000Date20251106131209 extends SimpleMigrationStep {
private readonly IDBConnection $connection,
) {
}
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
$qb = $this->connection->getQueryBuilder();
$qb->update('share')
->set('attributes', $qb->createNamedParameter('[["permissions","download",true]]'))
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('attributes', $qb->createNamedParameter('[["permissions","download",null]]', IQueryBuilder::PARAM_STR)));
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE, IQueryBuilder::PARAM_INT)));
if ($this->connection->getDatabaseProvider(true) === IDBConnection::PLATFORM_MYSQL) {
$qb->andWhere($qb->expr()->eq('attributes', $qb->createFunction("JSON_ARRAY(JSON_ARRAY('permissions','download',null))"), IQueryBuilder::PARAM_JSON));
} else {
$qb->andWhere($qb->expr()->eq('attributes', $qb->createNamedParameter('[["permissions","download",null]]'), IQueryBuilder::PARAM_JSON));
}
$qb->executeStatement();
}
}

@ -44,6 +44,8 @@ class MySqlExpressionBuilder extends ExpressionBuilder {
switch ($type) {
case IQueryBuilder::PARAM_STR:
return new QueryFunction('CAST(' . $this->helper->quoteColumnName($column) . ' AS CHAR)');
case IQueryBuilder::PARAM_JSON:
return new QueryFunction('CAST(' . $this->helper->quoteColumnName($column) . ' AS JSON)');
default:
return parent::castColumn($column, $type);
}

@ -27,6 +27,32 @@ class OCIExpressionBuilder extends ExpressionBuilder {
return parent::prepareColumn($column, $type);
}
/**
* @inheritdoc
*/
public function eq($x, $y, $type = null): string {
if ($type === IQueryBuilder::PARAM_JSON) {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return (string)(new QueryFunction('JSON_EQUAL(' . $x . ',' . $y . ')'));
}
return parent::eq($x, $y, $type);
}
/**
* @inheritdoc
*/
public function neq($x, $y, $type = null): string {
if ($type === IQueryBuilder::PARAM_JSON) {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return (string)(new QueryFunction('NOT JSON_EQUAL(' . $x . ',' . $y . ')'));
}
return parent::neq($x, $y, $type);
}
/**
* @inheritdoc
*/

@ -8,6 +8,8 @@
namespace OC\DB\QueryBuilder\ExpressionBuilder;
use OC\DB\QueryBuilder\QueryFunction;
use OCP\DB\QueryBuilder\ILiteral;
use OCP\DB\QueryBuilder\IParameter;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\IQueryFunction;
@ -25,12 +27,24 @@ class PgSqlExpressionBuilder extends ExpressionBuilder {
case IQueryBuilder::PARAM_INT:
return new QueryFunction('CAST(' . $this->helper->quoteColumnName($column) . ' AS BIGINT)');
case IQueryBuilder::PARAM_STR:
case IQueryBuilder::PARAM_JSON:
return new QueryFunction('CAST(' . $this->helper->quoteColumnName($column) . ' AS TEXT)');
default:
return parent::castColumn($column, $type);
}
}
/**
* @inheritdoc
*/
protected function prepareColumn($column, $type) {
if ($type === IQueryBuilder::PARAM_JSON && !is_array($column) && !($column instanceof IParameter) && !($column instanceof ILiteral)) {
$column = $this->castColumn($column, $type);
}
return parent::prepareColumn($column, $type);
}
/**
* @inheritdoc
*/

@ -99,6 +99,8 @@ interface IQueryBuilder {
/**
* @since 24.0.0
* @deprecated 33.0.0 JSON fields can not properly be used in WHERE statements of Oracle and MySQL.
* It is recommended to use a simple STRING field and handle JSON within PHP
*/
public const PARAM_JSON = 'json';

@ -174,6 +174,8 @@ final class Types {
/**
* @var string
* @since 24.0.0
* @deprecated 33.0.0 JSON fields can not properly be used in WHERE statements of Oracle and MySQL.
* It is recommended to use a simple STRING field and handle JSON within PHP
*/
public const JSON = 'json';
}

@ -141,6 +141,55 @@ class ExpressionBuilderDBTest extends TestCase {
self::assertEquals('myvalue', $entries[0]['configvalue']);
}
public function testJson(): void {
if ($this->connection->getDatabaseProvider(true) === IDBConnection::PLATFORM_ORACLE) {
$result = $this->connection->executeQuery('SELECT VERSION FROM PRODUCT_COMPONENT_VERSION');
$version = $result->fetchOne();
$result->closeCursor();
if (str_starts_with($version, '11.')) {
$this->markTestSkipped('JSON is not supported on Oracle 11, skipping until deprecation was clarified: ' . $version);
}
}
$appId = $this->getUniqueID('testing');
$query = $this->connection->getQueryBuilder();
$query->insert('share')
->values([
'uid_owner' => $query->createNamedParameter('uid_owner'),
'item_type' => $query->createNamedParameter('item_type'),
'permissions' => $query->createNamedParameter(0),
'stime' => $query->createNamedParameter(0),
'accepted' => $query->createNamedParameter(0),
'mail_send' => $query->createNamedParameter(0),
'share_type' => $query->createNamedParameter(0),
'share_with' => $query->createNamedParameter($appId),
'attributes' => $query->createNamedParameter('[["permissions","before"]]'),
])
->executeStatement();
$query = $this->connection->getQueryBuilder();
$query->update('share')
->set('attributes', $query->createNamedParameter('[["permissions","after"]]'));
if ($this->connection->getDatabaseProvider(true) === IDBConnection::PLATFORM_MYSQL) {
$query->where($query->expr()->eq('attributes', $query->createFunction("JSON_ARRAY(JSON_ARRAY('permissions','before'))"), IQueryBuilder::PARAM_JSON));
} else {
$query->where($query->expr()->eq('attributes', $query->createNamedParameter('[["permissions","before"]]'), IQueryBuilder::PARAM_JSON));
}
$query->executeStatement();
$query = $this->connection->getQueryBuilder();
$query->select('attributes')
->from('share')
->where($query->expr()->eq('share_with', $query->createNamedParameter($appId)));
$result = $query->executeQuery();
$entries = $result->fetchAll();
$result->closeCursor();
self::assertCount(1, $entries);
self::assertEquals([['permissions','after']], json_decode($entries[0]['attributes'], true));
}
public function testDateTimeEquals(): void {
$dateTime = new \DateTime('2023-01-01');
$insert = $this->connection->getQueryBuilder();