From 9514a46ae7dea38614548628c7a84986b8c0a448 Mon Sep 17 00:00:00 2001
From: Dave Mc Nicoll <info@mcnd.ca>
Date: Mon, 27 Mar 2023 18:38:31 +0000
Subject: [PATCH] - WIP on SQLite Adapter and migration

---
 src/Adapter/AdapterInterface.php    |  1 +
 src/Adapter/DefaultAdapterTrait.php | 14 +++++++++++---
 src/Adapter/MsSQL.php               |  4 ++++
 src/Adapter/MySQL.php               |  4 ++++
 src/Adapter/SQLite.php              | 12 +++++++++---
 src/Attribute/Property/Relation.php | 12 ++++++------
 src/Attribute/Property/Where.php    | 12 +++++++++++-
 src/Entity/Sqlite/Column.php        |  2 +-
 src/Entity/Sqlite/Schema.php        |  4 ++--
 src/EntityCollection.php            | 10 +++++-----
 src/EntityTrait.php                 | 14 ++++++++++++--
 src/Migration/FieldDefinition.php   |  4 +++-
 src/Repository.php                  | 25 +++++++++++++++++--------
 src/Repository/RelationBuilder.php  | 20 ++++++++++++--------
 14 files changed, 98 insertions(+), 40 deletions(-)

diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php
index fd4b655..f383caa 100644
--- a/src/Adapter/AdapterInterface.php
+++ b/src/Adapter/AdapterInterface.php
@@ -28,4 +28,5 @@ interface AdapterInterface {
     public function repositoryClass() : string;
     public function queryBuilderClass() : string;
     public function tableSyntax() : array;
+    public function whitelistAttributes(array &$parameters) : void;
 }
diff --git a/src/Adapter/DefaultAdapterTrait.php b/src/Adapter/DefaultAdapterTrait.php
index a8fe13a..a46b25a 100644
--- a/src/Adapter/DefaultAdapterTrait.php
+++ b/src/Adapter/DefaultAdapterTrait.php
@@ -30,9 +30,12 @@ trait DefaultAdapterTrait
         return $this->database;
     }
 
-    public function schemaTable(ConnectionAdapter $parent, $databaseName, string $tableName) /* : ? object */
+    public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object
     {
-        return Table::repository(Repository::DEFAULT_ALIAS, $parent)->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
+        return Table::repository(Repository::DEFAULT_ALIAS, $adapter)
+            ->select(\Ulmus\Common\Sql::raw('this.*'))
+            ->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
+            ->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
     }
 
     public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
@@ -92,4 +95,9 @@ trait DefaultAdapterTrait
 
         return $typeOnly ? $type : $type . ( isset($length) ? "($length" . ( ! empty($precision) ? ",$precision" : "" ) . ")" : "" );
     }
