Skip to content

Commit 01b83e6

Browse files
committed
fix: use $eq_pointer marker + collapse at build time (CodeRabbit review feedback)
Instead of directly assigning to $this->where (which drops same-key constraints), store ParseObject values via addCondition under a special $eq_pointer key, then collapse to raw pointer at query build time when it is the only condition for that key. This preserves the per-key operator map contract while still supporting array value queries.
1 parent c6d9e3c commit 01b83e6

3 files changed

Lines changed: 230 additions & 5 deletions

File tree

src/Parse/ParseQuery.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ public function get($objectId, $useMasterKey = false)
141141
public function equalTo($key, $value)
142142
{
143143
if ($value instanceof ParseObject) {
144-
// Pointer values (e.g. users => ParseUser) should match directly,
145-
// not wrapped in $eq, to support queries on array values.
146-
$this->where[$key] = ParseClient::_encode($value, true);
144+
// Mark pointer values with a special marker so we can collapse
145+
// them to raw pointers at query build time (supports array queries)
146+
$this->addCondition($key, '$eq_pointer', $value);
147147
} else {
148148
$this->addCondition($key, '$eq', $value);
149149
}
@@ -507,7 +507,14 @@ public function _getOptions()
507507
{
508508
$opts = [];
509509
if (!empty($this->where)) {
510-
$opts['where'] = $this->where;
510+
$where = $this->where;
511+
// Collapse $eq_pointer to raw pointer for array value queries
512+
foreach ($where as $key => $conditions) {
513+
if (is_array($conditions) && array_key_exists('$eq_pointer', $conditions) && count($conditions) === 1) {
514+
$where[$key] = $conditions['$eq_pointer'];
515+
}
516+
}
517+
$opts['where'] = $where;
511518
}
512519
if (count($this->includes)) {
513520
$opts['include'] = implode(',', $this->includes);
@@ -639,7 +646,13 @@ public function distinct($key)
639646
}
640647
$opts = [];
641648
if (!empty($this->where)) {
642-
$opts['where'] = $this->where;
649+
$where = $this->where;
650+
foreach ($where as $k => $conditions) {
651+
if (is_array($conditions) && array_key_exists('$eq_pointer', $conditions) && count($conditions) === 1) {
652+
$where[$k] = $conditions['$eq_pointer'];
653+
}
654+
}
655+
$opts['where'] = $where;
643656
}
644657
$opts['distinct'] = $key;
645658
$queryString = $this->buildQueryString($opts);

verify_equalto.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
spl_autoload_register(function ($class) {
3+
$prefix = 'Parse\\';
4+
$base = '/root/parse-php-sdk/src/Parse/';
5+
if (strpos($class, $prefix) !== 0) return;
6+
$relative = substr($class, strlen($prefix));
7+
$file = $base . str_replace('\\', '/', $relative) . '.php';
8+
if (file_exists($file)) require_once $file;
9+
});
10+
11+
use Parse\ParseClient;
12+
use Parse\ParseObject;
13+
use Parse\ParseQuery;
14+
15+
ParseClient::initialize('appId', 'restKey', 'https://api.example.com');
16+
17+
$passed = 0;
18+
$failed = 0;
19+
20+
function check($name, $condition) {
21+
global $passed, $failed;
22+
if ($condition) { echo " PASS: $name\n"; $passed++; }
23+
else { echo " FAIL: $name\n"; $failed++; }
24+
}
25+
26+
echo "=== equalTo Fix Verification ===\n\n";
27+
28+
// Test 1: ParseObject value should use direct pointer match at query build time
29+
echo "Test 1: ParseObject uses direct pointer match in built query\n";
30+
$user = ParseObject::create('_User', 'user123');
31+
$query = new ParseQuery('ParseRole');
32+
$query->equalTo('users', $user);
33+
$opts = $query->_getOptions();
34+
$encoded = json_encode($opts['where'] ?? []);
35+
echo " Built query where = $encoded\n";
36+
check('should NOT have $eq in output', strpos($encoded, '$eq') === false);
37+
check('should NOT have $eq_pointer in output', strpos($encoded, '$eq_pointer') === false);
38+
check('should have __type=Pointer in output', strpos($encoded, '"__type":"Pointer"') !== false);
39+
check('should have className=_User', strpos($encoded, '"className":"_User"') !== false);
40+
check('should have objectId=user123', strpos($encoded, '"objectId":"user123"') !== false);
41+
42+
// Test 2: String value still uses $eq
43+
echo "\nTest 2: String uses \$eq\n";
44+
$query2 = new ParseQuery('TestClass');
45+
$query2->equalTo('foo', 'bar');
46+
$opts2 = $query2->_getOptions();
47+
$encoded2 = json_encode($opts2['where'] ?? []);
48+
echo " where = $encoded2\n";
49+
check('foo should have $eq', strpos($encoded2, '$eq') !== false);
50+
check('foo $eq should be bar', strpos($encoded2, '"bar"') !== false);
51+
52+
// Test 3: Number value still uses $eq
53+
echo "\nTest 3: Number uses \$eq\n";
54+
$query3 = new ParseQuery('TestClass');
55+
$query3->equalTo('number', 17);
56+
$opts3 = $query3->_getOptions();
57+
$encoded3 = json_encode($opts3['where'] ?? []);
58+
echo " where = $encoded3\n";
59+
check('number should have $eq', strpos($encoded3, '$eq') !== false);
60+
61+
// Test 4: Chained constraints on same key still work
62+
echo "\nTest 4: Chained constraints on same key\n";
63+
$query4 = new ParseQuery('TestClass');
64+
$query4->equalTo('foo', 'bar');
65+
$query4->greaterThan('foo', 10);
66+
$opts4 = $query4->_getOptions();
67+
$encoded4 = json_encode($opts4['where'] ?? []);
68+
echo " where = $encoded4\n";
69+
check('foo should have both $eq and $gt', strpos($encoded4, '$eq') !== false && strpos($encoded4, '$gt') !== false);
70+
71+
// Test 5: ParseObject + another constraint on same key (order-dependent case)
72+
echo "\nTest 5: ParseObject with other constraints\n";
73+
$query5 = new ParseQuery('TestClass');
74+
$query5->greaterThan('count', 5);
75+
$query5->equalTo('users', $user);
76+
$opts5 = $query5->_getOptions();
77+
$encoded5 = json_encode($opts5['where'] ?? []);
78+
echo " where = $encoded5\n";
79+
check('count should have $gt', strpos($encoded5, '$gt') !== false);
80+
check('users should be direct pointer', strpos($encoded5, '"__type":"Pointer"') !== false);
81+
82+
echo "\n=== Results: $passed passed, $failed failed ===\n";
83+
exit($failed > 0 ? 1 : 0);

verify_fix.php

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Standalone verification for equalTo fix
3+
* Tests that ParseObject values use direct pointer matching (not $eq)
4+
* and non-Object values still use $eq
5+
*/
6+
7+
require_once __DIR__ . '/src/Parse/ParseClient.php';
8+
require_once __DIR__ . '/src/Parse/ParseObject.php';
9+
require_once __DIR__ . '/src/Parse/ParseQuery.php';
10+
require_once __DIR__ . '/src/Parse/ParseUser.php';
11+
require_once __DIR__ . '/src/Parse/ParseRole.php';
12+
require_once __DIR__ . '/src/Parse/Internal/Encodable.php';
13+
14+
use Parse\ParseClient;
15+
use Parse\ParseObject;
16+
use Parse\ParseQuery;
17+
use Parse\ParseUser;
18+
use Parse\ParseRole;
19+
20+
// Initialize Parse client
21+
ParseClient::initialize('appId', 'restKey', 'https://api.example.com');
22+
23+
$passed = 0;
24+
$failed = 0;
25+
26+
function assert_test($name, $condition) {
27+
global $passed, $failed;
28+
if ($condition) {
29+
echo " ✓ $name\n";
30+
$passed++;
31+
} else {
32+
echo " ✗ FAIL: $name\n";
33+
$failed++;
34+
}
35+
}
36+
37+
echo "=== equalTo Fix Verification ===\n\n";
38+
39+
// Test 1: ParseObject value should use direct pointer match (NOT $eq)
40+
echo "Test 1: ParseObject value uses direct pointer match\n";
41+
$user = ParseObject::create('_User', 'user123');
42+
$query = ParseQuery::getQuery('ParseRole');
43+
$query->equalTo('users', $user);
44+
$where = $query->getWhere();
45+
46+
assert_test(
47+
"where['users'] should NOT have \$eq key",
48+
!isset($where['users']['$eq'])
49+
);
50+
assert_test(
51+
"where['users'] should have __type = Pointer",
52+
isset($where['users']['__type']) && $where['users']['__type'] === 'Pointer'
53+
);
54+
assert_test(
55+
"where['users'] should have className = _User",
56+
isset($where['users']['className']) && $where['users']['className'] === '_User'
57+
);
58+
assert_test(
59+
"where['users'] should have objectId = user123",
60+
isset($where['users']['objectId']) && $where['users']['objectId'] === 'user123'
61+
);
62+
63+
// Test 2: String value should still use $eq
64+
echo "\nTest 2: String value uses \$eq\n";
65+
$query2 = ParseQuery::getQuery('TestClass');
66+
$query2->equalTo('foo', 'bar');
67+
$where2 = $query2->getWhere();
68+
69+
assert_test(
70+
"where['foo'] should have \$eq key",
71+
isset($where2['foo']['$eq'])
72+
);
73+
assert_test(
74+
"where['foo']['\$eq'] should be 'bar'",
75+
isset($where2['foo']['$eq']) && $where2['foo']['$eq'] === 'bar'
76+
);
77+
78+
// Test 3: Number value should still use $eq
79+
echo "\nTest 3: Number value uses \$eq\n";
80+
$query3 = ParseQuery::getQuery('TestClass');
81+
$query3->equalTo('number', 17);
82+
$where3 = $query3->getWhere();
83+
84+
assert_test(
85+
"where['number'] should have \$eq key",
86+
isset($where3['number']['$eq'])
87+
);
88+
assert_test(
89+
"where['number']['\$eq'] should be 17",
90+
isset($where3['number']['$eq']) && $where3['number']['$eq'] === 17
91+
);
92+
93+
// Test 4: null value should still use $eq
94+
echo "\nTest 4: null value uses \$eq\n";
95+
$query4 = ParseQuery::getQuery('TestClass');
96+
$query4->equalTo('num', null);
97+
$where4 = $query4->getWhere();
98+
99+
assert_test(
100+
"where['num'] should have \$eq key",
101+
isset($where4['num']['$eq'])
102+
);
103+
assert_test(
104+
"where['num']['\$eq'] should be null",
105+
isset($where4['num']['$eq']) && $where4['num']['$eq'] === null
106+
);
107+
108+
// Test 5: Array value should still use $eq
109+
echo "\nTest 5: Array value uses \$eq\n";
110+
$query5 = ParseQuery::getQuery('TestClass');
111+
$query5->equalTo('tags', ['foo', 'bar']);
112+
$where5 = $query5->getWhere();
113+
114+
assert_test(
115+
"where['tags'] should have \$eq key",
116+
isset($where5['tags']['$eq'])
117+
);
118+
119+
echo "\n=== Results ===\n";
120+
echo "Passed: $passed\n";
121+
echo "Failed: $failed\n";
122+
123+
if ($failed > 0) {
124+
echo "\n❌ SOME TESTS FAILED\n";
125+
exit(1);
126+
} else {
127+
echo "\n✅ ALL TESTS PASSED\n";
128+
exit(0);
129+
}

0 commit comments

Comments
 (0)