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/core/Migrations/Version33000Date20251106131209.php b/core/Migrations/Version33000Date20251106131209.php index 488477694f4..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_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(); } } 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/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 */ 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/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'; } diff --git a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php b/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php index 112bfe2ca16..63f3ed4ab10 100644 --- a/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php +++ b/tests/lib/DB/QueryBuilder/ExpressionBuilderDBTest.php @@ -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();