Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions src/Parse/ParseQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,13 @@ public function get($objectId, $useMasterKey = false)
*/
public function equalTo($key, $value)
{
$this->addCondition($key, '$eq', $value);
if ($value instanceof ParseObject) {
// Mark pointer values with a special marker so we can collapse
// them to raw pointers at query build time (supports array queries)
$this->addCondition($key, '$eq_pointer', $value);
} else {
$this->addCondition($key, '$eq', $value);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return $this;
}
Expand Down Expand Up @@ -492,6 +498,40 @@ public function fullText($key, $value)
return $this;
}

/**
* Recursively normalize the where clause, collapsing $eq_pointer sentinel
* values to raw pointers (for ParseObject equality) or rewriting mixed
* same-key cases to $eq. Handles nested condition trees (or/and/nor queries).
*
* @param mixed $value
* @return mixed
*/
private function normalizeWhere($value)
{
if (!is_array($value)) {
return $value;
}

if (array_key_exists('$eq_pointer', $value)) {
$pointer = $value['$eq_pointer'];
unset($value['$eq_pointer']);

if (count($value) === 0) {
// Sole condition: collapse to raw pointer
return $pointer;
}

// Mixed same-key case: rewrite to $eq alongside other operators
$value['$eq'] = $pointer;
}

foreach ($value as $k => $v) {
$value[$k] = $this->normalizeWhere($v);
}

return $value;
}

/**
* Returns an associative array of the query constraints.
*
Expand All @@ -501,7 +541,7 @@ public function _getOptions()
{
$opts = [];
if (!empty($this->where)) {
$opts['where'] = $this->where;
$opts['where'] = $this->normalizeWhere($this->where);
}
if (count($this->includes)) {
$opts['include'] = implode(',', $this->includes);
Expand Down Expand Up @@ -633,7 +673,7 @@ public function distinct($key)
}
$opts = [];
if (!empty($this->where)) {
$opts['where'] = $this->where;
$opts['where'] = $this->normalizeWhere($this->where);
}
$opts['distinct'] = $key;
$queryString = $this->buildQueryString($opts);
Expand Down
84 changes: 84 additions & 0 deletions verify_equalto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
spl_autoload_register(function ($class) {
$prefix = 'Parse\\';
$base = __DIR__ . '/src/Parse/';
if (strpos($class, $prefix) !== 0) return;
$relative = substr($class, strlen($prefix));
$file = $base . str_replace('\\', '/', $relative) . '.php';
if (file_exists($file)) require_once $file;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

use Parse\ParseClient;
use Parse\ParseObject;
use Parse\ParseQuery;

ParseClient::initialize('appId', 'restKey', 'https://api.example.com');

$passed = 0;
$failed = 0;

function check($name, $condition) {
global $passed, $failed;
if ($condition) { echo " PASS: $name\n"; $passed++; }
else { echo " FAIL: $name\n"; $failed++; }
}

echo "=== equalTo Fix Verification ===\n\n";

// Test 1: ParseObject value should use direct pointer match at query build time
echo "Test 1: ParseObject uses direct pointer match in built query\n";
$user = ParseObject::create('_User', 'user123');
$query = new ParseQuery('ParseRole');
$query->equalTo('users', $user);
$opts = $query->_getOptions();
$encoded = json_encode($opts['where'] ?? []);
echo " Built query where = $encoded\n";
check('should NOT have $eq in output', strpos($encoded, '$eq') === false);
check('should NOT have $eq_pointer in output', strpos($encoded, '$eq_pointer') === false);
check('should have __type=Pointer in output', strpos($encoded, '"__type":"Pointer"') !== false);
check('should have className=_User', strpos($encoded, '"className":"_User"') !== false);
check('should have objectId=user123', strpos($encoded, '"objectId":"user123"') !== false);

// Test 2: String value still uses $eq
echo "\nTest 2: String uses \$eq\n";
$query2 = new ParseQuery('TestClass');
$query2->equalTo('foo', 'bar');
$opts2 = $query2->_getOptions();
$encoded2 = json_encode($opts2['where'] ?? []);
echo " where = $encoded2\n";
check('foo should have $eq', strpos($encoded2, '$eq') !== false);
check('foo $eq should be bar', strpos($encoded2, '"bar"') !== false);

// Test 3: Number value still uses $eq
echo "\nTest 3: Number uses \$eq\n";
$query3 = new ParseQuery('TestClass');
$query3->equalTo('number', 17);
$opts3 = $query3->_getOptions();
$encoded3 = json_encode($opts3['where'] ?? []);
echo " where = $encoded3\n";
check('number should have $eq', strpos($encoded3, '$eq') !== false);

// Test 4: Chained constraints on same key still work
echo "\nTest 4: Chained constraints on same key\n";
$query4 = new ParseQuery('TestClass');
$query4->equalTo('foo', 'bar');
$query4->greaterThan('foo', 10);
$opts4 = $query4->_getOptions();
$encoded4 = json_encode($opts4['where'] ?? []);
echo " where = $encoded4\n";
check('foo should have both $eq and $gt', strpos($encoded4, '$eq') !== false && strpos($encoded4, '$gt') !== false);

// Test 5: ParseObject + another constraint on SAME key (mixed case)
echo "\nTest 5: ParseObject with other constraints on same key\n";
$query5 = new ParseQuery('TestClass');
$query5->equalTo('users', $user);
$query5->exists('users');
$opts5 = $query5->_getOptions();
$encoded5 = json_encode($opts5['where'] ?? []);
echo " where = $encoded5\n";
check('should NOT have $eq_pointer in output', strpos($encoded5, '$eq_pointer') === false);
check('users should still contain the pointer payload', strpos($encoded5, '"__type":"Pointer"') !== false);
check('users should still contain the sibling operator', strpos($encoded5, '$exists') !== false);

echo "\n=== Results: $passed passed, $failed failed ===\n";
exit($failed > 0 ? 1 : 0);
129 changes: 129 additions & 0 deletions verify_fix.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* Standalone verification for equalTo fix
* Tests that ParseObject values use direct pointer matching (not $eq)
* and non-Object values still use $eq
*/

