Skip to content

Add JsonPathExpression to support json path clauses#19463

Open
othercorey wants to merge 1 commit into
cakephp:5.nextfrom
othercorey:json-query
Open

Add JsonPathExpression to support json path clauses#19463
othercorey wants to merge 1 commit into
cakephp:5.nextfrom
othercorey:json-query

Conversation

@othercorey
Copy link
Copy Markdown
Contributor

Add support for the full json path expression clauses for JSON_VALUE() and JSON_EXISTS() functions. These can be extended to support JSON_QUERY and possibly JSON_TABLE in the future.

https://www.postgresql.org/docs/current/functions-json.html#SQLJSON-QUERY-FUNCTIONS

@dereuromark dereuromark added this to the 5.4.0 milestone May 24, 2026
Comment thread src/Database/Expression/JsonPathExpression.php Outdated
* @param array $types Associative array of types for the expressions.
* @return $this
*/
public function passing(array $passing, array $types = []): static
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should silence this rule for 5.4 so that we can at least add static for new methods.

/**
* Sets the ON EMPTY clause.
*
* @param string $behavior The behavior on empty (NULL, ERROR, or DEFAULT).
Copy link
Copy Markdown
Member

@ADmad ADmad May 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param string $behavior The behavior on empty (NULL, ERROR, or DEFAULT).
* @param self::BEHAVIOR_* $behavior The behavior on empty (NULL, ERROR, or DEFAULT).

phpstan supports this, most likely other tools too

https://phpstan.org/writing-php-code/phpdoc-types#literals-and-constants

@othercorey
Copy link
Copy Markdown
Contributor Author

@ADmad I should extend the query tests with the full format. The jsonValue() test is in UpateQueryTest, but it's not specific to update. Should I move it to QueryTest or somewhere else?

@ADmad
Copy link
Copy Markdown
Member

ADmad commented May 24, 2026

I should extend the query tests with the full format. The jsonValue() test is in UpateQueryTest, but it's not specific to update. Should I move it to QueryTest or somewhere else?

QueryTest would work I guess.

Copy link
Copy Markdown
Member

@markstory markstory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good to me.

Comment thread src/Database/FunctionsBuilder.php Outdated
}

/**
* Returns a FunctionExpression representing the Json Exists
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these methods include any hints about the limitations in database support?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Most clauses are supported by engines that support the sql standard function.

I'm not sure if we should translate this to non-standard similar functions in other db engines -- maybe yes, but we'd have to deal with ignoring clauses that have a value set.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most clauses are supported by engines that support the sql standard function.

Ok, I took a quick look at MySQL and Postgres docs, and I was surprised to see so much alignment in the implementations.

@LordSimal
Copy link
Copy Markdown
Contributor

There are a few CS & Stan issues, other than that this looks good to merge.

@othercorey
Copy link
Copy Markdown
Contributor Author

othercorey commented May 29, 2026

There are a few CS & Stan issues, other than that this looks good to merge.

I have an update that cleans up some types, but waiting until I have time to add a couple tests to ensure the engines parse ok.

@othercorey othercorey force-pushed the json-query branch 2 times, most recently from 699618e to e155348 Compare May 30, 2026 10:19
Comment thread .github/workflows/ci.yml
if: matrix.db-type == 'pgsql'
- name: Setup PostgreSQL 12 with PostGIS
if: matrix.db-type == 'pgsql' && matrix.php-version = '8.2'
run: docker run --rm --name=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=cakephp -p 5432:5432 -d postgis/postgis:12-3.5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why specifically postgres 12 here? According to https://endoflife.date/postgresql 12 is EOL'd since over a year.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12.0 adds the JSONB_PATH_EXISTS() function. Since we were already transforming JSON_VALUE to JSONB_PATH_QUERY, thought it would be expected.

17.0 adds the sql standard functions we should be using.

@othercorey
Copy link
Copy Markdown
Contributor Author

  • Updated JsonPathExpression::passing() to auto-set bind types
  • Added expression transforms for JSON_EXISTS
  • Updated ci to run PostgreSQL 12 since it adds driver-specific features
  • Fixed DEFAULT x behavior of ON EMPTY and ON ERROR clauses requiring literal values
  • Added FunctionsQueryTest to test driver-specific function expressions similar to AggregatesQueryTest

[null, 'literal'],
'',
parentheses: false,
);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cake makes modifying function expressions into something else a nightmare. I'm not sure if this is worth it or not, but it's one way to support it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is worth the effort. SQLite is a fantastic database when it is a suitable choice.

@othercorey othercorey force-pushed the json-query branch 5 times, most recently from 5f464fd to 71d2bbc Compare May 31, 2026 10:06
@othercorey
Copy link
Copy Markdown
Contributor Author

I don't know what to do about the rector failure. It wants to change the code layout while there's only one switch case.

@LordSimal
Copy link
Copy Markdown
Contributor

I don't know what to do about the rector failure. It wants to change the code layout while there's only one switch case.

Well because its the SingularSwitchToIfRector rule :D

A switch-case with only 1 case is basically just a if.

@othercorey
Copy link
Copy Markdown
Contributor Author

A switch-case with only 1 case is basically just a if.

I understand - it just sets a lousy code example that's different from the other drivers.

@LordSimal
Copy link
Copy Markdown
Contributor

I appreciate trying to keep it as consistent as possible. Alternatively we can also ignore that rector rule in our config, but I don't think that this is a big difference.

* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to transform.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless this is an overridden function we don't need the underscore prefix.

@othercorey
Copy link
Copy Markdown
Contributor Author

@ADmad How much do you dislike the parentheses parameter in QueryExpression? I don't know how else to concat to add another parameter or build a different sql expression.

protected function _expressionTranslators(): array
{
return [
FunctionExpression::class => '_transformFunctionExpression',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FunctionExpression::class => '_transformFunctionExpression',
FunctionExpression::class => 'transformFunctionExpression',

* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to transform.
* @return void
*/
protected function _transformFunctionExpression(FunctionExpression $expression): void
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
protected function _transformFunctionExpression(FunctionExpression $expression): void
protected function transformFunctionExpression(FunctionExpression $expression): void

->setName('JSON_CONTAINS_PATH')
->iterateParts(function ($p, $key) {
if ($key === 1) {
return new QueryExpression(["'all'", $p], ['literal'], ',', parentheses: false);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a separate class for a ExpressionList style container that doesn't emit parentheses? I could see this being useful in other parts of the database drivers as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful. Hoping one of you makes the call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LordSimal @ADmad Do you have any opinions on this so I can work on final update?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no strong opinion about this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@othercorey Adding a new class as markstory suggested would be cleaner instead of adding more switches to QueryExpression.

[null, 'literal'],
'',
parentheses: false,
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is worth the effort. SQLite is a fantastic database when it is a suitable choice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants