Skip to content

Commit 52187f1

Browse files
Support JOIN directly on Mapping (#18)
* [ENHANCE] Support joins on Mapping * Added regex check to column names when prefixing * Removed removeTablePrefixFrom as it's never needed * Manual prefixing in tests * Prefixing deletion timestamp and primary key where needed * Prefixing table name of definition only * Removed abstractions for primaryKey and deletionTimestamp * Update comments and fix some parameters * Made Mapping::prefixTableNameTo private * Add whitespace and formatting * Added curly braces to condition * Removed redundant prefixing of local column * Removed capability of prefixing dictionaries --------- Co-authored-by: Josh McRae <josh.mcrae@tithe.ly>
1 parent 36a870f commit 52187f1

File tree

3 files changed

+144
-7
lines changed

3 files changed

+144
-7
lines changed

src/Mapping.php

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function __construct(Database $db, Definition $definition, array $columns
5252
public function findOne()
5353
{
5454
if ($this->definition->getDeletionTimestamp()) {
55-
$this->isNull($this->definition->getDeletionTimestamp());
55+
$this->isNull($this->prefixTableNameTo($this->definition->getDeletionTimestamp()));
5656
}
5757

5858
$this->columns(...$this->buildColumns());
@@ -75,7 +75,7 @@ public function findOne()
7575
public function findAll()
7676
{
7777
if ($this->definition->getDeletionTimestamp()) {
78-
$this->isNull($this->definition->getDeletionTimestamp());
78+
$this->isNull($this->prefixTableNameTo($this->definition->getDeletionTimestamp()));
7979
}
8080

8181
$this->columns(...$this->buildColumns());
@@ -305,7 +305,7 @@ private function replace(array $data, array $original, array $deleteIds = [])
305305
}
306306

307307
if ($this->definition->getDeletionTimestamp()) {
308-
$query->isNull($this->definition->getDeletionTimestamp());
308+
$query->isNull($this->prefixTableNameTo($this->definition->getDeletionTimestamp()));
309309
}
310310

311311
$base = $this->getBaseData($data);
@@ -573,7 +573,7 @@ private function getBaseData(array $data)
573573
*/
574574
private function buildColumns()
575575
{
576-
$columns = $this->definition->getPrimaryKey();
576+
$columns = $this->prefixTableNameTo($this->definition->getPrimaryKey());
577577
$required = array_merge($this->definition->getColumns(), $this->columns);
578578

579579
foreach ($this->definition->getProperties() as $item) {
@@ -618,4 +618,48 @@ private function dispatch(string $event, array $data, $args = [])
618618
call_user_func_array($hook, $args);
619619
}
620620
}
621+
622+
/**
623+
* Checks if string is a valid SQL column name and has no table prefix.
624+
*
625+
* @param string $column
626+
* @return bool
627+
*/
628+
private function requiresPrefix(string $column): bool
629+
{
630+
$pattern = '/^[a-zA-Z_][a-zA-Z0-9_]*$/';
631+
return (bool) preg_match($pattern, $column);
632+
}
633+
634+
/**
635+
* Appends table name to provided value. Works with strings and arrays. If multidimensional array is
636+
* passed, the values will be updated recursively. Multiple passes are protected and will only prefix
637+
* the column name once.
638+
*
639+
* String Example:
640+
* 'field' -> 'table.field'
641+
*
642+
* Array Example:
643+
* ['field', 'field2'] -> ['table.field', 'table.field2']
644+
* ['field', ['field2', 'field3']] -> ['table.field', ['table.field2', 'table.field3']]
645+
*
646+
* @return string | array
647+
*/
648+
private function prefixTableNameTo($input) {
649+
$table = $this->definition->getTable();
650+
651+
if (is_string($input)) {
652+
return $this->requiresPrefix($input) ? "$table.$input" : $input;
653+
} elseif (is_array($input)) {
654+
$output = [];
655+
656+
foreach ($input as $value) {
657+
$output[] = $this->prefixTableNameTo($value);
658+
}
659+
660+
return $output;
661+
}
662+
663+
return $input;
664+
}
621665
}

tests/Fixtures.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-- Prepare schema
22
DROP TABLE IF EXISTS customers;
3-
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
3+
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, date_deleted TEXT);
44

55
DROP TABLE IF EXISTS orders;
66
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, date_created TEXT, date_deleted TEXT);

tests/MappingTest.php

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,97 @@ public function testUpdateRollback()
290290
$this->assertEquals($original, $updated);
291291
}
292292

