Compare commits

...

11 Commits

Author SHA1 Message Date
Dave Mc Nicoll
fb644b0b0a - Added withJoin from attributes in RelationBuilder (can't understand why it wasn't already..) 2025-06-03 20:27:15 +00:00
Dave Mc Nicoll
c719e4a6d1 - WIp on Migration for mssql 2025-05-26 18:21:37 +00:00
Dave Mc Nicoll
fb4985160a - WIP on Migration of MsSQL 2025-05-21 18:46:34 +00:00
Dave Mc Nicoll
c54de30069 - Fixed evalClosure() missing type which converted EntityField into string 2025-05-16 12:59:31 +00:00
Dave Mc Nicoll
8fff21ecb0 - Fixed RelationBuilder problem whenever a field wasn't set 2025-05-15 18:09:52 +00:00
Dave Mc Nicoll
cc6048d4ee - WIP on migration and some bug fixes linked to withJoin() select problems 2025-05-15 18:05:23 +00:00
Dave Mc Nicoll
761e0401b0 - Fixed SQLite truncate, added AdapterProxy autoloading 2025-05-12 15:11:00 +00:00
Dave Mc Nicoll
1ec582d1ac - Comparison made before transforming to string was skipping nulls and not nulls but empty 2025-04-03 19:38:48 +00:00
Dave Mc Nicoll
9556dea849 - Fixed a bug where withJoin() would mess with previous select() done on querybuilding
- Fixed a problem related with distinct in select whenever another select on fields was done
2025-03-20 17:31:19 +00:00
Dave Mc Nicoll
bfddf84564 - Added partial support for Index as primary keys 2025-03-11 19:36:00 +00:00
Dave Mc Nicoll
e0ec140661 - Sqlite now handles concurrency with transactions 2025-02-25 14:09:41 +00:00
42 changed files with 641 additions and 282 deletions

View File

@ -1,121 +0,0 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\{ConnectionAdapter, Entity\InformationSchema\Table, Migration\FieldDefinition, Repository, QueryBuilder\Sql\MysqlQueryBuilder, Entity};
trait DefaultAdapterTrait
{
public function repositoryClass() : string
{
return Repository::class;
}
public function queryBuilderClass() : string
{
return MysqlQueryBuilder::class;
}
public function tableSyntax() : array
{
return [
'ai' => "AUTO_INCREMENT",
'pk' => "PRIMARY KEY",
'unsigned' => "UNSIGNED",
];
}
public function databaseName() : string
{
return $this->database;
}
public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object
{
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
{
$type = $field->type;
$length = $field->length;
if ( is_a($type, Entity\Field\Date::class, true) ) {
$type = "DATE";
}
elseif ( is_a($type, Entity\Field\Time::class, true) ) {
$type = "TIME";
}
elseif ( is_a($type, \DateTime::class, true) ) {
$type = "DATETIME";
}
switch($type) {
case "bool":
$type = "TINYINT";
$length = 1;
break;
case "array":
case "string":
if ($length && $length <= 255) {
$type = "VARCHAR";
break;
}
elseif (! $length || ( $length <= 65535 ) ) {
$type = "TEXT";
}
elseif ( $length <= 16777215 ) {
$type = "MEDIUMTEXT";
}
elseif ($length <= 4294967295) {
$type = "LONGTEXT";
}
else {
throw new \Exception("A column with size bigger than 4GB cannot be created.");
}
# Length is unnecessary on TEXT fields
unset($length);
break;
case "float":
$type = "DOUBLE";
break;
default:
$type = strtoupper($type);
break;
}
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));
}
public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable
{
if ($field['previous']) {
$position = sprintf('AFTER %s', $this->escapeIdentifier($field['previous']['field'], AdapterInterface::IDENTIFIER_FIELD));
}
else {
$position = "FIRST";
}
return implode(" ", [
strtoupper($field['action']),
$this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD),
$definition->getSqlType(),
$definition->getSqlParams(),
$position,
]);
}
}

View File

@ -9,10 +9,15 @@ use Ulmus\Exception\AdapterConfigurationException;
use Ulmus\Ulmus; use Ulmus\Ulmus;
use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\FieldDefinition;
use Ulmus\{Entity\InformationSchema\Table, Migration\MigrateInterface, Repository, QueryBuilder}; use Ulmus\{ConnectionAdapter,
Entity\InformationSchema\Table,
Migration\MigrateInterface,
Migration\SqlMigrationTrait,
Repository,
QueryBuilder};
class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
use SqlAdapterTrait; use SqlAdapterTrait, SqlMigrationTrait;
const ALLOWED_ATTRIBUTES = [ const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment', 'default', 'primary_key', 'auto_increment',
@ -205,7 +210,14 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
$this->traceOn = $configuration['trace_on']; $this->traceOn = $configuration['trace_on'];
} }
} }
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$mapper = new MsSQLFieldMapper($field);
return $typeOnly ? $mapper->type : $mapper->render();
}
public static function escapeIdentifier(string $segment, int $type) : string public static function escapeIdentifier(string $segment, int $type) : string
{ {
switch($type) { switch($type) {
@ -235,4 +247,13 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
{ {
return QueryBuilder\Sql\MssqlQueryBuilder::class; return QueryBuilder\Sql\MssqlQueryBuilder::class;
} }
public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object
{
return \Ulmus\Entity\Mssql\Table::repository(Repository::DEFAULT_ALIAS, $adapter)
->select(\Ulmus\Common\Sql::raw('this.*'))
->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->or($this->escapeIdentifier('table_catalog', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
}
} }

View File

@ -0,0 +1,29 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\Migration\FieldDefinition;
class MsSQLFieldMapper extends SqlFieldMapper
{
public function map() : void
{
parent::map();
if (in_array($this->type, [ 'CHAR', 'VARCHAR', 'TEXT', ])) {
$this->type = "N" . $this->type;
};
}
/* @TODO !
public function postProcess() : void
{
if (
in_array($this->type, [ 'BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB', 'JSON', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'GEOMETRY' ]) &&
! is_object($this->field->default ?? false) # Could be a functional default, which would now be valid
) {
unset($this->field->default);
}
}
* */
}

View File

@ -6,6 +6,7 @@ use Ulmus\ConnectionAdapter;
use Ulmus\Entity\Mysql\Table; use Ulmus\Entity\Mysql\Table;
use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\FieldDefinition;
use Ulmus\Migration\MigrateInterface; use Ulmus\Migration\MigrateInterface;
use Ulmus\Migration\SqlMigrationTrait;
use Ulmus\QueryBuilder\Sql; use Ulmus\QueryBuilder\Sql;
use Ulmus\Common\PdoObject; use Ulmus\Common\PdoObject;
@ -13,7 +14,7 @@ use Ulmus\Exception\AdapterConfigurationException;
use Ulmus\Repository; use Ulmus\Repository;
class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
use SqlAdapterTrait; use SqlAdapterTrait, SqlMigrationTrait;
const ALLOWED_ATTRIBUTES = [ const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment', 'update', 'default', 'primary_key', 'auto_increment', 'update',
@ -180,4 +181,26 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName) ->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName); ->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
} }
public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable
{
if ($field['action'] === 'add') {
if (! empty($field['previous'])) {
$position = sprintf('AFTER %s', $this->escapeIdentifier($field['previous']->field, AdapterInterface::IDENTIFIER_FIELD));
}
else {
$position = "FIRST";
}
}
return implode(" ", array_filter([
strtoupper($field['action']),
$this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD),
$definition->getSqlType(),
$definition->getSqlParams(),
$position ?? null,
]));
}
} }

