From b27b0c7bff345f5bd8862df8a16974acdd78f27c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Sep 2026 22:13:08 +0400 Subject: [PATCH 1/2] test(Postgres): cover apostrophes in JSON path segments The Postgres JSON compilers quote path segments without escaping apostrophes, so such keys break the generated SQL. The tests fail until the compiler is fixed. Assisted-By: Claude Opus 5.5 --- .../Postgres/Query/JsonPathQuotingTest.php | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/Database/Functional/Driver/Postgres/Query/JsonPathQuotingTest.php diff --git a/tests/Database/Functional/Driver/Postgres/Query/JsonPathQuotingTest.php b/tests/Database/Functional/Driver/Postgres/Query/JsonPathQuotingTest.php new file mode 100644 index 00000000..78e96a85 --- /dev/null +++ b/tests/Database/Functional/Driver/Postgres/Query/JsonPathQuotingTest.php @@ -0,0 +1,119 @@ + [ + static fn(SelectQuery|UpdateQuery|DeleteQuery $q) => $q + ->whereJson("data->classification' IS NOT NULL OR 'x", 'x'), + ]; + yield 'whereJson nested path' => [ + static fn(SelectQuery|UpdateQuery|DeleteQuery $q) => $q + ->whereJson("data->meta' IS NOT NULL OR jsonb_build_object('k', 'x') #> '{}->k", 'x'), + ]; + yield 'whereJsonContains' => [ + static fn(SelectQuery|UpdateQuery|DeleteQuery $q) => $q + ->whereJsonContains("data->tags')::jsonb IS NOT NULL OR ('1", 1), + ]; + yield 'whereJsonDoesntContain' => [ + static fn(SelectQuery|UpdateQuery|DeleteQuery $q) => $q + ->whereJsonDoesntContain("data->tags')::jsonb IS NULL OR ('1", 1), + ]; + yield 'whereJsonContainsKey' => [ + static fn(SelectQuery|UpdateQuery|DeleteQuery $q) => $q + ->whereJsonContainsKey("data->x', false) OR coalesce(true, 't"), + ]; + yield 'whereJsonDoesntContainKey' => [ + static fn(SelectQuery|UpdateQuery|DeleteQuery $q) => $q + ->whereJsonDoesntContainKey("data->classification', false) OR coalesce(true, 't"), + ]; + yield 'whereJsonLength' => [ + static fn(SelectQuery|UpdateQuery|DeleteQuery $q) => $q + ->whereJsonLength("data->tags')::jsonb) >= 0 OR jsonb_array_length(jsonb_build_array('x", 1), + ]; + } + + /** + * @dataProvider provideJsonConditions + */ + public function testSelectDoesNotEscapeTenantScope(\Closure $condition): void + { + $select = $this->database->select('tenant_id')->from('documents')->where('tenant_id', 10); + $condition($select); + + $this->assertNotContains(20, \array_column($select->fetchAll(), 'tenant_id')); + } + + /** + * @dataProvider provideJsonConditions + */ + public function testUpdateDoesNotEscapeTenantScope(\Closure $condition): void + { + $update = $this->database->update('documents', ['data' => '{}'])->where('tenant_id', 10); + $condition($update); + $update->run(); + + $data = $this->database->select('data')->from('documents')->where('tenant_id', 20)->run()->fetchColumn(); + $this->assertStringContainsString('confidential', $data); + } + + /** + * @dataProvider provideJsonConditions + */ + public function testDeleteDoesNotEscapeTenantScope(\Closure $condition): void + { + $delete = $this->database->delete('documents')->where('tenant_id', 10); + $condition($delete); + $delete->run(); + + $this->assertSame(1, $this->database->select()->from('documents')->where('tenant_id', 20)->count()); + } + + public function testWhereJsonWithApostropheInKey(): void + { + $select = $this->database->select('tenant_id')->from('documents')->whereJson("data->it's", 'yes'); + + $this->assertSame([10], \array_column($select->fetchAll(), 'tenant_id')); + } + + public function testWhereJsonContainsKeyWithApostropheInKey(): void + { + $select = $this->database->select('tenant_id')->from('documents')->whereJsonContainsKey("data->it's"); + + $this->assertSame([10], \array_column($select->fetchAll(), 'tenant_id')); + } + + public function setUp(): void + { + parent::setUp(); + + $schema = $this->schema('documents'); + $schema->primary('id'); + $schema->integer('tenant_id'); + $schema->json('data'); + $schema->save(); + + $this->database->insert('documents')->columns(['tenant_id', 'data'])->values([ + [10, '{"classification": "public", "tags": ["a"], "meta": {"k": "v"}, "it\'s": "yes"}'], + [20, '{"classification": "confidential", "tags": ["b", "c"], "meta": {"k": "w"}}'], + ])->run(); + } +} From 56ce86f08e6b7bd7f74c872f22340846819b597a Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Sep 2026 23:22:37 +0400 Subject: [PATCH 2/2] fix(Postgres): escape apostrophes in JSON path segments Assisted-By: Claude Opus 5.5 --- src/Driver/Postgres/Injection/PostgresJsonExpression.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Driver/Postgres/Injection/PostgresJsonExpression.php b/src/Driver/Postgres/Injection/PostgresJsonExpression.php index 717aca04..1e3ed066 100644 --- a/src/Driver/Postgres/Injection/PostgresJsonExpression.php +++ b/src/Driver/Postgres/Injection/PostgresJsonExpression.php @@ -88,7 +88,7 @@ private function getPathArray(string $statement, string $quote = "'"): array foreach ($parsedAttributes as $attribute) { $result[] = \filter_var($attribute, FILTER_VALIDATE_INT) !== false ? $attribute - : $quote . $attribute . $quote; + : $quote . \str_replace($quote, $quote . $quote, $attribute) . $quote; } }