dev/core#2271 - APIv4 - Smarter check for adding default ON clause to explicit joins
authorColeman Watts <coleman@civicrm.org>
Thu, 24 Dec 2020 03:04:39 +0000 (22:04 -0500)
committerColeman Watts <coleman@civicrm.org>
Thu, 24 Dec 2020 13:00:37 +0000 (08:00 -0500)
When adding explicit joins in APIv4, the API will automacially add an ON clause linking it
to the base entity unless it detects some other link already present in the ON clause supplied.
Previously it would determine this by checking for the join entity ID;
now it looks for the join entity ID OR any other field in the join entity with an FK.
This gives more leeway for people to add creative joins without the defaults getting in the way.

Civi/Api4/Query/Api4SelectQuery.php

index 922f0954a203fc209dc4ef1ab0bd6a2f70df8065..c02568433a82620fbec688f9debc1893d62c3280 100644 (file)
@@ -535,7 +535,8 @@ class Api4SelectQuery {
       $side = array_shift($join) ? 'INNER' : 'LEFT';
       // Add all fields from joined entity to spec
       $joinEntityGet = \Civi\API\Request::create($entity, 'get', ['version' => 4, 'checkPermissions' => $this->getCheckPermissions()]);
-      foreach ($joinEntityGet->entityFields() as $field) {
+      $joinEntityFields = $joinEntityGet->entityFields();
+      foreach ($joinEntityFields as $field) {
         $field['sql_name'] = '`' . $alias . '`.`' . $field['column_name'] . '`';
         $this->addSpecField($alias . '.' . $field['name'], $field);
       }
@@ -543,7 +544,7 @@ class Api4SelectQuery {
         $conditions = $this->getBridgeJoin($join, $entity, $alias);
       }
       else {
-        $conditions = $this->getJoinConditions($join, $entity, $alias);
+        $conditions = $this->getJoinConditions($join, $entity, $alias, $joinEntityFields);
       }
       foreach (array_filter($join) as $clause) {
         $conditions[] = $this->treeWalkClauses($clause, 'ON');
@@ -559,20 +560,30 @@ class Api4SelectQuery {
    * @param array $joinTree
    * @param string $joinEntity
    * @param string $alias
+   * @param array $joinEntityFields
    * @return array
    */
-  private function getJoinConditions($joinTree, $joinEntity, $alias) {
+  private function getJoinConditions($joinTree, $joinEntity, $alias, $joinEntityFields) {
     $conditions = [];
     // getAclClause() expects a stack of 1-to-1 join fields to help it dedupe, but this is more flexible,
     // so unless this is a direct 1-to-1 join with the main entity, we'll just hack it
     // with a padded empty stack to bypass its deduping.
     $stack = [NULL, NULL];
-    // If we're not explicitly referencing the joinEntity ID in the ON clause, search for a default
-    $explicitId = array_filter($joinTree, function($clause) use ($alias) {
+    // See if the ON clause already contains an FK reference to joinEntity
+    $explicitFK = array_filter($joinTree, function($clause) use ($alias, $joinEntityFields) {
       list($sideA, $op, $sideB) = array_pad((array) $clause, 3, NULL);
-      return $op === '=' && ($sideA === "$alias.id" || $sideB === "$alias.id");
+      if ($op !== '=' || !$sideB) {
+        return FALSE;
+      }
+      foreach ([$sideA, $sideB] as $expr) {
+        if ($expr === "$alias.id" || !empty($joinEntityFields["$alias.$expr"]['fk_entity'])) {
+          return TRUE;
+        }
+      }
+      return FALSE;
     });
-    if (!$explicitId) {
+    // If we're not explicitly referencing the ID (or some other FK field) of the joinEntity, search for a default
+    if (!$explicitFK) {
       foreach ($this->apiFieldSpec as $name => $field) {
         if ($field['entity'] !== $joinEntity && $field['fk_entity'] === $joinEntity) {
           $conditions[] = $this->treeWalkClauses([$name, '=', "$alias.id"], 'ON');