- WIP on SQLite Adapter and migration

This commit is contained in:
Dave M. 2023-03-27 18:38:31 +00:00
parent cee978ecfd
commit 9514a46ae7
14 changed files with 98 additions and 40 deletions

View File

@ -28,4 +28,5 @@ interface AdapterInterface {
public function repositoryClass() : string; public function repositoryClass() : string;
public function queryBuilderClass() : string; public function queryBuilderClass() : string;
public function tableSyntax() : array; public function tableSyntax() : array;
public function whitelistAttributes(array &$parameters) : void;
} }

View File

@ -30,9 +30,12 @@ trait DefaultAdapterTrait
return $this->database; 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 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" : "" ) . ")" : "" ); return $typeOnly ? $type : $type . ( isset($length) ? "($length" . ( ! empty($precision) ? ",$precision" : "" ) . ")" : "" );
} }
public function whitelistAttributes(array &$parameters) : void
{
$parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES));
}
} }

View File

@ -14,6 +14,10 @@ use Ulmus\{Entity\InformationSchema\Table, Repository, QueryBuilder};
class MsSQL implements AdapterInterface { class MsSQL implements AdapterInterface {
use DefaultAdapterTrait; use DefaultAdapterTrait;
const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment',
];
const DSN_PREFIX = "sqlsrv"; const DSN_PREFIX = "sqlsrv";
public int $port; public int $port;

View File

@ -14,6 +14,10 @@ use Ulmus\Migration\FieldDefinition;
class MySQL implements AdapterInterface { class MySQL implements AdapterInterface {
use DefaultAdapterTrait; use DefaultAdapterTrait;
const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment', 'update',
];
const DSN_PREFIX = "mysql"; const DSN_PREFIX = "mysql";
public string $hostname; public string $hostname;

View File

@ -3,6 +3,7 @@
namespace Ulmus\Adapter; namespace Ulmus\Adapter;
use Ulmus\Common\PdoObject; use Ulmus\Common\PdoObject;
use Ulmus\ConnectionAdapter;
use Ulmus\Entity\Sqlite\Table; use Ulmus\Entity\Sqlite\Table;
use Ulmus\Exception\AdapterConfigurationException; use Ulmus\Exception\AdapterConfigurationException;
@ -11,6 +12,9 @@ use Ulmus\{Repository, QueryBuilder, Ulmus};
class SQLite implements AdapterInterface { class SQLite implements AdapterInterface {
use DefaultAdapterTrait; use DefaultAdapterTrait;
const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment'
];
const DSN_PREFIX = "sqlite"; const DSN_PREFIX = "sqlite";
@ -87,9 +91,11 @@ class SQLite implements AdapterInterface {
return substr($base, 0, strrpos($base, '.') ?: strlen($base)); 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 public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string

View File

@ -9,7 +9,7 @@ class Relation {
public function __construct( public function __construct(
public Relation\RelationTypeEnum|string $type, public Relation\RelationTypeEnum|string $type,
public \Stringable|string|array $key = "", 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 $foreignKey = null,
public null|\Stringable|string|array $foreignField = null, public null|\Stringable|string|array $foreignField = null,
public array $foreignKeys = [], public array $foreignKeys = [],
@ -20,7 +20,7 @@ class Relation {
public null|\Stringable|string|array $field = null, public null|\Stringable|string|array $field = null,
public null|string $entity = null, public null|string $entity = null,
public null|string $join = null, public null|string $join = null,
public string $function = "loadAll", public null|string $function = null,
) { ) {
$this->key = Attribute::handleArrayField($this->key); $this->key = Attribute::handleArrayField($this->key);
$this->foreignKey = Attribute::handleArrayField($this->foreignKey); $this->foreignKey = Attribute::handleArrayField($this->foreignKey);
@ -54,17 +54,17 @@ class Relation {
public function isOneToOne() : bool 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 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 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 public function function() : string
@ -73,7 +73,7 @@ class Relation {
return $this->function; return $this->function;
} }
elseif ($this->isOneToOne()) { elseif ($this->isOneToOne()) {
return 'load'; return 'loadOne';
} }
return 'loadAll'; return 'loadAll';

View File

@ -13,13 +13,23 @@ class Where {
public string $operator = Query\Where::OPERATOR_EQUAL, public string $operator = Query\Where::OPERATOR_EQUAL,
public string $condition = Query\Where::CONDITION_AND, public string $condition = Query\Where::CONDITION_AND,
public string|\Stringable|array|null $fieldValue = null, public string|\Stringable|array|null $fieldValue = null,
public null|array $generateValue = null,
) { ) {
$this->field = Attribute::handleArrayField($field); $this->field = Attribute::handleArrayField($field);
$this->fieldValue = Attribute::handleArrayField($fieldValue); $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; return $this->fieldValue ?? $this->value;
} }
} }

View File

@ -12,7 +12,7 @@ class Column
{ {
use \Ulmus\EntityTrait; use \Ulmus\EntityTrait;
#[Id] #[Field\Id]
public int $cid; public int $cid;
#[Field] #[Field]

View File

@ -12,7 +12,7 @@ class Schema
{ {
use \Ulmus\EntityTrait; use \Ulmus\EntityTrait;
#[Id] #[Field\Id]
public ? string $name; public ? string $name;
#[Field] #[Field]
@ -27,6 +27,6 @@ class Schema
#[Field] #[Field]
public ? string $sql; public ? string $sql;
#[Relation("oneToMany", key: "tableName", foreignKey: "tableName", entity: "Schema")] #[Relation("oneToMany", key: "tableName", foreignKey: "tableName", entity: Column::class)]
public EntityCollection $columns; public EntityCollection $columns;
} }

View File

@ -123,14 +123,14 @@ class EntityCollection extends \ArrayObject {
return $this; 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) { foreach($this->filters(fn($v) => isset($v->$field) ? ( $strict ? $v->$field === $value : $v->$field == $value ) : false) as $key => $item) {
yield $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 # Returning first value only
foreach($this->search($value, $field, $strict) as $item) { foreach($this->search($value, $field, $strict) as $item) {
@ -140,7 +140,7 @@ class EntityCollection extends \ArrayObject {
return null; 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(); $collection = new static();
@ -155,7 +155,7 @@ class EntityCollection extends \ArrayObject {
return $collection; 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()); $obj = new static($this->getArrayCopy());
@ -214,7 +214,7 @@ class EntityCollection extends \ArrayObject {
return $list; return $list;
} }
public function unique(\Stringable|callable $field, bool $strict = false) : self public function unique(\Stringable|callable|string $field, bool $strict = false) : self
{ {
$list = []; $list = [];
$obj = new static(); $obj = new static();

View File

@ -34,7 +34,6 @@ trait EntityTrait {
$entityResolver = $this->resolveEntity(); $entityResolver = $this->resolveEntity();
foreach($dataset as $key => $value) { foreach($dataset as $key => $value) {
$field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null; $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null;
$field ??= $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false); $field ??= $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false);
@ -51,7 +50,18 @@ trait EntityTrait {
} }
elseif ( $field['type'] === 'array' ) { elseif ( $field['type'] === 'array' ) {
if ( is_string($value)) { 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) ) { elseif ( is_array($value) ) {
$this->{$field['name']} = $value; $this->{$field['name']} = $value;

View File

@ -23,7 +23,7 @@ class FieldDefinition {
public ? int $precision; public ? int $precision;
public ? int $length; public null|int|string $length;
public ? string $update; public ? string $update;
@ -38,6 +38,8 @@ class FieldDefinition {
$this->tags = $data['tags']; $this->tags = $data['tags'];
$field = $this->getFieldTag(); $field = $this->getFieldTag();
$adapter->whitelistAttributes($field->attributes);
$this->type = $field->type ?? $data['type']; $this->type = $field->type ?? $data['type'];
$this->length = $field->length ?? null; $this->length = $field->length ?? null;
$this->precision = $field->precision ?? null; $this->precision = $field->precision ?? null;

View File

@ -39,7 +39,9 @@ class Repository
$this->alias = $alias; $this->alias = $alias;
$this->entityResolver = Ulmus::resolveEntity($entity); $this->entityResolver = Ulmus::resolveEntity($entity);
$this->adapter = $adapter ?? $this->entityResolver->databaseAdapter(); $this->adapter = $adapter ?? $this->entityResolver->databaseAdapter();
$this->queryBuilder = Ulmus::queryBuilder($entity);
$queryBuilder = $this->adapter->adapter()->queryBuilderClass();
$this->queryBuilder = new $queryBuilder();
} }
public function __clone() public function __clone()
@ -49,7 +51,7 @@ class Repository
public function loadOne() : ? object 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 public function loadOneFromField($field, $value) : ? object
@ -606,7 +608,7 @@ class Repository
$isRelation = ( $annotation instanceof Relation ) || ($annotation instanceof Attribute\Property\Relation); $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."); 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) { 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 ]) ) { if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ]) ) {
$escAlias = $this->escapeIdentifier($alias); $escAlias = $this->escapeIdentifier($alias);
$fieldName = $this->escapeIdentifier($key);
$name = $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Field::class, Field::class ])->name ?? $field['name']; $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) { 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) { foreach ($order as $item) {
@ -778,17 +783,21 @@ class Repository
$values[] = is_callable($field) ? $field($item) : $item->$entityProperty; $values[] = is_callable($field) ? $field($item) : $item->$entityProperty;
} }
$values = array_unique($values);
$repository->where($key, $values); $repository->where($key, $values);
$results = call_user_func([ $repository, $relation->function() ]); $results = $repository->loadAll();
if ($relation->isOneToOne()) { if ($relation->isOneToOne()) {
$item->$name = $results ?: new $baseEntity(); foreach ($collection as $item) {
$item->$name = $results->searchOne($item->$entityProperty, $property) ?: new $baseEntity();
}
} }
elseif ($relation->isOneToMany()) { elseif ($relation->isOneToMany()) {
foreach ($collection as $item) { foreach ($collection as $item) {
$item->$name = $baseEntity::entityCollection(); $item->$name = $baseEntity::entityCollection();
$item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty)); $item->$name->mergeWith($results->searchAll($item->$entityProperty, $property));
} }
} }
} }

View File

@ -102,23 +102,21 @@ class RelationBuilder
$this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository); $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);
$result = call_user_func([ $this->repository, $relation->function ]); return call_user_func([ $this->repository, $relation->function() ]) ?? $this->instanciateEmptyEntity($name, $relation);
return count($result) === 0 ? $this->instanciateEmptyEntity($name, $relation): $result[0];
case $relation->isOneToMany(): case $relation->isOneToMany():
$this->oneToMany($name, $relation); $this->oneToMany($name, $relation);
$this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository); $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(): case $relation->isManyToMany():
$this->manyToMany($name, $relation, $relationRelation); $this->manyToMany($name, $relation, $relationRelation);
$this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository); $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) { if ($relation->bridgeField ?? false) {
$collection = $relation->bridge::entityCollection(); $collection = $relation->bridge::entityCollection();
@ -152,7 +150,7 @@ class RelationBuilder
$this->repository->open(); $this->repository->open();
foreach($this->wheres as $condition) { 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(); $this->repository->close();
@ -201,7 +199,7 @@ class RelationBuilder
$vars = []; $vars = [];
$len = strlen( $name ) + 1; $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() ) { if ( $isRelation && $annotation->isManyToMany() ) {
$entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity; $entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity;
@ -262,7 +260,13 @@ class RelationBuilder
$field = $relation->key; $field = $relation->key;
if ($relation->foreignKey) { 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 ); $this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), $value );
} }