require_once __DIR__ . '/src/Parse/ParseClient.php';
require_once __DIR__ . '/src/Parse/ParseObject.php';
require_once __DIR__ . '/src/Parse/ParseQuery.php';
require_once __DIR__ . '/src/Parse/ParseUser.php';
require_once __DIR__ . '/src/Parse/ParseRole.php';
require_once __DIR__ . '/src/Parse/Internal/Encodable.php';

use Parse\ParseClient;
use Parse\ParseObject;
use Parse\ParseQuery;
use Parse\ParseUser;
use Parse\ParseRole;

// Initialize Parse client
ParseClient::initialize('appId', 'restKey', 'https://api.example.com');

$passed = 0;
$failed = 0;

function assert_test($name, $condition) {
global $passed, $failed;
if ($condition) {
echo " ✓ $name\n";
$passed++;
} else {
echo " ✗ FAIL: $name\n";
$failed++;
}
}

echo "=== equalTo Fix Verification ===\n\n";

// Test 1: ParseObject value should use direct pointer match (NOT $eq)
echo "Test 1: ParseObject value uses direct pointer match\n";
$user = ParseObject::create('_User', 'user123');
$query = ParseQuery::getQuery('ParseRole');
$query->equalTo('users', $user);
$where = $query->getWhere();

assert_test(
"where['users'] should NOT have \$eq key",
!isset($where['users']['$eq'])
);
assert_test(
"where['users'] should have __type = Pointer",
isset($where['users']['__type']) && $where['users']['__type'] === 'Pointer'
);
assert_test(
"where['users'] should have className = _User",
isset($where['users']['className']) && $where['users']['className'] === '_User'
);
assert_test(
"where['users'] should have objectId = user123",
isset($where['users']['objectId']) && $where['users']['objectId'] === 'user123'
);

// Test 2: String value should still use $eq
echo "\nTest 2: String value uses \$eq\n";
$query2 = ParseQuery::getQuery('TestClass');
$query2->equalTo('foo', 'bar');
$where2 = $query2->getWhere();

assert_test(
"where['foo'] should have \$eq key",
isset($where2['foo']['$eq'])
);
assert_test(
"where['foo']['\$eq'] should be 'bar'",
isset($where2['foo']['$eq']) && $where2['foo']['$eq'] === 'bar'
);

// Test 3: Number value should still use $eq
echo "\nTest 3: Number value uses \$eq\n";
$query3 = ParseQuery::getQuery('TestClass');
$query3->equalTo('number', 17);
$where3 = $query3->getWhere();

assert_test(
"where['number'] should have \$eq key",
isset($where3['number']['$eq'])
);
assert_test(
"where['number']['\$eq'] should be 17",
isset($where3['number']['$eq']) && $where3['number']['$eq'] === 17
);

// Test 4: null value should still use $eq
echo "\nTest 4: null value uses \$eq\n";
$query4 = ParseQuery::getQuery('TestClass');
$query4->equalTo('num', null);
$where4 = $query4->getWhere();

assert_test(
"where['num'] should have \$eq key",
isset($where4['num']['$eq'])
);
assert_test(
"where['num']['\$eq'] should be null",
isset($where4['num']['$eq']) && $where4['num']['$eq'] === null
);

// Test 5: Array value should still use $eq
echo "\nTest 5: Array value uses \$eq\n";
$query5 = ParseQuery::getQuery('TestClass');
$query5->equalTo('tags', ['foo', 'bar']);
$where5 = $query5->getWhere();

assert_test(
"where['tags'] should have \$eq key",
isset($where5['tags']['$eq'])
);

echo "\n=== Results ===\n";
echo "Passed: $passed\n";
echo "Failed: $failed\n";

if ($failed > 0) {
echo "\n❌ SOME TESTS FAILED\n";
exit(1);
} else {
echo "\n✅ ALL TESTS PASSED\n";
exit(0);
}