293+
public function testPrefixAndRemoveTableName()
294+
{
295+
$method = new \ReflectionMethod('PicoMapper\Mapping', 'prefixTableNameTo');
296+
$method->setAccessible(true);
297+
298+
// Test with string input
299+
$input = 'field';
300+
$expectedOutput = 'customers.field';
301+
$actualOutput = $method->invoke($this->getMapping(), $input);
302+
$this->assertEquals($expectedOutput, $actualOutput);
303+
304+
// Test with array input
305+
$input = ['field1', 'field2'];
306+
$expectedOutput = ['customers.field1', 'customers.field2'];
307+
$actualOutput = $method->invoke($this->getMapping(), $input);
308+
$this->assertEquals($expectedOutput, $actualOutput);
309+
310+
// Test with nested array input
311+
$input = ['field1', ['nestedField1', 'nestedField2']];
312+
$expectedOutput = ['customers.field1', ['customers.nestedField1', 'customers.nestedField2']];
313+
$actualOutput = $method->invoke($this->getMapping(), $input);
314+
$this->assertEquals($expectedOutput, $actualOutput);
315+
316+
// Test multi-pass safety
317+
$input = ['field1', 'field2'];
318+
$expectedOutput = ['customers.field1', 'customers.field2'];
319+
$actualOutput = $method->invoke($this->getMapping(), $method->invoke($this->getMapping(), $input));
320+
$this->assertEquals($expectedOutput, $actualOutput);
321+
322+
// Don't change input if other table already appended
323+
$input = 'table2.field';
324+
$expectedOutput = 'table2.field';
325+
$actualOutput = $method->invoke($this->getMapping(), $input);
326+
$this->assertEquals($expectedOutput, $actualOutput);
327+
}
328+
329+
public function testFindOneWithDirectJoin()
330+
{
331+
$customer = $this->getMapping()
332+
->join('orders', 'customer_id', 'id')
333+
->eq('customers.id', 2)
334+
->findOne();
335+
336+
$this->assertEquals('Jane Doe', $customer['name']);
337+
$this->assertCount(1, $customer['orders']);
338+
$this->assertCount(2, $customer['orders'][0]['items']);
339+
340+
$this->assertEquals('2018-01-02', $customer['orders'][0]['date_created']);
341+
$this->assertEquals('Bread', $customer['orders'][0]['items'][0]['description']);
342+
$this->assertEquals(120, $customer['orders'][0]['items'][0]['amount']);
343+
$this->assertEquals('Yogurt', $customer['orders'][0]['items'][1]['description']);
344+
$this->assertEquals(400, $customer['orders'][0]['items'][1]['amount']);
345+
}
346+
347+
public function testFindOneWithDirectLeft()
348+
{
349+
$customer = $this->getMapping()
350+
->left('orders', 'o', 'customer_id', 'customers', 'id')
351+
->eq('customers.id', 2)
352+
->findOne();
353+
354+
$this->assertEquals('Jane Doe', $customer['name']);
355+
$this->assertCount(1, $customer['orders']);
356+
$this->assertCount(2, $customer['orders'][0]['items']);
357+
358+
$this->assertEquals('2018-01-02', $customer['orders'][0]['date_created']);
359+
$this->assertEquals('Bread', $customer['orders'][0]['items'][0]['description']);
360+
$this->assertEquals(120, $customer['orders'][0]['items'][0]['amount']);
361+
$this->assertEquals('Yogurt', $customer['orders'][0]['items'][1]['description']);
362+
$this->assertEquals(400, $customer['orders'][0]['items'][1]['amount']);
363+
}
364+
365+
public function testFindOneWithDirectAndSecondaryLeft()
366+
{
367+
$customer = $this->getMapping()
368+
->left('orders', 'o', 'customer_id', 'customers', 'id')
369+
->left('items', 'i', 'order_id', 'o', 'id')
370+
->eq('customers.id', 2)
371+
->findOne();
372+
373+
$this->assertEquals('Jane Doe', $customer['name']);
374+
$this->assertCount(1, $customer['orders']);
375+
$this->assertCount(2, $customer['orders'][0]['items']);
376+
377+
$this->assertEquals('2018-01-02', $customer['orders'][0]['date_created']);
378+
$this->assertEquals('Bread', $customer['orders'][0]['items'][0]['description']);
379+
$this->assertEquals(120, $customer['orders'][0]['items'][0]['amount']);
380+
$this->assertEquals('Yogurt', $customer['orders'][0]['items'][1]['description']);
381+
$this->assertEquals(400, $customer['orders'][0]['items'][1]['amount']);
382+
}
383+
293384
/**
294385
* Returns a new mapping for testing.
295386
*
@@ -316,7 +407,8 @@ public function getMapping()
316407

317408
$customer = (new Definition('customers'))
318409
->withColumns('id', 'name')
319-
->withMany($order, 'orders', 'customer_id');
410+
->withMany($order, 'orders', 'customer_id')
411+
->withDeletionTimestamp('date_deleted');
320412

321413
return new Mapping($this->db, $customer, [], [
322414
'inserted' => [$this->hook],
@@ -346,7 +438,8 @@ public function getReadOnlyMapping()
346438

347439
$customer = (new Definition('customers'))
348440
->withColumns('id', 'name')
349-
->withMany($order, 'orders', 'customer_id');
441+
->withMany($order, 'orders', 'customer_id')
442+
->withDeletionTimestamp('date_deleted');
350443

351444
return new Mapping($this->db, $customer);
352445
}

0 commit comments

Comments
 (0)