From 667df92e52760d7add304e3522322891ec072279 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Mon, 6 Dec 2021 18:44:17 +0000 Subject: [PATCH 1/2] - Added BLOBs types --- src/Annotation/Property/Field/Blob.php | 11 +++++++++++ src/Annotation/Property/Field/Longblob.php | 11 +++++++++++ src/Annotation/Property/Field/Mediumblob.php | 11 +++++++++++ src/Annotation/Property/Field/Tinyblob.php | 11 +++++++++++ src/EntityTrait.php | 2 +- 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/Annotation/Property/Field/Blob.php create mode 100644 src/Annotation/Property/Field/Longblob.php create mode 100644 src/Annotation/Property/Field/Mediumblob.php create mode 100644 src/Annotation/Property/Field/Tinyblob.php diff --git a/src/Annotation/Property/Field/Blob.php b/src/Annotation/Property/Field/Blob.php new file mode 100644 index 0000000..7a32180 --- /dev/null +++ b/src/Annotation/Property/Field/Blob.php @@ -0,0 +1,11 @@ + Date: Fri, 28 Jan 2022 16:37:35 +0000 Subject: [PATCH 2/2] - A lot of work done on bug fixing. Added a new SQLite adapter --- src/Adapter/AdapterInterface.php | 9 +- src/Adapter/DefaultAdapterTrait.php | 95 +++++++++ src/Adapter/MsSQL.php | 7 +- src/Adapter/MySQL.php | 14 +- src/Adapter/SQLite.php | 140 +++++++++++++ src/Common/EntityField.php | 4 +- src/Common/EntityResolver.php | 2 +- src/ConnectionAdapter.php | 4 +- src/Entity/Sqlite/Schema.php | 43 ++++ src/Entity/Sqlite/Table.php | 19 ++ src/EntityCollection.php | 23 ++- src/Migration/FieldDefinition.php | 83 ++------ src/Modeler/Schema.php | 21 -- src/Query/Alter.php | 11 +- src/Query/Create.php | 9 +- src/Query/Join.php | 7 +- src/Query/Select.php | 1 - src/Query/Sqlite/Pragma.php | 40 ++++ src/QueryBuilder.php | 27 ++- src/QueryBuilder/MssqlQueryBuilder.php | 34 +++ src/QueryBuilder/SqliteQueryBuilder.php | 24 +++ src/Repository.php | 193 +++++++----------- src/Repository/EscapeTrait.php | 51 +++++ src/Repository/MssqlRepository.php | 31 ++- src/Repository/MysqlRepository.php | 26 +++ src/Repository/SqliteRepository.php | 25 +++ .../SearchRequestPaginationTrait.php | 6 + src/Ulmus.php | 12 +- 28 files changed, 727 insertions(+), 234 deletions(-) create mode 100644 src/Adapter/DefaultAdapterTrait.php create mode 100644 src/Adapter/SQLite.php create mode 100644 src/Entity/Sqlite/Schema.php create mode 100644 src/Entity/Sqlite/Table.php delete mode 100644 src/Modeler/Schema.php create mode 100644 src/Query/Sqlite/Pragma.php create mode 100644 src/QueryBuilder/MssqlQueryBuilder.php create mode 100644 src/QueryBuilder/SqliteQueryBuilder.php create mode 100644 src/Repository/EscapeTrait.php create mode 100644 src/Repository/MysqlRepository.php create mode 100644 src/Repository/SqliteRepository.php diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php index d0ca171..ca7ee53 100644 --- a/src/Adapter/AdapterInterface.php +++ b/src/Adapter/AdapterInterface.php @@ -3,6 +3,7 @@ namespace Ulmus\Adapter; use Ulmus\Common\PdoObject; +use Ulmus\Migration\FieldDefinition; interface AdapterInterface { public const IDENTIFIER_FIELD = 1; @@ -10,10 +11,16 @@ interface AdapterInterface { public const IDENTIFIER_DATABASE = 3; public const IDENTIFIER_SCHEMA = 4; public const IDENTIFIER_VALUE = 5; - + public function connect() : object /* | PdoObject|mixed */; public function buildDataSourceName() : string; public function setup(array $configuration) : void; public function escapeIdentifier(string $segment, int $type) : string; public function defaultEngine() : ? string; + public function databaseName() : string; + public function mapFieldType(FieldDefinition $field) : string; + public function schemaTable(string $databaseName, string $tableName) /*: object|EntityCollection */; + public function repositoryClass() : string; + public function queryBuilderClass() : string; + public function tableSyntax() : array; } diff --git a/src/Adapter/DefaultAdapterTrait.php b/src/Adapter/DefaultAdapterTrait.php new file mode 100644 index 0000000..4fee52b --- /dev/null +++ b/src/Adapter/DefaultAdapterTrait.php @@ -0,0 +1,95 @@ + "AUTO_INCREMENT", + 'pk' => "PRIMARY KEY", + 'unsigned' => "UNSIGNED", + ]; + } + + public function databaseName() : string + { + return $this->database; + } + + public function schemaTable(string $databaseName, string $tableName) /* : ? object */ + { + return Table::repository()->where(Table::field('schema'), $databaseName)->loadOneFromField(Table::field('name'), $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 . ( $length ? "($length" . ( $precision ? ",$precision" : "" ) . ")" : "" ); + } +} \ No newline at end of file diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index 4c01539..1d410cf 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -5,8 +5,13 @@ namespace Ulmus\Adapter; use Ulmus\Common\PdoObject; use Ulmus\Exception\AdapterConfigurationException; +use Ulmus\Migration\FieldDefinition; +use Ulmus\{Entity\InformationSchema\Table, Repository, QueryBuilder}; class MsSQL implements AdapterInterface { + use DefaultAdapterTrait; + + const DSN_PREFIX = "sqlsrv"; public int $port; @@ -129,7 +134,7 @@ class MsSQL implements AdapterInterface { $parts[] = "WSID={$this->wsid}"; } - return "sqlsrv:" . implode(';', $parts); + return static::DSN_PREFIX . ":" . implode(';', $parts); } public function setup(array $configuration) : void diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index 8b9113f..1caf641 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -2,11 +2,18 @@ namespace Ulmus\Adapter; +use Ulmus\Entity\InformationSchema\Table; +use Ulmus\QueryBuilder; +use Ulmus\Repository; use Ulmus\Common\PdoObject; use Ulmus\Exception\AdapterConfigurationException; +use Ulmus\Migration\FieldDefinition; class MySQL implements AdapterInterface { + use DefaultAdapterTrait; + + const DSN_PREFIX = "mysql"; public string $hostname; @@ -17,7 +24,7 @@ class MySQL implements AdapterInterface { public string $password; public string $charset = "utf8mb4"; - + public ? string $socket; public int $port = 3306; @@ -68,6 +75,9 @@ class MySQL implements AdapterInterface { catch(PDOException $ex){ throw $ex; } + finally { + $this->password = str_repeat('*', strlen($this->password)); + } return $pdo; } @@ -91,7 +101,7 @@ class MySQL implements AdapterInterface { $parts[] = "charset={$this->charset}"; } - return "mysql:" . implode(';', $parts); + return static::DSN_PREFIX . ":" . implode(';', $parts); } public function setup(array $configuration) : void diff --git a/src/Adapter/SQLite.php b/src/Adapter/SQLite.php new file mode 100644 index 0000000..dd89b0d --- /dev/null +++ b/src/Adapter/SQLite.php @@ -0,0 +1,140 @@ +path = $path; + } + + if ($pragma !== null) { + $this->pragma = $pragma; + } + } + + public function connect() : PdoObject + { + try { + $pdo = new PdoObject($this->buildDataSourceName(), null, null); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); + $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); + } + catch(PDOException $ex){ + throw $ex; + } + + return $pdo; + } + + public function buildDataSourceName() : string + { + $parts[] = $this->path; + + return static::DSN_PREFIX . ":" . implode(';', $parts); + } + + public function setup(array $configuration) : void + { + $this->path = $configuration['path'] ?? ""; + $this->pragma = $configuration['pragma'] ?? []; + } + + # https://sqlite.org/lang_keywords.html + public function escapeIdentifier(string $segment, int $type) : string + { + switch($type) { + case static::IDENTIFIER_DATABASE: + case static::IDENTIFIER_TABLE: + case static::IDENTIFIER_FIELD: + return '"' . trim(str_replace('"', '""', $segment), '"') . '"'; + + case static::IDENTIFIER_VALUE: + return "'$segment'"; + } + } + + public function defaultEngine(): ? string + { + return null; + } + + public function databaseName() : string + { + $base = basename($this->path); + + return substr($base, 0, strrpos($base, '.') ?: strlen($base)); + } + + public function schemaTable(string $databaseName, string $tableName) /* : ? object */ + { + return Table::repository()->loadOneFromField(Table::field('tableName'), $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) || is_a($type, Entity\Field\Time::class, true) || is_a($type, \DateTime::class, true) ) { + $type = "TEXT"; + $length = strlen((string) $type); + } + else { + switch($type) { + case "bool": + $check = sprintf("CHECK (%s IN (0, 1))", $field->getColumnName()); + + case "bigint": + case "int": + $type = "INTEGER"; + break; + + case "array": + case "string": + $type = "TEXT"; + $length = null; + break; + + case "float": + $type = "REAL"; + break; + + default: + $type = "BLOB"; + break; + } + } + + return $typeOnly ? $type : $type . ( $length ? "($length" . ( $precision ? ",$precision" : "" ) . ")" : "" ); + } + + public function tableSyntax() : array + { + return [ + 'ai' => "AUTOINCREMENT", + 'pk' => "PRIMARY KEY", + 'unsigned' => "", + ]; + } +} diff --git a/src/Common/EntityField.php b/src/Common/EntityField.php index 4847005..40fc164 100644 --- a/src/Common/EntityField.php +++ b/src/Common/EntityField.php @@ -50,9 +50,9 @@ class EntityField implements WhereRawParameter return false; } - public static function generateCreateColumn($field) : string + public static function generateCreateColumn(AdapterInterface $adapter, $field) : string { - $definition = new FieldDefinition($field); + $definition = new FieldDefinition($adapter, $field); return implode(" ", [ $definition->getSqlName(), diff --git a/src/Common/EntityResolver.php b/src/Common/EntityResolver.php index b973e5d..2b8923d 100644 --- a/src/Common/EntityResolver.php +++ b/src/Common/EntityResolver.php @@ -164,7 +164,7 @@ class EntityResolver { public function databaseName() : ? string { - return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->database ?? null; + return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->databaseName() ?? null; } public function sqlAdapter() : \Ulmus\ConnectionAdapter diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php index 97d37f5..db6ea72 100644 --- a/src/ConnectionAdapter.php +++ b/src/ConnectionAdapter.php @@ -37,6 +37,8 @@ class ConnectionAdapter } $this->adapter->setup($connection); + + unset($this->configuration['connections'][$this->name]); } public function getConfiguration() : array @@ -77,7 +79,7 @@ class ConnectionAdapter */ protected function instanciateAdapter($name) : AdapterInterface { - $class = substr($name, 0, 2) === "\\" ? $name : "\\Ulmus\\Adapter\\$name"; + $class = substr($name, 0, 2) === "\\" ? $name : sprintf("\\%s\\Adapter\\%s", __NAMESPACE__, $name); return new $class(); } diff --git a/src/Entity/Sqlite/Schema.php b/src/Entity/Sqlite/Schema.php new file mode 100644 index 0000000..ed57e5f --- /dev/null +++ b/src/Entity/Sqlite/Schema.php @@ -0,0 +1,43 @@ + "sqlite_master") + */ +class Schema +{ + use \Ulmus\EntityTrait; + + /** + * @Id + */ + public ? string $name; + + /** + * @Field + */ + public ? string $type; + + /** + * @Field('name' => 'tbl_name') + */ + public ? string $tableName; + + /** + * @Field + */ + public ? int $rootpage; + + /** + * @Field + */ + public ? string $sql; + + /** + * @Relation('oneToMany', 'key' => 'tableName', 'foreignKey' => 'tableName', 'entity' => Schema::class) + */ + public EntityCollection $columns; +} \ No newline at end of file diff --git a/src/Entity/Sqlite/Table.php b/src/Entity/Sqlite/Table.php new file mode 100644 index 0000000..43652fd --- /dev/null +++ b/src/Entity/Sqlite/Table.php @@ -0,0 +1,19 @@ +select(Table::field('tableName'))->groupBy(Table::field('tableName')); + } + }; + } +} \ No newline at end of file diff --git a/src/EntityCollection.php b/src/EntityCollection.php index 0ef38e8..75b9e3f 100644 --- a/src/EntityCollection.php +++ b/src/EntityCollection.php @@ -146,7 +146,12 @@ class EntityCollection extends \ArrayObject { return $this->filtersCollection(fn($obj) => is_a($obj, $className)); } - public function column($field, bool $unique = false) : array + public function column($field, bool $uniqueValue = false, $keyField = null) : array + { + return $this->map($field, $keyField, $uniqueValue); + } + + public function map($field, $keyField = null, bool $uniqueValue = false) : array { $list = []; @@ -158,11 +163,23 @@ class EntityCollection extends \ArrayObject { $value = $item->$field; } - if ($unique && in_array($value, $list, true)) { + if ($uniqueValue && in_array($value, $list, true)) { continue; } - $list[] = $value; + if ( $keyField !== null ) { + if ( is_callable($keyField) ) { + $key = call_user_func_array($keyField, [ $item ]); + } + else { + $key = $item->$keyField; + } + + $list[$key] = $value; + } + else { + $list[] = $value; + } } return $list; diff --git a/src/Migration/FieldDefinition.php b/src/Migration/FieldDefinition.php index ad35d5d..e5f6459 100644 --- a/src/Migration/FieldDefinition.php +++ b/src/Migration/FieldDefinition.php @@ -2,6 +2,7 @@ namespace Ulmus\Migration; +use Ulmus\Adapter\AdapterInterface; use Ulmus\Annotation\Property\Field; use Ulmus\Entity; @@ -25,8 +26,12 @@ class FieldDefinition { public ? string $update; - public function __construct(array $data) + public AdapterInterface $adapter; + + public function __construct(AdapterInterface $adapter, array $data) { + $this->adapter = $adapter; + $this->name = $data['name']; $this->builtIn = $data['builtin']; $this->tags = $data['tags']; @@ -46,108 +51,56 @@ class FieldDefinition { public function getSqlType(bool $typeOnly = false) : string { - $type = $this->type; - - $length = $this->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 . ( $length ? "($length" . ( $precision ? ",$precision" : "" ) . ")" : "" ); + return $this->adapter->mapFieldType($this, $typeOnly); } public function getSqlParams() : string { $default = $this->getDefault(); + $syntax = $this->adapter->tableSyntax(); return implode(' ', array_filter([ - $this->isUnsigned() ? "UNSIGNED" : null, + $this->isUnsigned() ? $syntax['unsigned'] : null, $this->nullable ? "NULL" : "NOT NULL", $default ? "DEFAULT $default" : null, - $this->isAutoIncrement() ? "AUTO_INCREMENT" : null, - $this->isPrimaryKey() ? "PRIMARY KEY" : null, + $this->isPrimaryKey() ? $syntax['pk'] : null, + $this->isAutoIncrement() ? $syntax['ai'] : null, ])); } - protected function getFieldTag() : ? Field + public function getFieldTag() : ? Field { $field = array_filter($this->tags, fn($item) => $item['object'] instanceof Field); return array_pop($field)['object']; } - protected function getColumnName() : ? string + public function getColumnName() : ? string { return $this->getFieldTag()->name ?? $this->name; } - protected function getDefault() : ? string + public function getDefault() : ? string { return $this->getFieldTag()->attributes['default'] ?? ( $this->nullable ? "NULL" : null ) ; } - protected function isPrimaryKey() : bool + public function isPrimaryKey() : bool { return $this->getFieldTag()->attributes['primary_key'] ?? false; } - protected function isAutoIncrement() : bool + public function isAutoIncrement() : bool { return $this->getFieldTag()->attributes['auto_increment'] ?? false; } - protected function isUnique() : bool + public function isUnique() : bool { return $this->getFieldTag()->attributes['unique'] ?? false; } - protected function isUnsigned() : bool + public function isUnsigned() : bool { return $this->getFieldTag()->attributes['unsigned'] ?? false; } diff --git a/src/Modeler/Schema.php b/src/Modeler/Schema.php deleted file mode 100644 index 5744811..0000000 --- a/src/Modeler/Schema.php +++ /dev/null @@ -1,21 +0,0 @@ -adapter = $adapter; + } public function render() : string { @@ -29,7 +36,7 @@ class Alter extends Fragment { public function renderFields() : string { return "(" . PHP_EOL . implode("," . PHP_EOL, array_map(function($field) { - return " " . EntityField::generateAlterColumn($field); + return " " . EntityField::generateAlterColumn($this->adapter, $field); }, $this->fieldList)) . PHP_EOL . ")"; } } diff --git a/src/Query/Create.php b/src/Query/Create.php index b884339..6e95a1d 100644 --- a/src/Query/Create.php +++ b/src/Query/Create.php @@ -2,6 +2,7 @@ namespace Ulmus\Query; +use Ulmus\Adapter\AdapterInterface; use Ulmus\Annotation, Ulmus\Common\EntityField; @@ -19,6 +20,12 @@ class Create extends Fragment { public array $fieldList; + public AdapterInterface $adapter; + + public function __construct(AdapterInterface $adapter) { + $this->adapter = $adapter; + } + public function render() : string { return $this->renderSegments([ @@ -29,7 +36,7 @@ class Create extends Fragment { public function renderFields() : string { return "(" . PHP_EOL . implode("," . PHP_EOL, array_map(function($field) { - return " " . EntityField::generateCreateColumn($field); + return " " . EntityField::generateCreateColumn($this->adapter, $field); }, $this->fieldList)) . PHP_EOL . ")"; } } diff --git a/src/Query/Join.php b/src/Query/Join.php index 77a5e95..f07544d 100644 --- a/src/Query/Join.php +++ b/src/Query/Join.php @@ -8,7 +8,7 @@ use Ulmus\Repository\ConditionTrait; class Join extends Fragment { use ConditionTrait; - + const SQL_OUTER = "OUTER"; const SQL_TOKEN = "JOIN"; @@ -39,9 +39,12 @@ class Join extends Fragment public int $order = 40; + public int $joinOrder = 0; + public function __construct(QueryBuilderInterface $queryBuilder) { - $this->queryBuilder = new QueryBuilder(); + $cls = get_class($queryBuilder); + $this->queryBuilder = new $cls(); $this->queryBuilder->parent = $queryBuilder; } diff --git a/src/Query/Select.php b/src/Query/Select.php index 4f7c01f..a4efe9f 100644 --- a/src/Query/Select.php +++ b/src/Query/Select.php @@ -42,7 +42,6 @@ class Select extends Fragment { if ( ! is_numeric($key) ) { $value = sprintf(static::FIELD_AS, $value, $key); } - } return $this->renderSegments([ diff --git a/src/Query/Sqlite/Pragma.php b/src/Query/Sqlite/Pragma.php new file mode 100644 index 0000000..9c67f33 --- /dev/null +++ b/src/Query/Sqlite/Pragma.php @@ -0,0 +1,40 @@ +pragma = $pragma; + + if ($value) { + $this->value = $value; + } + + $this->callable = $callable; + + return $this; + } + + public function render() : string + { + if ( isset($this->value) ) { + $value = sprintf($this->callable ? " (%s)" : " = %s", $this->value); + } + + return $this->renderSegments([ + sprintf(static::SQL_TOKEN, $this->pragma, $value ?? "") + ]); + } +} diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 2b9ada3..3f54269 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -305,7 +305,7 @@ class QueryBuilder implements Query\QueryBuilderInterface $join->outer = $outer; $join->alias = $alias; - + return $join; } @@ -328,7 +328,7 @@ class QueryBuilder implements Query\QueryBuilderInterface return $this; } - public function create(array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self + public function create(Adapter\AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self { if ( null === $this->getFragment(Query\Create::class) ) { if ( $schema ) { @@ -339,7 +339,7 @@ class QueryBuilder implements Query\QueryBuilderInterface $table = "$database.$table"; } - $create = new Query\Create(); + $create = new Query\Create($adapter); $this->push($create); $create->fieldList = $fieldlist; @@ -352,7 +352,7 @@ class QueryBuilder implements Query\QueryBuilderInterface return $this; } - public function alter(array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self + public function alter(Adapter\AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self { if ( null === $this->getFragment(Query\Create::class) ) { if ( $schema ) { @@ -363,7 +363,7 @@ class QueryBuilder implements Query\QueryBuilderInterface $table = "$database.$table"; } - $alter = new Query\Alter(); + $alter = new Query\Alter($adapter); $this->push($alter); $alter->fieldList = $fieldlist; @@ -405,7 +405,7 @@ class QueryBuilder implements Query\QueryBuilderInterface $sql = []; usort($this->queryStack, function($q1, $q2) { - return $q1->order <=> $q2->order; + return (float) $q1->order <=> (float) $q2->order; }); foreach($this->queryStack as $fragment) { @@ -456,9 +456,9 @@ class QueryBuilder implements Query\QueryBuilderInterface } } - public function getFragments() : array + public function getFragments(/*? Query\Fragment|array*/ $fragment = null) : array { - return $this->queryStack; + return $fragment === null ? array_filter($this->queryStack, fn($e) => get_class($e) === $fragment) : $this->queryStack; } public function __toString() : string @@ -485,4 +485,15 @@ class QueryBuilder implements Query\QueryBuilderInterface { $this->values = $values; } + + protected function nextJoinOrder() : float + { + $next = 0; + + foreach($this->getFragments(Query\Join::class) as $join) { + $next = max($next, $join->joinOrder - Query\Join::ORDER_VALUE); + } + + return $next + 1; + } } diff --git a/src/QueryBuilder/MssqlQueryBuilder.php b/src/QueryBuilder/MssqlQueryBuilder.php new file mode 100644 index 0000000..e139517 --- /dev/null +++ b/src/QueryBuilder/MssqlQueryBuilder.php @@ -0,0 +1,34 @@ +getFragment(Query\MsSQL\Offset::class) ) { + $offset = new Query\MsSQL\Offset(); + $this->push($offset); + } + + $offset->limit = $value; + + return $this; + } + + public function offset(int $value) : self + { + if ( null === $offset = $this->getFragment(Query\MsSQL\Offset::class) ) { + $offset = new Query\MsSQL\Offset(); + $this->push($offset); + } + + $offset->offset = $value; + + return $this; + } +} diff --git a/src/QueryBuilder/SqliteQueryBuilder.php b/src/QueryBuilder/SqliteQueryBuilder.php new file mode 100644 index 0000000..821df22 --- /dev/null +++ b/src/QueryBuilder/SqliteQueryBuilder.php @@ -0,0 +1,24 @@ +getFragment(Query\Sqlite\Pragma::class) ) ) { + $pragma->set($name, $value, $callable); + } + else { + $pragma = new Query\Sqlite\Pragma(); + $pragma->set($name, $value, $callable); + $this->push($pragma); + } + + return $this; + } +} diff --git a/src/Repository.php b/src/Repository.php index 267efe8..c78bdf9 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -7,7 +7,7 @@ use Ulmus\Common\EntityResolver; class Repository { - use EventTrait, Repository\ConditionTrait; + use EventTrait, Repository\ConditionTrait, Repository\EscapeTrait; const DEFAULT_ALIAS = "this"; @@ -28,9 +28,9 @@ class Repository $this->alias = $alias; $this->entityResolver = Ulmus::resolveEntity($entity); $this->adapter = $adapter ?? $this->entityResolver->databaseAdapter(); - $this->queryBuilder = new QueryBuilder(); + $this->queryBuilder = Ulmus::queryBuilder($entity); } - + public function __clone() { #$this->queryBuilder = clone $this->queryBuilder; @@ -148,9 +148,9 @@ class Repository # $dataset = array_filter($dataset, fn($item, $field) => ! ($this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false), \ARRAY_FILTER_USE_BOTH); $statement = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery(); - - if ( ( 0 !== $statement->lastInsertId ) && - ( null !== $primaryKeyDefinition )) { + + if ( ( 0 !== $statement->lastInsertId ) && + ( null !== $primaryKeyDefinition )) { $pkField = key($primaryKeyDefinition); $dataset[$pkField] = $statement->lastInsertId; @@ -195,74 +195,20 @@ class Repository } - public function loadCollectionRelation(EntityCollection $collection, /*array|string*/ $fields) : void - { - foreach ((array)$fields as $name) { - if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation()))) { - $relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type)); - - $order = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\OrderBy()); - $where = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\Where()); - - $baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type']; - $baseEntityResolver = $baseEntity::resolveEntity(); - - $property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name']; - $entityProperty = ($this->entityResolver->field($relation->key, 01, false) ?: $this->entityResolver->field($relation->key, 02))['name']; - - $repository = $baseEntity::repository(); - - foreach ($where as $condition) { - $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->value, $condition->operator, $condition->condition); - } - - foreach ($order as $item) { - $repository->orderBy($item->field, $item->order); - } - - $field = $relation->key; - - $values = []; - - $key = is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey); - - foreach ($collection as $item) { - $values[] = is_callable($field) ? $field($item) : $item->$entityProperty; - } - - $repository->where($key, $values); - - switch ($relationType) { - case 'onetoone': - $results = call_user_func([$repository, "loadOne"]); - $item->$name = $results ?: new $baseEntity(); - - break; - - case 'onetomany': - $results = call_user_func([$repository, $relation->function]); - - foreach ($collection as $item) { - $item->$name = $baseEntity::entityCollection(); - $item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty)); - } - - break; - } - } - } - } - public function replace(/*object|array*/ $entity, ? array $fieldsAndValue = null) : bool { return $this->save($entity, $fieldsAndValue, true); } - public function replaceAll(/*EntityCollection|array*/ $collection) : void + public function replaceAll(/*EntityCollection|array*/ $collection) : int { + $changed = 0; + foreach($collection as $entity) { - $this->replace($entity); + $this->replace($entity) && $changed++; } + + return $changed; } public function truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self @@ -489,10 +435,10 @@ class Repository public function randomizeOrder() : self { $this->queryBuilder->orderBy(Common\Sql::function('RAND', Sql::identifier('CURDATE()+0'))); - + return $this; } - + public function orders(array $orderList) : self { foreach($orderList as $field => $direction) { @@ -678,6 +624,64 @@ class Repository return $this; } + public function loadCollectionRelation(EntityCollection $collection, /*array|string*/ $fields) : void + { + foreach ((array)$fields as $name) { + if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation()))) { + $relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type)); + + $order = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\OrderBy()); + $where = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\Where()); + + $baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type']; + $baseEntityResolver = $baseEntity::resolveEntity(); + + $property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name']; + $entityProperty = ($this->entityResolver->field($relation->key, 01, false) ?: $this->entityResolver->field($relation->key, 02))['name']; + + $repository = $baseEntity::repository(); + + foreach ($where as $condition) { + $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->value, $condition->operator, $condition->condition); + } + + foreach ($order as $item) { + $repository->orderBy($item->field, $item->order); + } + + $field = $relation->key; + + $values = []; + + $key = is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey); + + foreach ($collection as $item) { + $values[] = is_callable($field) ? $field($item) : $item->$entityProperty; + } + + $repository->where($key, $values); + + switch ($relationType) { + case 'onetoone': + $results = call_user_func([$repository, "loadOne"]); + $item->$name = $results ?: new $baseEntity(); + + break; + + case 'onetomany': + $results = call_user_func([$repository, $relation->function]); + + foreach ($collection as $item) { + $item->$name = $baseEntity::entityCollection(); + $item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty)); + } + + break; + } + } + } + } + public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self { if ($count) { @@ -819,13 +823,7 @@ class Repository public function createSqlQuery() : self { if ( null === $this->queryBuilder->getFragment(Query\Create::class) ) { - $this->queryBuilder->create($this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); - } - - if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) { - if ( $engine = $this->entityResolver->tableAnnotation()->engine ?? $this->entityResolver->databaseAdapter()->adapter()->defaultEngine() ) { - $this->queryBuilder->engine($engine); - } + $this->queryBuilder->create($this->adapter->adapter(), $this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); } return $this; @@ -834,7 +832,7 @@ class Repository public function alterSqlQuery(array $fields) : self { if ( null === $this->queryBuilder->getFragment(Query\Alter::class) ) { - $this->queryBuilder->create($this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); + $this->queryBuilder->create($this->adapter->adapter(), $this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); } @@ -859,7 +857,7 @@ class Repository return $result; } - + public function instanciateEntityCollection(...$arguments) : EntityCollection { return $this->entityClass::entityCollection(...$arguments); @@ -870,47 +868,6 @@ class Repository return new $this->entityClass(); } - public function escapeField(string $identifier) : string - { - return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_FIELD); - } - - public function escapeFieldList(array $fieldList) : array - { - foreach($fieldList as &$list) { - $list['name'] = $this->escapeField($list['name']); - - $fieldTag = array_filter($list['tags'] ?? [], fn($item) => $item['object'] instanceof Field)[0]['object'] ?? null; - - if ( $fieldTag && isset($fieldTag->name) ) { - $fieldTag->name = $this->escapeField($fieldTag->name); - } - } - - return $fieldList; - } - - public function escapeTable(string $identifier) : string - { - return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_TABLE); - } - - public function escapeDatabase(string $identifier) : string - { - return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_DATABASE); - } - - public function escapedDatabase() : ? string - { - $name = $this->entityResolver->tableAnnotation()->database ?? $this->adapter->adapter()->database ?? null; - - return $name ? static::escapeDatabase($name) : null; - } - - public function escapeSchema(string $identifier) : string - { - return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_SCHEMA); - } public function hasFilters() : bool { diff --git a/src/Repository/EscapeTrait.php b/src/Repository/EscapeTrait.php new file mode 100644 index 0000000..eb69ab7 --- /dev/null +++ b/src/Repository/EscapeTrait.php @@ -0,0 +1,51 @@ +adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_FIELD); + } + + public function escapeFieldList(array $fieldList) : array + { + foreach($fieldList as &$list) { + $list['name'] = $this->escapeField($list['name']); + + $fieldTag = array_filter($list['tags'] ?? [], fn($item) => $item['object'] instanceof Field)[0]['object'] ?? null; + + if ( $fieldTag && isset($fieldTag->name) ) { + $fieldTag->name = $this->escapeField($fieldTag->name); + } + } + + return $fieldList; + } + + public function escapeTable(string $identifier) : string + { + return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_TABLE); + } + + public function escapeDatabase(string $identifier) : string + { + return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_DATABASE); + } + + public function escapedDatabase() : ? string + { + $name = $this->entityResolver->tableAnnotation()->database ?? $this->adapter->adapter()->database ?? null; + + return $name ? static::escapeDatabase($name) : null; + } + + public function escapeSchema(string $identifier) : string + { + return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_SCHEMA); + } +} \ No newline at end of file diff --git a/src/Repository/MssqlRepository.php b/src/Repository/MssqlRepository.php index 590fb47..81ba517 100644 --- a/src/Repository/MssqlRepository.php +++ b/src/Repository/MssqlRepository.php @@ -5,7 +5,7 @@ namespace Ulmus\Repository; use Ulmus\{ Repository, Query }; class MssqlRepository extends Repository { - +/* protected function finalizeQuery() : void { if ( null !== $offset = $this->queryBuilder->getFragment(Query\Offset::class) ) { @@ -39,5 +39,34 @@ class MssqlRepository extends Repository { $this->queryBuilder->removeFragment($limit); } } +*/ + protected function finalizeQuery() : void + { + if ( null !== $offset = $this->queryBuilder->getFragment(Query\MsSQL\Offset::class) ) { + # an order by is mandatory for mssql offset/limit + if ( null === $order = $this->queryBuilder->getFragment(Query\OrderBy::class) ) { + $this->orderBy("(SELECT 0)"); + } + + if ( empty ($offset->offset ) ) { + if ( null !== $select = $this->queryBuilder->getFragment(Query\Select::class) ) { + $select->top = $offset->limit; + } + elseif ( null !== $delete = $this->queryBuilder->getFragment(Query\Delete::class) ) { + $delete->top = $offset->limit; + } + + $this->queryBuilder->removeFragment($offset); + } + elseif ( empty($offset->limit) ) { + throw new \Exception("Your offset query fragment is missing a LIMIT value."); + } + } + } + + protected function serverRequestCountRepository() : Repository + { + return new static($this->entityClass, $this->alias, $this->adapter); + } } diff --git a/src/Repository/MysqlRepository.php b/src/Repository/MysqlRepository.php new file mode 100644 index 0000000..b12d87d --- /dev/null +++ b/src/Repository/MysqlRepository.php @@ -0,0 +1,26 @@ +queryBuilder->pragma($pragma, $argument, $callable); + + return $this; + } + + public function createSqlQuery() : self + { + if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) { + if ( $engine = $this->entityResolver->tableAnnotation()->engine ?? $this->entityResolver->databaseAdapter()->adapter()->defaultEngine() ) { + $this->queryBuilder->engine($engine); + } + } + + return parent::createSqlQuery(); + } +} diff --git a/src/Repository/SqliteRepository.php b/src/Repository/SqliteRepository.php new file mode 100644 index 0000000..6392fe1 --- /dev/null +++ b/src/Repository/SqliteRepository.php @@ -0,0 +1,25 @@ +queryBuilder->pragma($pragma, $argument, $callable); + + return $this; + } + + protected function finalizeQuery() : void + { + + } + + protected function serverRequestCountRepository() : Repository + { + return new static($this->entityClass, $this->alias, $this->adapter); + } +} diff --git a/src/SearchRequest/SearchRequestPaginationTrait.php b/src/SearchRequest/SearchRequestPaginationTrait.php index 912b862..e93cc72 100644 --- a/src/SearchRequest/SearchRequestPaginationTrait.php +++ b/src/SearchRequest/SearchRequestPaginationTrait.php @@ -41,4 +41,10 @@ trait SearchRequestPaginationTrait { { return $this->pageCount() > 1; } + public function skipCount(bool $value) : self + { + $this->skipCount = $value; + + return $this; + } } diff --git a/src/Ulmus.php b/src/Ulmus.php index 07fa13c..258a6a6 100644 --- a/src/Ulmus.php +++ b/src/Ulmus.php @@ -102,14 +102,18 @@ abstract class Ulmus return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass); } - public static function repository(...$arguments) : Repository + public static function repository(string $entityClass, ...$arguments) : Repository { - return new static::$repositoryClass(...$arguments); + $cls = $entityClass::resolveEntity()->sqlAdapter()->adapter()->repositoryClass(); + + return new $cls($entityClass, ...$arguments); } - public static function queryBuilder(...$arguments) : Query\QueryBuilderInterface + public static function queryBuilder($entityClass, ...$arguments) : Query\QueryBuilderInterface { - return new static::$queryBuilderClass(...$arguments); + $cls = $entityClass::resolveEntity()->sqlAdapter()->adapter()->queryBuilderClass(); + + return new $cls(...$arguments); } public static function instanciateObject(string $type, array $arguments) : object