View File

@ -6,10 +6,6 @@ use Ulmus\Migration\FieldDefinition;
class MySQLFieldMapper extends SqlFieldMapper class MySQLFieldMapper extends SqlFieldMapper
{ {
public readonly string $type;
public readonly string $length;
public function map() : void public function map() : void
{ {
$type = $this->field->type; $type = $this->field->type;

View File

@ -7,10 +7,10 @@ use Ulmus\Common\PdoObject;
use Ulmus\ConnectionAdapter; use Ulmus\ConnectionAdapter;
use Ulmus\Entity; use Ulmus\Entity;
use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\FieldDefinition;
use Ulmus\{Migration\MigrateInterface, Repository, QueryBuilder}; use Ulmus\{Migration\MigrateInterface, Migration\SqlMigrationTrait, Repository, QueryBuilder};
class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface { class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
use SqlAdapterTrait; use SqlAdapterTrait, SqlMigrationTrait;
const ALLOWED_ATTRIBUTES = [ const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment', 'collate nocase', 'collate binary', 'collate rtrim', 'default', 'primary_key', 'auto_increment', 'collate nocase', 'collate binary', 'collate rtrim',
@ -27,6 +27,7 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
public function connect() : PdoObject public function connect() : PdoObject
{ {
try { try {
#$pdo = new PdoObject($this->buildDataSourceName(), null, null);
$pdo = new PdoObject\SqlitePdoObject($this->buildDataSourceName(), null, null); $pdo = new PdoObject\SqlitePdoObject($this->buildDataSourceName(), null, null);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);

View File

@ -36,44 +36,11 @@ trait SqlAdapterTrait
return $this->database; return $this->database;
} }
public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object
{
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
{
$mapper = new SqlFieldMapper($field);
return $typeOnly ? $mapper->type : $mapper->render();
}
public function whitelistAttributes(array &$parameters) : void public function whitelistAttributes(array &$parameters) : void
{ {
$parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES)); $parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES));
} }
public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable
{
if (! empty($field['previous']) ) {
$position = sprintf('AFTER %s', $this->escapeIdentifier($field['previous']->field, AdapterInterface::IDENTIFIER_FIELD));
}
else {
$position = "FIRST";
}
return implode(" ", [
strtoupper($field['action']),
$this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD),
$definition->getSqlType(),
$definition->getSqlParams(),
$position,
]);
}
public function writableValue(mixed $value) : mixed public function writableValue(mixed $value) : mixed
{ {
switch (true) { switch (true) {
@ -92,9 +59,4 @@ trait SqlAdapterTrait
return $value; return $value;
} }
public function splitAlterQuery() : bool
{
return false;
}
} }

View File

@ -7,9 +7,9 @@ use Ulmus\Entity;
class SqlFieldMapper class SqlFieldMapper
{ {
public readonly string $type; public string $type;
public readonly string $length; public string $length;
public function __construct( public function __construct(
public FieldDefinition $field, public FieldDefinition $field,

View File

@ -10,6 +10,12 @@ class Attribute
public static function handleArrayField(null|\Stringable|string|array $field, null|string|bool $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed public static function handleArrayField(null|\Stringable|string|array $field, null|string|bool $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed
{ {
if ( is_array($field) ) { if ( is_array($field) ) {
if (count($field) < 2) {
throw new \RuntimeException(
sprintf("Array field must be formed of at least two things, a class name, and a property. (received %s)", json_encode($field))
);
}
$class = array_shift($field); $class = array_shift($field);
$field[1] ??= $alias; $field[1] ??= $alias;

View File

@ -11,4 +11,4 @@ class Index {
public IndexTypeEnum $type = IndexTypeEnum::Unique, public IndexTypeEnum $type = IndexTypeEnum::Unique,
public null|string $name = null, public null|string $name = null,
) {} ) {}
} }

View File

@ -6,6 +6,5 @@ namespace Ulmus\Attribute\Property;
class Filter { class Filter {
public function __construct( public function __construct(
public string $method = "" public string $method = ""
) ) {}
{}
} }

View File

@ -3,6 +3,7 @@
namespace Ulmus\Attribute\Property; namespace Ulmus\Attribute\Property;
use Ulmus\Attribute\Attribute; use Ulmus\Attribute\Attribute;
use Ulmus\Repository;
#[\Attribute] #[\Attribute]
class Join implements ResettablePropertyInterface { class Join implements ResettablePropertyInterface {
@ -16,4 +17,20 @@ class Join implements ResettablePropertyInterface {
$this->key = Attribute::handleArrayField($this->key); $this->key = Attribute::handleArrayField($this->key);
$this->foreignKey = Attribute::handleArrayField($this->foreignKey); $this->foreignKey = Attribute::handleArrayField($this->foreignKey);
} }
## AWAITING THE Closures in constant expression from PHP 8.5 !
public function foreignKey(? Repository $repository = null) : mixed
{
if (is_string($this->foreignKey)) {
if (str_starts_with($this->foreignKey, 'fn(') && str_contains($this->foreignKey, '=>')) {
$closure = eval("return {$this->foreignKey};");
return $repository ? $closure($repository) : $closure;
}
return $this->entity::field($this->foreignKey, $this->alias);
}
return $this->foreignKey;
}
} }

View File

@ -3,6 +3,7 @@
namespace Ulmus\Attribute\Property; namespace Ulmus\Attribute\Property;
use Ulmus\Attribute\Attribute; use Ulmus\Attribute\Attribute;
use Ulmus\Repository;
#[\Attribute(\Attribute::TARGET_PROPERTY)] #[\Attribute(\Attribute::TARGET_PROPERTY)]
class Relation implements ResettablePropertyInterface { class Relation implements ResettablePropertyInterface {
@ -21,6 +22,7 @@ class Relation implements ResettablePropertyInterface {
public null|string $entity = null, public null|string $entity = null,
public null|string $join = null, public null|string $join = null,
public null|string $function = null, public null|string $function = null,
public null|string $alias = 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);
@ -83,4 +85,20 @@ class Relation implements ResettablePropertyInterface {
{ {
return (bool) $this->bridge; return (bool) $this->bridge;
} }
## AWAITING THE Closures in constant expression from PHP 8.5 !
public function foreignKey(? Repository $repository = null) : mixed
{
if (is_string($this->foreignKey)) {
if (str_starts_with($this->foreignKey, 'fn(') && str_contains($this->foreignKey, '=>')) {
$closure = eval("return {$this->foreignKey};");
return $repository ? $closure($repository) : $closure;
}
return $this->entity::field($this->foreignKey, $this->alias);
}
return $this->foreignKey;
}
} }

View File

@ -10,6 +10,7 @@ class Virtual extends Field implements ResettablePropertyInterface {
public function __construct( public function __construct(
public null|string|array $method = null, public null|string|array $method = null,
public ? \Closure $closure = null, public ? \Closure $closure = null,
public null|string $name = null,
) { ) {
$this->method ??= [ static::class, 'noop' ]; $this->method ??= [ static::class, 'noop' ];
} }

View File

@ -5,6 +5,6 @@ namespace Ulmus\Attribute\Property;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class WithJoin { class WithJoin {
public function __construct( public function __construct(
array $joins = [] public array $joins = []
) {} ) {}
} }

View File

@ -5,6 +5,7 @@ namespace Ulmus\Common;
use Notes\Common\ReflectedClass; use Notes\Common\ReflectedClass;
use Notes\Common\ReflectedProperty; use Notes\Common\ReflectedProperty;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Ulmus\Attribute\Obj\Index;
use Ulmus\Ulmus, use Ulmus\Ulmus,
Ulmus\Attribute\Obj\Table, Ulmus\Attribute\Obj\Table,
Ulmus\Attribute\Obj\AdapterAttributeInterface, Ulmus\Attribute\Obj\AdapterAttributeInterface,
@ -113,7 +114,12 @@ class EntityResolver {
throw new \InvalidArgumentException("Can't find entity relation's column named `$name` from entity {$this->entityClass}"); throw new \InvalidArgumentException("Can't find entity relation's column named `$name` from entity {$this->entityClass}");
} }
} }
public function getPropertyEntityType(string $name) : false|string
{
return $this->reflectedClass->getProperties(true)[$name]->getTypes()[0]->type ?? false;
}
public function searchFieldAnnotation(string $field, array|object|string $annotationType, bool $caseSensitive = true) : ? object public function searchFieldAnnotation(string $field, array|object|string $annotationType, bool $caseSensitive = true) : ? object
{ {
return $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive)[0] ?? null; return $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive)[0] ?? null;
@ -214,9 +220,9 @@ class EntityResolver {
return null; return null;
} }
public function getCompoundKeyFields() : ? array public function getCompoundKeyFields() : ? Index
{ {
return null; return $this->getAttributeImplementing(Index::class);
} }
public function getUniqueFields() : ? array public function getUniqueFields() : ? array

View File

@ -56,7 +56,7 @@ class PdoObject extends PDO {
} }
} }
catch (\Throwable $e) { catch (\Throwable $e) {
throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters)); throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters), (int) $e->getCode(), $e);
} }
return null; return null;
@ -83,7 +83,7 @@ class PdoObject extends PDO {
$this->lastInsertId = null; $this->lastInsertId = null;
try { try {
if ( ! $this->inTransaction() ) { if (! $this->inTransaction()) {
$this->beginTransaction(); $this->beginTransaction();
} }
@ -113,8 +113,6 @@ class PdoObject extends PDO {
catch (\Throwable $e) { catch (\Throwable $e) {
throw $e; throw $e;
} }
return null;
} }
protected function bindVariables(PDOStatement $statement, array &$parameters) : void protected function bindVariables(PDOStatement $statement, array &$parameters) : void

View File

@ -12,7 +12,7 @@ class SqlPdoObject extends \Ulmus\Common\PdoObject
return $this->openTransaction(); return $this->openTransaction();
} }
return $this->exec('SAVEPOINT transaction_'.$this->openedTransaction) !== false; return $this->exec("SAVEPOINT transaction_{$this->openedTransaction}") !== false;
} }
public function commit() : bool public function commit() : bool
@ -21,16 +21,19 @@ class SqlPdoObject extends \Ulmus\Common\PdoObject
return parent::commit(); return parent::commit();
} }
return false; return $this->exec("RELEASE SAVEPOINT transaction_{$this->openedTransaction}") !== false;
} }
public function rollback() : bool public function rollback() : bool
{ {
if (0 !== $this->openedTransaction) { if ($this->openedTransaction > 1) {
return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false; return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false;
} }
elseif ($this->openedTransaction-- === 1) {
return parent::rollback();
}
return parent::rollback(); return false;
} }
protected function openTransaction() : bool protected function openTransaction() : bool