-}
\ No newline at end of file
+
+    public function whitelistAttributes(array &$parameters) : void
+    {
+        $parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES));
+    }
+}
diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php
index 1d88805..bd6e130 100644
--- a/src/Adapter/MsSQL.php
+++ b/src/Adapter/MsSQL.php
@@ -14,6 +14,10 @@ use Ulmus\{Entity\InformationSchema\Table, Repository, QueryBuilder};
 class MsSQL implements AdapterInterface {
     use DefaultAdapterTrait;
 
+    const ALLOWED_ATTRIBUTES = [
+        'default', 'primary_key', 'auto_increment',
+    ];
+
     const DSN_PREFIX = "sqlsrv";
 
     public int $port;
diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php
index ab31bf9..b5cc47a 100644
--- a/src/Adapter/MySQL.php
+++ b/src/Adapter/MySQL.php
@@ -14,6 +14,10 @@ use Ulmus\Migration\FieldDefinition;
 class MySQL implements AdapterInterface {
     use DefaultAdapterTrait;
 
+    const ALLOWED_ATTRIBUTES = [
+        'default', 'primary_key', 'auto_increment', 'update',
+    ];
+
     const DSN_PREFIX = "mysql";
 
     public string $hostname;
diff --git a/src/Adapter/SQLite.php b/src/Adapter/SQLite.php
index 743e2e0..d07ca0d 100644
--- a/src/Adapter/SQLite.php
+++ b/src/Adapter/SQLite.php
@@ -3,6 +3,7 @@
 namespace Ulmus\Adapter;
 
 use Ulmus\Common\PdoObject;
+use Ulmus\ConnectionAdapter;
 
 use Ulmus\Entity\Sqlite\Table;
 use Ulmus\Exception\AdapterConfigurationException;
@@ -11,6 +12,9 @@ use Ulmus\{Repository, QueryBuilder, Ulmus};
 
 class SQLite implements AdapterInterface {
     use DefaultAdapterTrait;
+    const ALLOWED_ATTRIBUTES = [
+        'default', 'primary_key', 'auto_increment'
+    ];
 
     const DSN_PREFIX = "sqlite";
 
@@ -87,9 +91,11 @@ class SQLite implements AdapterInterface {
         return substr($base, 0, strrpos($base, '.') ?: strlen($base));
     }
 
-    public function schemaTable(string $databaseName, string $tableName) : null|object
+    public function schemaTable(ConnectionAdapter $adapter, string $databaseName, string $tableName) : null|object
     {
-        return Table::repository()->loadOneFromField(Table::field('tableName'), $tableName);
+        return Table::repository(Repository::DEFAULT_ALIAS, $adapter)
+            ->select(\Ulmus\Common\Sql::raw('this.*'))
+            ->loadOneFromField(Table::field('tableName'), $tableName);
     }
 
     public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
@@ -197,4 +203,4 @@ class SQLite implements AdapterInterface {
         $pdo->sqliteCreateFunction('month', fn($date) => ( new \DateTime($date) )->format('m'), 1);
         $pdo->sqliteCreateFunction('year', fn($date) => ( new \DateTime($date) )->format('Y'), 1);
     }
-}
\ No newline at end of file
+}
diff --git a/src/Attribute/Property/Relation.php b/src/Attribute/Property/Relation.php
index 9a8ddb6..58e6f0d 100644
--- a/src/Attribute/Property/Relation.php
+++ b/src/Attribute/Property/Relation.php
@@ -9,7 +9,7 @@ class Relation {
     public function __construct(
         public Relation\RelationTypeEnum|string $type,
         public \Stringable|string|array $key = "",
-        public null|\Closure $generateKey = null,
+        public null|\Closure|array $generateKey = null,
         public null|\Stringable|string|array $foreignKey = null,
         public null|\Stringable|string|array $foreignField = null,
         public array $foreignKeys = [],
@@ -20,7 +20,7 @@ class Relation {
         public null|\Stringable|string|array $field = null,
         public null|string $entity = null,
         public null|string $join = null,
-        public string $function = "loadAll",
+        public null|string $function = null,
     ) {
         $this->key = Attribute::handleArrayField($this->key);
         $this->foreignKey = Attribute::handleArrayField($this->foreignKey);
@@ -54,17 +54,17 @@ class Relation {
 
     public function isOneToOne() : bool
     {
-        return $this->type === Relation\RelationTypeEnum::oneToOne || $this->normalizeType() === 'onetoone';
+        return $this->type instanceof Relation\RelationTypeEnum ? $this->type === Relation\RelationTypeEnum::oneToOne : $this->normalizeType() === 'onetoone';
     }
 
     public function isOneToMany() : bool
     {
-        return $this->type === Relation\RelationTypeEnum::oneToMany || $this->normalizeType() === 'onetomany';
+        return $this->type instanceof Relation\RelationTypeEnum ? $this->type === Relation\RelationTypeEnum::oneToMany : $this->normalizeType() === 'onetomany';
     }
 
     public function isManyToMany() : bool
     {
-        return $this->type === Relation\RelationTypeEnum::manyToMany || $this->normalizeType() === 'manytomany';
+        return $this->type instanceof Relation\RelationTypeEnum ? $this->type === Relation\RelationTypeEnum::manyToMany : $this->normalizeType() === 'manytomany';
     }
 
     public function function() : string
@@ -73,7 +73,7 @@ class Relation {
             return $this->function;
         }
         elseif ($this->isOneToOne()) {
-            return 'load';
+            return 'loadOne';
         }
 
         return 'loadAll';
diff --git a/src/Attribute/Property/Where.php b/src/Attribute/Property/Where.php
index b073022..ce90e21 100644
--- a/src/Attribute/Property/Where.php
+++ b/src/Attribute/Property/Where.php
@@ -13,13 +13,23 @@ class Where {
         public string $operator = Query\Where::OPERATOR_EQUAL,
         public string $condition = Query\Where::CONDITION_AND,
         public string|\Stringable|array|null $fieldValue = null,
+        public null|array $generateValue = null,
     ) {
         $this->field = Attribute::handleArrayField($field);
         $this->fieldValue = Attribute::handleArrayField($fieldValue);
     }
 
-    public function getValue() : mixed
+    public function getValue(/* null|EntityInterface */ $entity = null) : mixed
     {
+        if ($this->generateValue) {
+            if ($entity) {
+                return call_user_func_array($this->generateValue, [ $entity ]);
+            }
+            else {
+                throw new \Exception(sprintf("Could not generate value from non-instanciated entity for field %s.", (string) $this->field));
+            }
+        }
+
         return $this->fieldValue ?? $this->value;
     }
 }
diff --git a/src/Entity/Sqlite/Column.php b/src/Entity/Sqlite/Column.php
index bb420b2..4537b3c 100644
--- a/src/Entity/Sqlite/Column.php
+++ b/src/Entity/Sqlite/Column.php
@@ -12,7 +12,7 @@ class Column
 {
     use \Ulmus\EntityTrait;
 
-    #[Id]
+    #[Field\Id]
     public int $cid;
 
     #[Field]
diff --git a/src/Entity/Sqlite/Schema.php b/src/Entity/Sqlite/Schema.php
index f15aae8..491a1c2 100644
--- a/src/Entity/Sqlite/Schema.php
+++ b/src/Entity/Sqlite/Schema.php
@@ -12,7 +12,7 @@ class Schema
 {
     use \Ulmus\EntityTrait;
 
-    #[Id]
+    #[Field\Id]
     public ? string $name;
 
     #[Field]
@@ -27,6 +27,6 @@ class Schema
     #[Field]
     public ? string $sql;
 
-    #[Relation("oneToMany", key: "tableName", foreignKey: "tableName", entity: "Schema")]
+    #[Relation("oneToMany", key: "tableName", foreignKey: "tableName", entity: Column::class)]
     public EntityCollection $columns;
 }
\ No newline at end of file
diff --git a/src/EntityCollection.php b/src/EntityCollection.php
index 100c275..0058f41 100644
--- a/src/EntityCollection.php
+++ b/src/EntityCollection.php
@@ -123,14 +123,14 @@ class EntityCollection extends \ArrayObject {
         return $this;
     }
 
-    public function search($value, string $field, bool $strict = true) : Generator
+    public function search(mixed $value, string $field, bool $strict = true) : Generator
     {
         foreach($this->filters(fn($v) => isset($v->$field) ? ( $strict ? $v->$field === $value : $v->$field == $value ) : false) as $key => $item) {
             yield $key => $item;
         }
     }
 
-    public function searchOne($value, string $field, bool $strict = true) : ? object
+    public function searchOne(mixed $value, string $field, bool $strict = true) : ? object
     {
         # Returning first value only
         foreach($this->search($value, $field, $strict) as $item) {
@@ -140,7 +140,7 @@ class EntityCollection extends \ArrayObject {
         return null;
     }
 
-    public function searchAll(/* mixed*/ $values, string $field, bool $strict = true, bool $compareArray = false) : self
+    public function searchAll(mixed $values, string $field, bool $strict = true, bool $compareArray = false) : self
     {
         $collection = new static();
 
@@ -155,7 +155,7 @@ class EntityCollection extends \ArrayObject {
         return $collection;
     }
 
-    public function diffAll(/* mixed */ $values, string $field, bool $strict = true, bool $compareArray = false) : self
+    public function diffAll(mixed $values, string $field, bool $strict = true, bool $compareArray = false) : self
     {
         $obj = new static($this->getArrayCopy());
 
@@ -214,7 +214,7 @@ class EntityCollection extends \ArrayObject {
         return $list;
     }
 
-    public function unique(\Stringable|callable $field, bool $strict = false) : self
+    public function unique(\Stringable|callable|string $field, bool $strict = false) : self
     {
         $list = [];
         $obj = new static();
diff --git a/src/EntityTrait.php b/src/EntityTrait.php
index 6fd97fd..ef1bf56 100644
--- a/src/EntityTrait.php
+++ b/src/EntityTrait.php
@@ -34,7 +34,6 @@ trait EntityTrait {
         $entityResolver = $this->resolveEntity();
 
         foreach($dataset as $key => $value) {
-
             $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null;
             $field ??= $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false);
 
@@ -51,7 +50,18 @@ trait EntityTrait {
             }
             elseif ( $field['type'] === 'array' ) {
                 if ( is_string($value)) {
-                    $this->{$field['name']} = substr($value, 0, 1) === "a" ? unserialize($value) : json_decode($value, true);
+                    if (substr($value, 0, 1) === "a") {
+                        $this->{$field['name']} = unserialize($value);
+                    }
+                    else {
+                        $data = json_decode($value, true);
+
+                        if (json_last_error() !== \JSON_ERROR_NONE) {
+                            throw new \Exception(sprintf("JSON error while decoding in EntityTrait : '%s' given %s", json_last_error_msg(), $value));
+                        }
+
+                        $this->{$field['name']} = $data;
+                    }
                 }
                 elseif ( is_array($value) ) {
                     $this->{$field['name']} = $value;
diff --git a/src/Migration/FieldDefinition.php b/src/Migration/FieldDefinition.php
index 8e1209b..449ae83 100644
--- a/src/Migration/FieldDefinition.php
+++ b/src/Migration/FieldDefinition.php
@@ -23,7 +23,7 @@ class FieldDefinition {
 
     public ? int $precision;
 
-    public ? int $length;
+    public null|int|string $length;
 
     public ? string $update;
 
@@ -38,6 +38,8 @@ class FieldDefinition {
         $this->tags = $data['tags'];
 
         $field = $this->getFieldTag();
+        $adapter->whitelistAttributes($field->attributes);
+
         $this->type = $field->type ?? $data['type'];
         $this->length = $field->length ?? null;
         $this->precision = $field->precision ?? null;
diff --git a/src/Repository.php b/src/Repository.php
index 0ad31a5..1f19b63 100644
--- a/src/Repository.php
+++ b/src/Repository.php
@@ -39,7 +39,9 @@ class Repository
         $this->alias = $alias;
         $this->entityResolver = Ulmus::resolveEntity($entity);
         $this->adapter = $adapter ?? $this->entityResolver->databaseAdapter();
-        $this->queryBuilder = Ulmus::queryBuilder($entity);
+
+        $queryBuilder = $this->adapter->adapter()->queryBuilderClass();
+        $this->queryBuilder = new $queryBuilder();
     }
 
     public function __clone()
@@ -49,7 +51,7 @@ class Repository
 
     public function loadOne() : ? object
     {
-        return $this->limit(1)->selectSqlQuery()->collectionFromQuery()[0] ?? null;
+        return $this->limit(1)->selectSqlQuery()->collectionFromQuery()[0];
     }
 
     public function loadOneFromField($field, $value) : ? object
@@ -606,7 +608,7 @@ class Repository
 
             $isRelation = ( $annotation instanceof Relation ) || ($annotation instanceof Attribute\Property\Relation);
 
-            if ($isRelation && ( $annotation->normalizeType() === 'manytomany' )) {
+            if ($isRelation && ( $annotation->isManyToMany() )) {
                 throw new Exception("Many-to-many relation can not be preloaded within joins.");
             }
 
@@ -618,10 +620,12 @@ class Repository
                 foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
                     if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ]) ) {
                         $escAlias = $this->escapeIdentifier($alias);
+                        $fieldName = $this->escapeIdentifier($key);
 
                         $name = $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Field::class, Field::class ])->name ?? $field['name'];
 
-                        $this->select("$escAlias.$key as $alias\${$name}");
+
+                        $this->select("$escAlias.$fieldName as $alias\${$name}");
                     }
                 }
 
@@ -761,7 +765,8 @@ class Repository
                 }
 
                 foreach ($where as $condition) {
-                    $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->getValue(), $condition->operator, $condition->condition);
+                    # $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->getValue(), $condition->operator, $condition->condition);
+                    $repository->where($condition->field, $condition->getValue($this), $condition->operator, $condition->condition);
                 }
 
                 foreach ($order as $item) {
@@ -778,17 +783,21 @@ class Repository
                     $values[] = is_callable($field) ? $field($item) : $item->$entityProperty;
                 }
 
+                $values = array_unique($values);
+
                 $repository->where($key, $values);
 
-                $results = call_user_func([ $repository, $relation->function() ]);
+                $results = $repository->loadAll();
 
                 if ($relation->isOneToOne()) {
-                    $item->$name = $results ?: new $baseEntity();
+                    foreach ($collection as $item) {
+                        $item->$name = $results->searchOne($item->$entityProperty, $property) ?: new $baseEntity();
+                    }
                 }
                 elseif ($relation->isOneToMany()) {
                     foreach ($collection as $item) {
                         $item->$name = $baseEntity::entityCollection();
-                        $item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty));
+                        $item->$name->mergeWith($results->searchAll($item->$entityProperty, $property));
                     }
                 }
             }
diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php
index cd1ef38..5202053 100644
--- a/src/Repository/RelationBuilder.php
+++ b/src/Repository/RelationBuilder.php
@@ -102,23 +102,21 @@ class RelationBuilder
 
                     $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);
 
-                    $result = call_user_func([ $this->repository, $relation->function ]);
-
-                    return count($result) === 0 ? $this->instanciateEmptyEntity($name, $relation): $result[0];
+                    return call_user_func([ $this->repository, $relation->function() ]) ?? $this->instanciateEmptyEntity($name, $relation);
 
                 case $relation->isOneToMany():
                     $this->oneToMany($name, $relation);
 
                     $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);
 
-                    return call_user_func([ $this->repository, $relation->function ]);
+                    return call_user_func([ $this->repository, $relation->function() ]);
 
                 case $relation->isManyToMany():
                     $this->manyToMany($name, $relation, $relationRelation);
 
                     $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);
 
-                    $results = call_user_func([ $this->repository, $relationRelation->function ]);
+                    $results = call_user_func([ $this->repository, $relationRelation->function() ]);
 
                     if ($relation->bridgeField ?? false) {
                         $collection = $relation->bridge::entityCollection();
@@ -152,7 +150,7 @@ class RelationBuilder
             $this->repository->open();
 
             foreach($this->wheres as $condition) {
-                $this->repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [ $this->entity ]) : $condition->getValue(), $condition->operator);
+                $this->repository->where($condition->field, $condition->getValue($this->entity), $condition->operator);
             }
 
             $this->repository->close();
@@ -201,7 +199,7 @@ class RelationBuilder
             $vars = [];
             $len = strlen( $name ) + 1;
 
-            $isRelation = ( $annotation instanceof Relation ) || ($annotation instanceof Attribute\Property\Relation);
+            $isRelation = ( $annotation instanceof Relation ) || ( $annotation instanceof Attribute\Property\Relation );
 
             if ( $isRelation && $annotation->isManyToMany() ) {
                 $entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity;
@@ -262,7 +260,13 @@ class RelationBuilder
         $field = $relation->key;
 
         if ($relation->foreignKey) {
-            $value = ! is_string($field) && is_callable($field) ? $field($this->entity) : $this->entity->$field;
+            if ( $relation->generateKey ) {
+                $value = call_user_func_array($relation->generateKey, [ $this->entity ]);
+            }
+            else {
+                $value = $this->entity->$field;
+            }
+
             $this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), $value );
         }