From fb8caecbb0a51d54759aa0cc17b8ff31b8f46c4e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 18 Nov 2025 08:38:56 +0100 Subject: [PATCH 1/5] fix(db): Fix JSON handling in WHERE statements for postgres Signed-off-by: Joas Schilling --- .../Version33000Date20251106131209.php | 2 +- .../PgSqlExpressionBuilder.php | 14 ++++++++ .../QueryBuilder/ExpressionBuilderDBTest.php | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/core/Migrations/Version33000Date20251106131209.php b/core/Migrations/Version33000Date20251106131209.php index 488477694f4..b8f0a92736e 100644 --- a/core/Migrations/Version33000Date20251106131209.php +++ b/core/Migrations/Version33000Date20251106131209.php @@ -26,7 +26,7 @@ class Version33000Date20251106131209 extends SimpleMigrationStep { $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))); + ->andWhere($qb->expr()->eq('attributes', $qb->createNamedParameter('[["permissions","download",null]]'), IQueryBuilder::PARAM_JSON)); $qb->executeStatement(); } } diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php index 53a566a7eb6..f30fccadb1f 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php @@ -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 */ diff --git a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php b/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php index 112bfe2ca16..d4dc530a963 100644 --- a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php +++ b/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php @@ -141,6 +141,41 @@ class ExpressionBuilderDBTest extends TestCase { self::assertEquals('myvalue', $entries[0]['configvalue']); } + public function testJson(): void { + $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"]]')) + ->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"]]', $entries[0]['attributes']); + } + public function testDateTimeEquals(): void { $dateTime = new \DateTime('2023-01-01'); $insert = $this->connection->getQueryBuilder(); From 4676b12a321c7ccaca4015048681839bb94f06ff Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 18 Nov 2025 10:10:44 +0100 Subject: [PATCH 2/5] fix(db): Fix comparing JSON data in MySQL and MariaDB Signed-off-by: Joas Schilling --- core/Migrations/Version33000Date20251106131209.php | 11 +++++++++-- .../ExpressionBuilder/MySqlExpressionBuilder.php | 2 ++ tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php | 10 +++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/core/Migrations/Version33000Date20251106131209.php b/core/Migrations/Version33000Date20251106131209.php index b8f0a92736e..9fab7d6d3c0 100644 --- a/core/Migrations/Version33000Date20251106131209.php +++ b/core/Migrations/Version33000Date20251106131209.php @@ -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_JSON)); + ->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(); } } diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php index 7216fd8807b..0227c3154e3 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php @@ -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); } diff --git a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php b/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php index d4dc530a963..74fa0f23032 100644 --- a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php +++ b/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php @@ -160,8 +160,12 @@ class ExpressionBuilderDBTest extends TestCase { $query = $this->connection->getQueryBuilder(); $query->update('share') - ->set('attributes', $query->createNamedParameter('[["permissions","after"]]')) - ->where($query->expr()->eq('attributes', $query->createNamedParameter('[["permissions","before"]]'), IQueryBuilder::PARAM_JSON)); + ->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(); @@ -173,7 +177,7 @@ class ExpressionBuilderDBTest extends TestCase { $entries = $result->fetchAll(); $result->closeCursor(); self::assertCount(1, $entries); - self::assertEquals('[["permissions","after"]]', $entries[0]['attributes']); + self::assertEquals([['permissions','after']], json_decode($entries[0]['attributes'], true)); } public function testDateTimeEquals(): void { From 60bfab24216da489f90f9bed61d3b8d12adff324 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 18 Nov 2025 10:19:04 +0100 Subject: [PATCH 3/5] fix(db): Fix Oracle JSON handling Signed-off-by: Joas Schilling --- .../OCIExpressionBuilder.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php index 542e8d62ede..20308b24550 100644 --- a/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php @@ -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 */ From 7c870a8f67311f3b5c78732dbf3509d5c99537f4 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 18 Nov 2025 10:56:50 +0100 Subject: [PATCH 4/5] fix(db)!: Deprecate JSON fields due to problems with querying and selecting Signed-off-by: Joas Schilling --- build/psalm-baseline.xml | 21 ++++++++++++++++++++ lib/public/DB/QueryBuilder/IQueryBuilder.php | 2 ++ lib/public/DB/Types.php | 2 ++ 3 files changed, 25 insertions(+) diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 71d9fe016a4..369b1eebd28 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2073,6 +2073,11 @@ + + + + + @@ -2082,6 +2087,11 @@ + + + + + @@ -3280,6 +3290,17 @@ request->server]]> + + + + + + + + + + + diff --git a/lib/public/DB/QueryBuilder/IQueryBuilder.php b/lib/public/DB/QueryBuilder/IQueryBuilder.php index 58f274702bb..b1c483522ee 100644 --- a/lib/public/DB/QueryBuilder/IQueryBuilder.php +++ b/lib/public/DB/QueryBuilder/IQueryBuilder.php @@ -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'; diff --git a/lib/public/DB/Types.php b/lib/public/DB/Types.php index 969ec5e6611..f6221d98a89 100644 --- a/lib/public/DB/Types.php +++ b/lib/public/DB/Types.php @@ -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'; } From 75a8b9aff5a079574aa3c60ee88ef3b6923ddd2d Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 18 Nov 2025 16:10:12 +0100 Subject: [PATCH 5/5] fix(db): Skip test on Oracle 11g Signed-off-by: Joas Schilling --- tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php b/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php index 74fa0f23032..63f3ed4ab10 100644 --- a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php +++ b/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php @@ -142,6 +142,16 @@ class ExpressionBuilderDBTest extends TestCase { } 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')