View File

@ -17,6 +17,19 @@ class SqlitePdoObject extends SqlPdoObject
return $this->exec("COMMIT") !== false; return $this->exec("COMMIT") !== false;
} }
return $this->exec("RELEASE SAVEPOINT transaction_{$this->openedTransaction}") !== false;
}
public function rollback() : bool
{
if ($this->openedTransaction > 1) {
return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false;
}
elseif ($this->openedTransaction-- === 1) {
# We must do it manually since opening a transaction manually stucks PDO into thinking we've got no transaction opened
return $this->exec('ROLLBACK') !== false;
}
return false; return false;
} }

View File

@ -2,12 +2,27 @@
namespace Ulmus\Container; namespace Ulmus\Container;
use Ulmus\ConnectionAdapter;
class AdapterProxy class AdapterProxy
{ {
public array $connections = []; public array $connections = [];
public function __construct(...$arguments) public function __construct(...$arguments)
{ {
$this->connections = $arguments; $this->push(array_filter($arguments));
}
public function push(array|ConnectionAdapter $connection) : void
{
foreach((array) $connection as $adapter) {
foreach($this->connections as $existing) {
if ($adapter === $existing) {
continue 2;
}
}
$this->connections[] = $adapter;
}
} }
} }

View File

@ -56,7 +56,10 @@ class DatasetHandler
} }
elseif ( $field->expectType('array') ) { elseif ( $field->expectType('array') ) {
if ( is_string($value)) { if ( is_string($value)) {
if (substr($value, 0, 1) === "a") { if (empty($value)) {
yield $field->name => [];
}
elseif (substr($value, 0, 1) === "a") {
yield $field->name => unserialize($value); yield $field->name => unserialize($value);
} }
@ -108,15 +111,17 @@ class DatasetHandler
return $unmatched; return $unmatched;
} }
public function pullRelation(object $entity) : Generator public function pullRelation(object $entity, bool $onlyPreviouslyLoaded = true) : Generator
{ {
foreach($this->entityResolver->reflectedClass->getProperties(true) as $name => $field){ foreach($this->entityResolver->reflectedClass->getProperties(true) as $name => $field) {
$relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] ); $relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] );
if ($relation) { if ($relation) {
$ignore = $this->entityResolver->searchFieldAnnotation($name, [ Relation\Ignore::class ] ); $ignore = $this->entityResolver->searchFieldAnnotation($name, [ Relation\Ignore::class ] );
if ($ignore && $ignore->ignoreExport) { $load = $onlyPreviouslyLoaded ? (new \ReflectionProperty($entity::class, $name))->isInitialized($entity) : true;
if ( ! $load || ( $ignore && $ignore->ignoreExport ) ) {
if ( $relation->isOneToOne() ) { if ( $relation->isOneToOne() ) {
# @TODO TO INCLUDED INTO getTypes() RETURNED CLASS WHEN DONE ! # @TODO TO INCLUDED INTO getTypes() RETURNED CLASS WHEN DONE !
yield $name => ( new \ReflectionClass($field->getTypes()[0]) )->newInstanceWithoutConstructor(); yield $name => ( new \ReflectionClass($field->getTypes()[0]) )->newInstanceWithoutConstructor();
@ -129,9 +134,10 @@ class DatasetHandler
continue; continue;
} }
# @TODO Must fix recursive bug.. this last check is way too basic to work
if ( $entity->__isset($name) && ($relation->entity ?? $relation->bridge) !== static::class ) { if ( $entity->__isset($name) && ($relation->entity ?? $relation->bridge) !== static::class ) {
if ( null !== $value = $entity->__isset($name) ?? null ) { $value = $entity->$name ?? null;
if ( null !== $value ) {
if ( is_iterable($value) ) { if ( is_iterable($value) ) {
$list = []; $list = [];

View File

@ -13,9 +13,6 @@ class Column
{ {
use \Ulmus\EntityTrait; use \Ulmus\EntityTrait;
#[Field\Id]
public ? id $srs_id;
#[Field(name: "TABLE_CATALOG", length: 512)] #[Field(name: "TABLE_CATALOG", length: 512)]
public string $tableCatalog; public string $tableCatalog;
@ -64,38 +61,27 @@ class Column
# #[Field(name: "COLLATION_TYPE", type: "longtext")] # #[Field(name: "COLLATION_TYPE", type: "longtext")]
# public string $type; # public string $type;
#[Field(name: "COLUMN_KEY", length: 3)]
public string $key;
#[Field(name: "EXTRA", length: 30)]
public string $extra;
#[Field(name: "PRIVILEGES", length: 80)]
public string $privileges;
#[Field(name: "COLUMN_COMMENT", length: 1024)]
public string $comment;
# #[Field(name: "IS_GENERATED", length: 6)] # #[Field(name: "IS_GENERATED", length: 6)]
# public string $generated; # public string $generated;
#[Field(name: "GENERATION_EXPRESSION", type: "longtext")]
public ? string $generationExpression;
public function matchFieldDefinition(ReflectedProperty $definition) : bool public function matchFieldDefinition(ReflectedProperty $definition) : bool
{ {
$nullable = $this->nullable === 'YES'; $nullable = $this->nullable === 'YES';
$definitionValue = $this->getDefinitionValue($definition);
if ($nullable !== $definition->allowsNull()) { if ($nullable !== $definition->allowsNull()) {
return false; return false;
} }
if ( isset($definition->value) && $this->canHaveDefaultValue() ) { if ( isset($definitionValue) && $this->canHaveDefaultValue() ) {
if ( $definition->value !== $this->defaultValue()) { if ( $definitionValue !== $this->defaultValue() ) {
# dump($definition->value, $this->defaultValue(), $definition->name);
return false; return false;
} }
} }
elseif (! isset($definition->value)) { elseif (! isset($definitionValue)) {
if ( ! $this->defaultValueIsNull() ) { if ( ! $this->defaultValueIsNull() ) {
return false; return false;
} }
@ -104,6 +90,12 @@ class Column
return true; return true;
} }
protected function getDefinitionValue(ReflectedProperty $definition) : mixed
{
# Attribute's value first, then defined in class value
return $definition->getAttribute(Field::class)->object->default ?? $definition->value ?? null;
}
protected function defaultValueIsNull() : bool protected function defaultValueIsNull() : bool
{ {
return $this->defaultValue() === null; return $this->defaultValue() === null;

View File

@ -0,0 +1,85 @@
<?php
namespace Ulmus\Entity\Mssql;
use Ulmus\Attribute\Obj\Table as TableObj;
use Ulmus\Attribute\Property\Field;
use Ulmus\ConnectionAdapter;
use Ulmus\Entity\Mysql\id;
use Ulmus\Repository;
use Ulmus\Ulmus;
#[TableObj(name: "columns", schema: "information_schema")]
class Column extends \Ulmus\Entity\InformationSchema\Column
{
/*
public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null): Repository
{
$adapter = Ulmus::$registeredAdapters[$this->loadedFromAdapter];
return Ulmus::repository(static::class, $alias, $adapter);
}*/
# TODO ! Handle FUNCTIONAL default value
protected function canHaveDefaultValue(): bool
{
return ! in_array(strtoupper($this->dataType), [
# 'BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB', 'JSON', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'GEOMETRY'
]);
}
protected function defaultValue() : mixed
{
if ($this->default === null) {
return null;
}
# Removing first pairs of brackets
$default = $this->isBetween($this->default, '(', ')') ? substr($this->default, 1, -1) : $this->default;
if ($default === "NULL") {
return null;
}
# Checking if another pairs of brackets surrounds the value
if ($this->isBetween($default, '(', ')')) {
$default = substr($default, 1, -1);
if (is_numeric($default)) {
return (int) $default;
}
}
elseif ($this->isBetween($default, "'", "'")) {
$default = new class($default) implements \Stringable {
public function __construct(
private readonly string $default
) {}
public function __toString(): string
{
return substr($this->default, 1, -1);;
}
};
}
else {
$default = new class($default) implements \Stringable {
public function __construct(
private readonly string $default
) {}
public function __toString(): string
{
return $this->default;
}
};
}
return (string) $default;
}
private function isBetween(string $str, string $first, string $last) : bool
{
return str_starts_with($str, $first) && str_ends_with($str, $last);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Ulmus\Entity\Mssql;
use Ulmus\EntityCollection,
Ulmus\Entity\Field\Datetime;
use Ulmus\{Attribute\Obj\Table as TableObj, Ulmus};
use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where};
use Ulmus\Repository;
#[TableObj(name: "tables", schema: "information_schema")]
class Table extends \Ulmus\Entity\InformationSchema\Table
{
#[Relation(type: "oneToMany", key: "name", foreignKey: [ Column::class, 'tableName' ], entity: Column::class)]
#[Where('TABLE_SCHEMA', generateValue: [ Table::class, 'getSchema' ])]
##[Filter(method: "filterColumns")]
public EntityCollection $columns;
public function filterColumns() : Repository
{
$adapter = Ulmus::$registeredAdapters[$this->loadedFromAdapter];
return Column::repository(Repository\MssqlRepository::DEFAULT_ALIAS, $adapter);
}
}

View File

@ -2,8 +2,28 @@
namespace Ulmus\Entity\Mysql; namespace Ulmus\Entity\Mysql;
use Ulmus\Attribute\Property\Field;
class Column extends \Ulmus\Entity\InformationSchema\Column class Column extends \Ulmus\Entity\InformationSchema\Column
{ {
#[Field\Id]
public ? id $srs_id;
#[Field(name: "COLUMN_KEY", length: 3)]
public string $key;
#[Field(name: "EXTRA", length: 30)]
public string $extra;
#[Field(name: "PRIVILEGES", length: 80)]
public string $privileges;
#[Field(name: "COLUMN_COMMENT", length: 1024)]
public string $comment;
#[Field(name: "GENERATION_EXPRESSION", type: "longtext")]
public ? string $generationExpression;
# TODO ! Handle FUNCTIONAL default value # TODO ! Handle FUNCTIONAL default value
protected function canHaveDefaultValue(): bool protected function canHaveDefaultValue(): bool
{ {
@ -12,4 +32,5 @@ class Column extends \Ulmus\Entity\InformationSchema\Column
]); ]);
} }
} }

View File

@ -343,7 +343,20 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
$list[] = $entity instanceof \JsonSerializable ? $entity->jsonSerialize() : $entity->toArray($includeRelations); $list[] = $entity instanceof \JsonSerializable ? $entity->jsonSerialize() : $entity->toArray($includeRelations);
} }
return $list; return $this->utf8ize($list, "UTF-8", "UTF-8");
}
protected function utf8ize(mixed $content) {
if (is_array($content)) {
foreach ($content as $key => $value) {
$content[$key] = $this->utf8ize($value);
}
}
elseif (is_string($content)) {
return mb_convert_encoding($content, "UTF-8", "UTF-8");
}
return $content;
} }
public function fromArray(array $datasets, ? string /*stringable*/ $entityClass = null) : self public function fromArray(array $datasets, ? string /*stringable*/ $entityClass = null) : self

View File

@ -34,6 +34,9 @@ trait EntityTrait {
#[Ignore] #[Ignore]
protected DatasetHandler $datasetHandler; protected DatasetHandler $datasetHandler;
#[Ignore]
public string $loadedFromAdapter;
#[Ignore] #[Ignore]
public function __construct(iterable|null $dataset = null) public function __construct(iterable|null $dataset = null)
{ {
@ -43,12 +46,12 @@ trait EntityTrait {
#[Ignore] #[Ignore]
public function initializeEntity(iterable|null $dataset = null) : void public function initializeEntity(iterable|null $dataset = null) : void
{ {
$this->datasetHandler = new DatasetHandler(static::resolveEntity(), $this->entityStrictFieldsDeclaration);
if ($dataset) { if ($dataset) {
$this->fromArray($dataset); $this->fromArray($dataset);
} }
$this->datasetHandler = new DatasetHandler(static::resolveEntity(), $this->entityStrictFieldsDeclaration);
$this->resetVirtualProperties(); $this->resetVirtualProperties();
} }
@ -144,17 +147,31 @@ trait EntityTrait {
return false; return false;
} }
if ( null === $pkField = $this->resolveEntity()->getPrimaryKeyField($this) ) { if ( null === $pkField = $this->resolveEntity()->getPrimaryKeyField() ) {
throw new Exception\EntityPrimaryKeyUnknown(sprintf("Entity %s has no field containing attributes 'primary_key'", static::class)); if ( null !== $compoundKeyFields = $this->resolveEntity()->getCompoundKeyFields() ) {
$loaded = false;
foreach ($compoundKeyFields->column as $column) {
$field = $this->resolveEntity()->field($column);
if (! $field->allowsNull() ) {
$loaded |= isset($this->{$field->name});
}
}
return $loaded;
};
}
else {
$key = key($pkField);
return isset($this->$key);
} }
$key = key($pkField); throw new Exception\EntityPrimaryKeyUnknown(sprintf("Entity %s has no field containing attributes 'primary_key'", static::class));
return isset($this->$key);
} }
#[Ignore] #[Ignore]
public function __get(string $name) public function __get(string $name) : mixed
{ {
$relation = new Repository\RelationBuilder($this); $relation = new Repository\RelationBuilder($this);

View File

@ -5,4 +5,4 @@ namespace Ulmus\Migration\Sql;
class SqliteMigration class SqliteMigration
{ {
} }

View File

@ -0,0 +1,47 @@
<?php
namespace Ulmus\Migration;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Adapter\SqlFieldMapper;
use Ulmus\ConnectionAdapter;
use Ulmus\Entity\Mysql\Table;
use Ulmus\Repository;
trait SqlMigrationTrait
{
protected array $QUERY_REPLACE = [
'change' => "alter",
];
public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object
{
return \Ulmus\Entity\InformationSchema\Table::repository(Repository::DEFAULT_ALIAS, $adapter)
->select(\Ulmus\Common\Sql::raw('this.*'))
->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->or($this->escapeIdentifier('table_catalog', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
}
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$mapper = new SqlFieldMapper($field);
return $typeOnly ? $mapper->type : $mapper->render();
}
public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable
{
return implode(" ", array_filter([
strtoupper($field['action']),
$this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD),
$definition->getSqlType(),
$definition->getSqlParams(),
]));
}
public function splitAlterQuery() : bool
{
return false;
}
}

View File

@ -12,6 +12,8 @@ class Select extends Fragment {
public bool $union = false; public bool $union = false;
public bool $isInternalSelect = false;
public ? int $top = null; public ? int $top = null;
protected array $fields = []; protected array $fields = [];

View File

@ -0,0 +1,28 @@
<?php
namespace Ulmus\Query\Sqlite;
use Ulmus\Query\Fragment;
class Truncate extends Fragment {
const SQL_TOKEN = "DELETE FROM";
public int $order = -100;
public string $table;
public function set(string $tableName) : self
{
$this->table = $tableName;
return $this;
}
public function render() : string
{
return $this->renderSegments([
static::SQL_TOKEN, $this->table,
]);
}
}

View File

@ -59,7 +59,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
$this->push($select); $this->push($select);
} }
$select->distinct = $distinct; $select->distinct = $select->distinct || $distinct;
return $this; return $this;
} }

View File

@ -3,6 +3,7 @@
namespace Ulmus\QueryBuilder\Sql; namespace Ulmus\QueryBuilder\Sql;
use Ulmus\Query; use Ulmus\Query;
use Ulmus\Adapter\AdapterInterface;
# Soon to extends SqlQueryBuilder # Soon to extends SqlQueryBuilder
class SqliteQueryBuilder extends MysqlQueryBuilder class SqliteQueryBuilder extends MysqlQueryBuilder
@ -38,4 +39,41 @@ class SqliteQueryBuilder extends MysqlQueryBuilder
return $this; return $this;
} }
public function insert(array $fieldlist, string $table, ? string $alias = null, ? string $database = null, ? string $schema = null, bool $replace = false) : self
{
return parent::insert($fieldlist, $table, $alias, null, null, $replace);
}
public function update(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self
{
return parent::update($table, $alias, null, null);
}
public function create(AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
{
return parent::create($adapter, $fieldlist, $table, null, null);
}
public function alter(AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
{
return parent::alter($adapter, $fieldlist, $table, null, null);
}
public function from(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self
{
return parent::from($table, $alias, null, null);
}
public function truncate(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self
{
$truncate = new Query\Sqlite\Truncate($this);
$this->push($truncate);
$truncate->set($table);
return $this;
}
} }

View File

@ -5,7 +5,7 @@ namespace Ulmus\QueryBuilder;
use Ulmus\Query\QueryFragmentInterface; use Ulmus\Query\QueryFragmentInterface;
# TODO -> Extract from MysqlQueryBuilder to build an ISO/IEC 9075:2023 compatible layer for a basic SQL QueryBuilder # TODO -> Extract from MysqlQueryBuilder to build an ISO/IEC 9075:2023 compatible layer for a basic SQL QueryBuilder
class SqlQueryBuilder implements QueryBuilderInterface abstract class SqlQueryBuilder implements QueryBuilderInterface
{ {
protected string $rendered; protected string $rendered;

View File

@ -6,6 +6,7 @@ use Ulmus\Attribute\Property\{
Field, OrderBy, Where, Having, Relation, Filter, Join, FilterJoin, WithJoin Field, OrderBy, Where, Having, Relation, Filter, Join, FilterJoin, WithJoin
}; };
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Common\EntityResolver; use Ulmus\Common\EntityResolver;
use Ulmus\Entity\EntityInterface; use Ulmus\Entity\EntityInterface;
use Ulmus\Repository\RepositoryInterface; use Ulmus\Repository\RepositoryInterface;
@ -25,7 +26,7 @@ class Repository implements RepositoryInterface
public ? ConnectionAdapter $adapters; public ? ConnectionAdapter $adapters;
public readonly QueryBuilder\QueryBuilderInterface $queryBuilder; public QueryBuilder\QueryBuilderInterface $queryBuilder;
protected EntityResolver $entityResolver; protected EntityResolver $entityResolver;
@ -36,6 +37,11 @@ class Repository implements RepositoryInterface
$this->alias = $alias; $this->alias = $alias;
$this->entityResolver = Ulmus::resolveEntity($entity); $this->entityResolver = Ulmus::resolveEntity($entity);
$this->setAdapter($adapter);
}
public function setAdapter(? ConnectionAdapter $adapter = null) : void {
if ($adapter) { if ($adapter) {
$this->adapter = $adapter; $this->adapter = $adapter;
@ -97,7 +103,7 @@ class Repository implements RepositoryInterface
else { else {
$pk = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); $pk = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
if (count($pk) === 1) { if ($pk && count($pk) === 1) {
$field = key($pk); $field = key($pk);
$this->select(Common\Sql::function('COUNT', Common\Sql::raw("DISTINCT " . $this->entityClass::field($field)))); $this->select(Common\Sql::function('COUNT', Common\Sql::raw("DISTINCT " . $this->entityClass::field($field))));
@ -216,7 +222,12 @@ class Repository implements RepositoryInterface
} }
else { else {
if ( $primaryKeyDefinition === null ) { if ( $primaryKeyDefinition === null ) {
throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); if ( null !== $compoundKeyFields = Ulmus::resolveEntity($this->entityClass)->getCompoundKeyFields() ) {
throw new \Exception("TO DO!");
}
else {
throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass));
}
} }
$diff = $fieldsAndValue ?? $this->generateWritableDataset($entity); $diff = $fieldsAndValue ?? $this->generateWritableDataset($entity);
@ -319,6 +330,10 @@ class Repository implements RepositoryInterface
} }
} }
if (is_null($e1) || is_null($e2)) {
return $e1 !== $e2;
}
return (string) $e1 !== (string) $e2; return (string) $e1 !== (string) $e2;
}); });
} }
@ -407,59 +422,58 @@ class Repository implements RepositoryInterface
public function withJoin(string|array $fields, array $options = []) : self public function withJoin(string|array $fields, array $options = []) : self
{ {
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { $selectObj = $this->queryBuilder->getFragment(Query\Select::class);
$canSelect = empty($selectObj) || $selectObj->isInternalSelect === true;
if ( $canSelect ) {
$select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true); $select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
$this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select))); $this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select)));
} }
# @TODO Apply FILTER annotation to this too ! # @TODO Apply FILTER annotation to this too !
foreach(array_filter((array) $fields) as $item) { foreach(array_filter((array) $fields, fn($e) => $e && ! isset($this->joined[$e]) ) as $item) {
if ( isset($this->joined[$item]) ) { $this->joined[$item] = true;
continue;
}
else {
$this->joined[$item] = true;
}
$attribute = $this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?: $attribute = $this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?:
$this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]); $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]);
$isRelation = ( $attribute instanceof Relation ) || ($attribute instanceof Relation); $isRelation = $attribute instanceof Relation;
if ($isRelation && ( $attribute->isManyToMany() )) { if ($isRelation && ( $attribute->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.");
} }
if ( $attribute ) { if ( $attribute ) {
$alias = $attribute->alias ?? $item; $attribute->alias ??= $item;
$entity = $attribute->entity ?? $this->entityResolver->reflectedClass->getProperties(true)[$item]->getTypes()[0]->type; $attribute->entity ??= $this->entityResolver->getPropertyEntityType($item);
foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { foreach($attribute->entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) { if ( null === $attribute->entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) {
$escAlias = $this->escapeIdentifier($alias); $escAlias = $this->escapeIdentifier($attribute->alias);
$fieldName = $this->escapeIdentifier($key); $fieldName = $this->escapeIdentifier($key);
$name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name; $name = $attribute->entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name;
if ($canSelect) {
$this->select("$escAlias.$fieldName as $alias\${$name}"); $this->select("$escAlias.$fieldName as {$attribute->alias}\${$name}");
}
} }
} }
$this->open(); $this->open();
if ( ! in_array(WithOptionEnum::SkipWhere, $options)) { if ( ! in_array(WithOptionEnum::SkipWhere, $options)) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) { foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) {
if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) { if ( is_object($condition->field) && ( $condition->field->entityClass !== $attribute->entity ) ) {
$this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); $this->where(is_object($condition->field) ? $condition->field : $attribute->entity::field($condition->field), $condition->getValue(), $condition->operator);
}
} }
} }
}
if ( ! in_array(WithOptionEnum::SkipHaving, $options)) { if ( ! in_array(WithOptionEnum::SkipHaving, $options)) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) { foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) {
$this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); $this->having(is_object($condition->field) ? $condition->field : $attribute->entity::field($condition->field), $condition->getValue(), $condition->operator);
} }
} }
@ -472,10 +486,9 @@ class Repository implements RepositoryInterface
$this->close(); $this->close();
$key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key; $key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key;
$foreignKey = $this->evalClosure($attribute->foreignKey, $attribute->entity, $attribute->alias);
$foreignKey = is_string($attribute->foreignKey) ? $entity::field($attribute->foreignKey, $alias) : $attribute->foreignKey; $this->join($isRelation ? "LEFT" : $attribute->type, $attribute->entity::resolveEntity()->tableName(), $key, $attribute->foreignKey($this), $attribute->alias, function($join) use ($item, $attribute, $options) {
$this->join("LEFT", $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $options) {
if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) { if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) { foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) {
if ( ! is_object($condition->field) ) { if ( ! is_object($condition->field) ) {
@ -486,10 +499,10 @@ class Repository implements RepositoryInterface
} }
# Adding directly # Adding directly
if ( $field->entityClass === $entity ) { if ( $field->entityClass === $attribute->entity ) {
$field->alias = $alias; $field->alias = $attribute->alias;
$join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->getValue(), $condition->operator); $join->where(is_object($field) ? $field : $attribute->entity::field($field, $attribute->alias), $condition->getValue(), $condition->operator);
} }
} }
} }
@ -506,6 +519,12 @@ class Repository implements RepositoryInterface
} }
} }
if ($canSelect) {
if ( $selectObj ??= $this->queryBuilder->getFragment(Query\Select::class) ) {
$selectObj->isInternalSelect = true;
}
}
return $this; return $this;
} }
@ -523,7 +542,7 @@ class Repository implements RepositoryInterface
# Apply FILTER annotation to this too ! # Apply FILTER annotation to this too !
foreach(array_filter((array) $fields) as $item) { foreach(array_filter((array) $fields) as $item) {
if ( $relation = $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]) ) { if ( $relation = $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]) ) {
$alias = $relation->alias ?? $item; $relation->alias ??= $item;
if ( $relation->isManyToMany() ) { if ( $relation->isManyToMany() ) {
$entity = $relation->bridge; $entity = $relation->bridge;
@ -532,13 +551,14 @@ class Repository implements RepositoryInterface
extract(Repository\RelationBuilder::relationAnnotations($item, $relation)); extract(Repository\RelationBuilder::relationAnnotations($item, $relation));
$repository->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $relation->bridgeField), $relationRelation->entity::field($relationRelation->foreignKey, $alias), $relation->bridgeField) $repository->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $relation->bridgeField), $relationRelation->entity::field($relationRelation->foreignKey, $relation->alias), $relation->bridgeField)
->where( $entity::field($bridgeRelation->key, $relation->bridgeField), $entity::field($bridgeRelation->foreignKey, $this->alias)) ->where( $entity::field($bridgeRelation->key, $relation->bridgeField), $entity::field($bridgeRelation->foreignKey, $this->alias))
->selectJsonEntity($relationRelation->entity, $alias)->open(); ->selectJsonEntity($relationRelation->entity, $relation->alias)->open();
} }
else { else {
$entity = $relation->entity ?? $this->entityResolver->properties[$item]['type']; $relation->entity ??= $this->entityResolver->getPropertyEntityType($name);
$repository = $entity::repository()->selectJsonEntity($entity, $alias)->open(); $entity = $relation->entity;
$repository = $relation->entity::repository()->selectJsonEntity($entity, $relation->alias)->open();
} }
# $relation->isManyToMany() and $repository->selectJsonEntity($relation->bridge, $relation->bridgeField, true); # $relation->isManyToMany() and $repository->selectJsonEntity($relation->bridge, $relation->bridgeField, true);
@ -547,16 +567,21 @@ class Repository implements RepositoryInterface
$repository->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); $repository->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
} }
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ] ) as $condition) { foreach($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ] ) as $condition) {
$repository->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); $repository->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
} }
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Filter::class ]) as $filter) {
call_user_func_array([$this->entityClass, $filter->method], [$this, $item, true]);
}
$repository->close(); $repository->close();
$key = is_string($relation->key) ? $this->entityClass::field($relation->key) : $relation->key; $key = is_string($relation->key) ? $this->entityClass::field($relation->key) : $relation->key;
if (! $relation->isManyToMany() ) { if (! $relation->isManyToMany() ) {
$foreignKey = is_string($relation->foreignKey) ? $entity::field($relation->foreignKey, $alias) : $relation->foreignKey; $foreignKey = is_string($relation->foreignKey) ? $entity::field($relation->foreignKey, $relation->alias) : $relation->foreignKey;
$repository->where( $foreignKey, $key); $repository->where( $foreignKey, $key);
} }
@ -577,8 +602,9 @@ class Repository implements RepositoryInterface
if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] ))) { if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] ))) {
$order = $this->entityResolver->searchFieldAnnotationList($name, [ OrderBy::class ]); $order = $this->entityResolver->searchFieldAnnotationList($name, [ OrderBy::class ]);
$where = $this->entityResolver->searchFieldAnnotationList($name, [ Where::class ]); $where = $this->entityResolver->searchFieldAnnotationList($name, [ Where::class ]);
$filters = $this->entityResolver->searchFieldAnnotationList($name, [ Filter::class ]);
$baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type']; $baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->getPropertyEntityType($name);
$baseEntityResolver = $baseEntity::resolveEntity(); $baseEntityResolver = $baseEntity::resolveEntity();
$property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name']; $property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name'];
@ -597,6 +623,10 @@ class Repository implements RepositoryInterface
$repository->where($condition->field, $condition->getValue(/* why repository sent here ??? $this */), $condition->operator, $condition->condition); $repository->where($condition->field, $condition->getValue(/* why repository sent here ??? $this */), $condition->operator, $condition->condition);
} }
foreach ($filters as $filter) {
call_user_func_array([ $this->entityClass, $filter->method ], [ $repository, $name, true ]);
}
foreach ($order as $item) { foreach ($order as $item) {
$repository->orderBy($item->field, $item->order); $repository->orderBy($item->field, $item->order);
} }
@ -615,7 +645,8 @@ class Repository implements RepositoryInterface
$repository->where($key, $values); $repository->where($key, $values);
$results = $repository->loadAll(); $loadMethod = $relation->isOneToOne() ? 'loadAll' : $relation->function();
$results = $repository->{$loadMethod}();
if ($relation->isOneToOne()) { if ($relation->isOneToOne()) {
foreach ($collection as $item) { foreach ($collection as $item) {
@ -625,13 +656,30 @@ class Repository implements RepositoryInterface
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->searchAll($item->$entityProperty, $property)); $search = $results->searchAll($item->$entityProperty, $property);
$item->$name->mergeWith($search);
} }
} }
} }
} }
} }
## AWAITING THE Closures in constant expression from PHP 8.5 !
protected function evalClosure(mixed $content, string $entityClass, mixed $alias = self::DEFAULT_ALIAS) : mixed
{
if (is_string($content)) {
if ( str_starts_with($content, 'fn(') ) {
$closure = eval("return $content;");
return $closure($this);
}
return $entityClass::field($content, $alias);
}
return $content;
}
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self
{ {
if ($count) { if ($count) {
@ -736,7 +784,15 @@ class Repository implements RepositoryInterface
public function instanciateEntity(? string $entityClass = null) : object public function instanciateEntity(? string $entityClass = null) : object
{ {
$entity = ( new \ReflectionClass($entityClass ?? $this->entityClass) )->newInstanceWithoutConstructor(); $entityClass ??= $this->entityClass;
try {
$entity = new $entityClass();
}
catch (\Throwable $ex) {
$entity = ( new \ReflectionClass($entityClass) )->newInstanceWithoutConstructor();
}
$entity->initializeEntity(); $entity->initializeEntity();
return $entity; return $entity;

View File

@ -2,19 +2,35 @@
namespace Ulmus\Repository; namespace Ulmus\Repository;
use Ulmus\{Repository, Query, Ulmus, Common}; use Ulmus\{EntityCollection, Repository, Query, Entity, Ulmus, Common};
class MssqlRepository extends Repository { class MssqlRepository extends Repository {
protected function finalizeQuery() : void protected function finalizeQuery() : void
{ {
if ( $this->queryBuilder->getFragment(Query\MsSQL\Offset::class) ) { $delete = $this->queryBuilder->getFragment(Query\Delete::class);
if (null === $order = $this->queryBuilder->getFragment(Query\OrderBy::class)) { $offset = $this->queryBuilder->getFragment(Query\MsSQL\Offset::class);
if ($offset) {
if ( $delete ) {
$delete->top = $offset->limit;
$this->queryBuilder->removeFragment(Query\MsSQL\Offset::class);
}
elseif (null === $this->queryBuilder->getFragment(Query\OrderBy::class)) {
$this->orderBy("(SELECT 0)"); $this->orderBy("(SELECT 0)");
} }
} }
} }
public function listColumns(? string $table = null) : EntityCollection
{
$table ??= $this->entityResolver->tableName();
$this->showColumnsSqlQuery($table);
return $this->collectionFromQuery(Entity\Mssql\Column::class)->iterate(fn($e) => $e->tableName = $table);
}
protected function serverRequestCountRepository() : Repository protected function serverRequestCountRepository() : Repository
{ {
return new static($this->entityClass, $this->alias, $this->adapter); return new static($this->entityClass, $this->alias, $this->adapter);

View File

@ -2,7 +2,7 @@
namespace Ulmus\Repository; namespace Ulmus\Repository;
use Ulmus\{ Repository, Query, Ulmus}; use Ulmus\{EntityCollection, Repository, Query, Entity, Ulmus};
class MysqlRepository extends Repository { class MysqlRepository extends Repository {
use JsonConditionTrait; use JsonConditionTrait;
@ -14,6 +14,14 @@ class MysqlRepository extends Repository {
return $this; return $this;
} }
public function listColumns(? string $table = null) : EntityCollection
{
$table ??= $this->entityResolver->tableName();
$this->showColumnsSqlQuery($table);
return $this->collectionFromQuery(Entity\Mysql\Column::class)->iterate(fn($e) => $e->tableName = $table);
}
public function createSqlQuery() : self public function createSqlQuery() : self
{ {
if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) { if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) {

View File

@ -3,7 +3,7 @@
namespace Ulmus\Repository; namespace Ulmus\Repository;
use Ulmus\{ use Ulmus\{
Ulmus, Query, Common\EntityResolver, Repository, Event, Ulmus, Query, Common\EntityResolver, Repository, Event, Common\Sql
}; };
use Ulmus\Attribute\Property\{ use Ulmus\Attribute\Property\{
@ -57,7 +57,7 @@ class RelationBuilder
} }
else { else {
if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) { if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class, Join::class ] ) ) {
return $this->instanciateEmptyObject($name, $relation); return $this->instanciateEmptyObject($name, $relation);
} }
elseif ($virtual = $this->resolveVirtual($name)) { elseif ($virtual = $this->resolveVirtual($name)) {
@ -177,6 +177,13 @@ class RelationBuilder
} }
} }
protected function applyWithJoin() : void
{
foreach($this->joins as $item) {
$this->repository->withJoin($item->joins);
}
}
protected function instanciateEmptyEntity(string $name, Relation $relation) : object protected function instanciateEmptyEntity(string $name, Relation $relation) : object
{ {
$class = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type; $class = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
@ -262,25 +269,33 @@ class RelationBuilder
public function oneToMany(string $name, Relation $relation) : Repository public function oneToMany(string $name, Relation $relation) : Repository
{ {
$baseEntity = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type; $relation->entity ??= $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
$this->repository = $baseEntity::repository(); $this->repository ??= $relation->entity::repository();
$this->applyWhere(); $this->applyWhere();
$this->applyOrderBy(); $this->applyOrderBy();
$this->applyWithJoin();
$field = $relation->key; $field = $relation->key;
if ($relation->foreignKey) { if ($relation->foreignKey) {
if ( $relation->generateKey ) { if ( $relation->generateKey ) {
$value = call_user_func_array($relation->generateKey, [ $this->entity ]); $value = call_user_func_array($relation->generateKey, [ $this->entity ]);
} }
else { elseif (isset($this->entity->$field)) {
$value = $this->entity->$field; $value = $this->entity->$field;
} }
$this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), $value ); if (isset($value)) {
#$this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $relation->entity::field($relation->foreignKey), $value );
$this->repository->where($relation->foreignKey($this->repository), $value);
}
else {
$this->repository->where(Sql::raw('TRUE'), Sql::raw('FALSE'));
}
} }
return $this->applyFilter($this->repository, $name); return $this->applyFilter($this->repository, $name);
@ -290,7 +305,7 @@ class RelationBuilder
{ {
extract($this->relationAnnotations($name, $relation)); extract($this->relationAnnotations($name, $relation));
$this->repository = $relationRelation->entity::repository(); $this->repository ??= $relationRelation->entity::repository();
$bridgeAlias = $relation->bridgeField ?? uniqid("bridge_"); $bridgeAlias = $relation->bridgeField ?? uniqid("bridge_");
@ -307,6 +322,8 @@ class RelationBuilder
$this->applyOrderBy(); $this->applyOrderBy();
$this->applyWithJoin();
if ($selectBridgeField && $relation->bridgeField) { if ($selectBridgeField && $relation->bridgeField) {
$this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias); $this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias);
} }
@ -340,7 +357,7 @@ class RelationBuilder
return []; return [];
} }
protected function fieldIsNullable(string $name) : bool protected function fieldIsNullable(string $name) : bool
{ {
return $this->resolver->reflectedClass->getProperties(true)[$name]->allowsNull(); return $this->resolver->reflectedClass->getProperties(true)[$name]->allowsNull();

View File

@ -27,7 +27,6 @@ class SqliteRepository extends Repository {
return $this->collectionFromQuery(Entity\Sqlite\Column::class)->iterate(fn($e) => $e->tableName = $table); return $this->collectionFromQuery(Entity\Sqlite\Column::class)->iterate(fn($e) => $e->tableName = $table);
} }
protected function serverRequestCountRepository() : Repository protected function serverRequestCountRepository() : Repository
{ {
return new static($this->entityClass, $this->alias, $this->adapter); return new static($this->entityClass, $this->alias, $this->adapter);

View File

@ -5,7 +5,7 @@ namespace Ulmus\Repository;
enum WithOptionEnum enum WithOptionEnum
{ {
case SkipWhere; case SkipWhere;
case SkipHaving; case SkipHaving;
case SkipFilter; case SkipFilter;
case SkipOrderBy; case SkipOrderBy;
case SkipJoinWhere; case SkipJoinWhere;

View File

@ -20,6 +20,6 @@ abstract class SearchParameter {
public function getParameters() : array public function getParameters() : array
{ {
return array_filter((array) $this->parameter); return array_filter((array) ( $this->parameter ?? [] ));
} }
} }

View File

@ -76,7 +76,8 @@ trait SearchRequestFromRequestTrait
case $attribute instanceof SearchManual: case $attribute instanceof SearchManual:
if ($attribute->toggle) { if ($attribute->toggle) {
$this->$propertyName = !empty($value); $this->$propertyName = !empty($value);
} elseif ($value !== null) { }
elseif ($value !== null) {
foreach ($property->getTypes() as $type) { foreach ($property->getTypes() as $type) {
$enum = $type->type; $enum = $type->type;