Repository: flow-php/flow (PHP)

Issue:
Add alias to KeySet pagination Key
Currently when using keyset pagination we might get to the edge case where key might be ambiguous column. 

This might happen when we join tables, both have `id` column but in the select we expose only one of them: 

```sql
SELECT
 a.id as id
FROM table_a a
LEFT JOIN table_b b ON a.sub_id = b.sub_id
WHERE id > :id_previous
ORDER BY id
```

In this case postgresql will complain that id is ambiguous. 

The solution is to add `alias` to key, so the column will be used in the WHERE/ORDER BY part of the query and alias is going to be used to take last value from previous results. 

Alternatively we could dynamically add an alias to the query and then remove it once we retrieve the value from last row of previous batch. 



Fix (patch):
diff --git a/src/adapter/etl-adapter-doctrine/src/Flow/ETL/Adapter/Doctrine/DbalKeySetExtractor.php b/src/adapter/etl-adapter-doctrine/src/Flow/ETL/Adapter/Doctrine/DbalKeySetExtractor.php
index e6d44a263..a11601f7d 100644
--- a/src/adapter/etl-adapter-doctrine/src/Flow/ETL/Adapter/Doctrine/DbalKeySetExtractor.php
+++ b/src/adapter/etl-adapter-doctrine/src/Flow/ETL/Adapter/Doctrine/DbalKeySetExtractor.php
@@ -20,6 +20,8 @@
  */
 final class DbalKeySetExtractor implements Extractor
 {
+    private string $keyAliasSuffix = '_previous';
+
     private ?int $maximum = null;
 
     private int $pageSize = 1000;
@@ -56,6 +58,7 @@ public function extract(FlowContext $context) : \Generator
 
             foreach ($this->keySet->keys as $key) {
                 $qb->addOrderBy($key->column, $key->order->value);
+                $qb->addSelect($key->column . ' AS ' . $this->keyAlias($key));
             }
 
             if ($lastRow !== null) {
@@ -64,28 +67,30 @@ public function extract(FlowContext $context) : \Generator
                 $parameterTypes = [];
 
                 foreach ($this->keySet->keys as $index => $key) {
-                    if (!\array_key_exists($key->column, $lastRow)) {
+                    $keyAlias = $this->keyAlias($key);
+
+                    if (!\array_key_exists($keyAlias, $lastRow)) {
                         throw new RuntimeException(sprintf('Column "%s" not found in last row for keyset pagination', $key->column));
                     }
 
-                    $lastValue = $lastRow[$key->column];
+                    $lastValue = $lastRow[$keyAlias];
 
                     if ($lastValue === null) {
                         throw new RuntimeException(sprintf('NULL value found in column "%s" for keyset pagination; key columns must be non-null', $key->column));
                     }
 
-                    $paramName = $key->column . '_previous';
-                    $parameters[$paramName] = $lastValue;
-                    $parameterTypes[$paramName] = $key->type;
+                    $parameters[$keyAlias] = $lastValue;
+                    $parameterTypes[$keyAlias] = $key->type;
 
                     $subConditions = [];
 
                     for ($i = 0; $i < $index; $i++) {
                         $prevKey = $this->keySet->keys[$i];
-                        $subConditions[] = $qb->expr()->eq($prevKey->column, ':' . $prevKey->column . '_previous');
+                        $subConditions[] = $qb->expr()->eq($prevKey->column, ':' . $this->keyAlias($prevKey));
                     }
+
                     $operator = $key->order->value === 'DESC' ? 'lt' : 'gt';
-                    $subConditions[] = $qb->expr()->{$operator}($key->column, ':' . $paramName);
+                    $subConditions[] = $qb->expr()->{$operator}($key->column, ':' . $keyAlias);
 
                     $conditions[] = $qb->expr()->and(...$subConditions);
                 }
@@ -112,6 +117,14 @@ public function extract(FlowContext $context) : \Generator
                 $hasRows = true;
                 $lastRow = $row;
 
+                foreach ($this->keySet->keys as $key) {
+                    $keyAlias = $this->keyAlias($key);
+
+                    if (\array_key_exists($keyAlias, $row)) {
+                        unset($row[$keyAlias]);
+                    }
+                }
+
                 $signal = yield array_to_rows($row, $context->entryFactory(), [], $this->schema);
 
                 if ($signal === Extractor\Signal::STOP) {
@@ -131,6 +144,13 @@ public function extract(FlowContext $context) : \Generator
         }
     }
 
+    public function withKeyAliasSuffix(string $keyAliasSuffix) : self
+    {
+        $this->keyAliasSuffix = $keyAliasSuffix;
+
+        return $this;
+    }
+
     /**
      * Sets the maximum number of rows to fetch.
      *
@@ -184,4 +204,9 @@ public function withSchema(Schema $schema) : self
 
         return $this;
     }
+
+    private function keyAlias(Pagination\Key $key) : string
+    {
+        return (string) preg_replace('/[^a-zA-Z0-9_]/', '_', str_replace('.', '_', $key->column . $this->keyAliasSuffix));
+    }
 }