This commit is contained in:
Dave Mc Nicoll 2020-11-27 12:21:10 -05:00
commit f74d1907d9
21 changed files with 759 additions and 555 deletions

View File

@ -14,4 +14,5 @@ interface AdapterInterface {
public function buildDataSourceName() : string;
public function setup(array $configuration) : void;
public function escapeIdentifier(string $segment, int $type) : string;
public function defaultEngine() : ? string;
}

View File

@ -196,4 +196,9 @@ class MsSQL implements AdapterInterface {
return "[" . str_replace(["[", "]"], [ "[[", "]]" ], $segment) . "]";
}
}
public function defaultEngine(): ? string
{
return null;
}
}

View File

@ -134,4 +134,8 @@ class MySQL implements AdapterInterface {
}
}
public function defaultEngine(): ? string
{
return "InnoDB";
}
}

View File

@ -6,14 +6,22 @@ class Table implements \Ulmus\Annotation\Annotation {
public string $name;
public string $database;
public string $schema;
public string $adapter;
public string $engine;
public function __construct($name = null)
public function __construct($name = null, $engine = null)
{
if ( $name !== null ) {
$this->name = $name;
}
if ( $engine !== null ) {
$this->engine = $engine;
}
}
}

View File

@ -7,19 +7,21 @@ class Field implements \Ulmus\Annotation\Annotation {
public string $type;
public string $name;
public int $length;
public int $precision;
public array $attributes = [];
public bool $nullable = false;
public function __construct(? string $type = null, ? int $length = null)
{
if ( $type !== null ) {
$this->type = $type;
}
if ( $length !== null ) {
$this->length = $length;
}

View File

@ -14,6 +14,7 @@ class ForeignKey extends Id {
unset($this->nullable);
$this->attributes['primary_key'] = false;
$this->attributes['auto_increment'] = false;
}
}

View File

@ -2,6 +2,8 @@
namespace Ulmus\Common;
use Ulmus\Annotation\Annotation;
use Ulmus\Migration\FieldDefinition;
use Ulmus\Ulmus,
Ulmus\Adapter\AdapterInterface,
Ulmus\Annotation\Property\Field;
@ -28,8 +30,8 @@ class EntityField
{
$name = $this->entityResolver->searchFieldAnnotation($this->name, new Field() )->name ?? $this->name;
$name = ( $this->entityResolver->databaseAdapter() ?? Ulmus::$defaultAdapter )->adapter()->escapeIdentifier($name, AdapterInterface::IDENTIFIER_FIELD);
$name = $this->entityResolver->databaseAdapter()->adapter()->escapeIdentifier($name, AdapterInterface::IDENTIFIER_FIELD);
return $useAlias ? "{$this->alias}.$name" : $name;
}
@ -47,6 +49,19 @@ class EntityField
return false;
}
public static function generateCreateColumn($field) : string
{
$definition = new FieldDefinition($field);
# column_name data_type(length) [NOT NULL] [DEFAULT value] [AUTO_INCREMENT] column_constraint;
return implode(" ", [
$definition->getSqlName(),
$definition->getSqlType(),
$definition->getSqlParams(),
]);
}
public static function isObjectType($type) : bool
{
# @ Should be fixed with isBuiltIn() instead, it won't be correct based only on name

View File

@ -2,7 +2,8 @@
namespace Ulmus\Common;
use Ulmus\Annotation\Annotation,
use Ulmus\Ulmus,
Ulmus\Annotation\Annotation,
Ulmus\Annotation\Classes\Table,
Ulmus\Annotation\Property\Field,
Ulmus\Annotation\Property\Virtual,
@ -128,20 +129,36 @@ class EntityResolver {
return $list;
}
public function tableName() : string
public function tableName($required = false) : string
{
if ( null === $table = $this->getAnnotationFromClassname( Table::class ) ) {
throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation");
$table = $this->tableAnnotation($required);
if ( ( $table->name ?? "" ) === "") {
if ($required) {
throw new \ArgumentCountError("Your entity {$this->entityClass} seems to be missing a `name` argument for your @Table() annotation");
}
}
if ( $table->name === "" ) {
throw new \ArgumentCountError("Your entity {$this->entityClass} seems to be missing a `name` argument for your @Table() annotation");
}
return $table->name;
return $table->name ?? "";
}
public function databaseAdapter() : ? \Ulmus\ConnectionAdapter
public function tableAnnotation($required = false) : Table
{
if ( null === $table = $this->getAnnotationFromClassname( Table::class ) ) {
if ($required) {
throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation");
}
}
return $table;
}
public function databaseName() : string
{
return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->database;
}
public function databaseAdapter() : \Ulmus\ConnectionAdapter
{
if ( null !== $table = $this->getAnnotationFromClassname( Table::class ) ) {
if ( $table->adapter ?? null ) {
@ -153,18 +170,17 @@ class EntityResolver {
}
}
}
return null;
return Ulmus::$defaultAdapter;
}
public function schemaName() : ? string
public function schemaName(bool $required = false) : ? string
{
if ( null === $table = $this->getAnnotationFromClassname( Table::class ) ) {
throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation");
}
if ( $table->name === "" ) {
if ( $required && ( ( $table->schema ?? "" ) === "" ) ) {
throw new \ArgumentCountError("Your entity {$this->entityClass} seems to be missing a `schema` argument for your @Table() annotation");
}

View File

@ -12,7 +12,7 @@ class PdoObject extends PDO {
public function select(string $sql, array $parameters = []): PDOStatement
{
static::$dump && call_user_func_array(static::$dump, [ $sql, $parameters ]);
try {
if (false !== ( $statement = $this->prepare($sql) )) {
$statement = $this->execute($statement, $parameters, false);

View File

@ -39,6 +39,11 @@ class ConnectionAdapter
$this->adapter->setup($connection);
}
public function getConfiguration() : array
{
return $this->configuration['connections'][$this->name];
}
/**
* Connect the adapter
* @return self

View File

@ -0,0 +1,13 @@
<?php
namespace Ulmus\Container;
class AdapterProxy
{
public array $connections = [];
public function __construct(...$arguments)
{
$this->connections = $arguments;
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace Ulmus\Entity\InformationSchema;
use Ulmus\Entity\Field\Datetime;
/**
* @Table('name' => "columns", 'database' => "information_schema")
*/
class Column
{
use \Ulmus\EntityTrait;
/**
* @Field('name' => "TABLE_CATALOG", 'length' => 512)
*/
public string $tableCatalog;
/**
* @Field('name' => "TABLE_SCHEMA", 'length' => 64)
*/
public string $tableSchema;
/**
* @Field('name' => "TABLE_NAME", 'length' => 64)
*/
public string $tableName;
/**
* @Field('name' => "COLUMN_NAME", 'length' => 64, 'attributes' => [ 'primary_key' => true ])
*/
public string $name;
/**
* @Field('name' => "ORDINAL_POSITION", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public int $ordinalPosition;
/**
* @Field('name' => "COLUMN_DEFAULT", 'type' => "longtext")
*/
public ? string $default;
/**
* @Field('name' => "IS_NULLABLE", 'length' => 3)
*/
public string $nullable;
/**
* @Field('name' => "DATA_TYPE", 'length' => 64)
*/
public string $dataType;
/**
* @Field('name' => "CHARACTER_MAXIMUM_LENGTH", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? int $characterMaximumLength;
/**
* @Field('name' => "CHARACTER_OCTET_LENGTH", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? int $characterOctetLength;
/**
* @Field('name' => "NUMERIC_PRECISION", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? int $numericPrecision;
/**
* @Field('name' => "NUMERIC_SCALE", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? int $numericScale;
/**
* @Field('name' => "DATETIME_PRECISION", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? int $datetimePrecision;
/**
* @Field('name' => "CHARACTER_SET_NAME", 'length' => 32)
*/
public ? string $characterSetName;
/**
* @Field('name' => "COLLATION_NAME", 'length' => 32)
*/
public ? string $collationName;
/**
* @Field('name' => "COLLATION_TYPE", 'type' => "longtext")
*/
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)
*/
public string $generated;
/**
* @Field('name' => "GENERATION_EXPRESSION", 'type' => "longtext")
*/
public ? string $generationExpression;
}

View File

@ -0,0 +1,135 @@
<?php
namespace Ulmus\Entity\InformationSchema;
use Ulmus\EntityCollection,
Ulmus\Entity\Field\Datetime;
/**
* @Table('name' => "tables", 'database' => "information_schema")
*/
class Table
{
use \Ulmus\EntityTrait;
/**
* @Field('name' => "TABLE_CATALOG", 'length' => 512)
*/
public string $catalog;
/**
* @Field('name' => "TABLE_SCHEMA", 'length' => 64)
*/
public string $schema;
/**
* @Field('name' => "TABLE_NAME", 'length' => 64, 'attributes' => [ 'primary_key' => true ])
*/
public string $name;
/**
* @Field('name' => "TABLE_TYPE", 'length' => 64)
*/
public string $type;
/**
* @Field('name' => "ENGINE", 'length' => 64)
*/
public ? string $engine ;
/**
* @Field('name' => "VERSION", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $version;
/**
* @Field('name' => "ROW_FORMAT", 'length' => 10)
*/
public ? string $rowFormat;
/**
* @Field('name' => "TABLE_ROWS", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $rows;
/**
* @Field('name' => "AVG_ROW_LENGTH", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $averageRowLength;
/**
* @Field('name' => "DATA_LENGTH", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $dataLength;
/**
* @Field('name' => "MAX_DATA_LENGTH", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $maxDataLength;
/**
* @Field('name' => "INDEX_LENGTH", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $indexLength;
/**
* @Field('name' => "DATA_FREE", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $dataFree;
/**
* @Field('name' => "AUTO_INCREMENT", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $autoIncrement;
/**
* @Field('name' => "CREATE_TIME")
*/
public ? Datetime $createTime;
/**
* @Field('name' => "UPDATE_TIME")
*/
public ? Datetime $updateTime;
/**
* @Field('name' => "CHECK_TIME")
*/
public ? Datetime $checkTime;
/**
* @Field('name' => "TABLE_COLLATION", 'length' => 32)
*/
public ? string $tableCollation;
/**
* @Field('name' => "CHECKSUM", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? string $checksum;
/**
* @Field('name' => "CREATE_OPTIONS", 'length' => 2048)
*/
public ? string $createOptions;
/**
* @Field('name' => "TABLE_COMMENT", 'length' => 2048)
*/
public string $tableComment;
/**
* @Field('name' => "MAX_INDEX_LENGTH", 'type' => "bigint", 'length' => 21, 'attributes' => [ 'unsigned' => true ])
*/
public ? int $maxIndexLength;
/**
* @Field('name' => "TEMPORARY", 'length' => 1)
*/
public ? string $temporary;
/**
* @Relation('oneToMany', 'key' => 'name', 'foreignKey' => Column::field('tableName'), 'entity' => Column::class)
* @Where('TABLE_SCHEMA', Column::field('tableSchema'))
*/
public EntityCollection $columns;
}

View File

@ -35,12 +35,11 @@ trait EntityTrait {
public function __get(string $name)
{
$entityResolver = $this->resolveEntity();
# Resolve relations here if one is called
# @TODO REFACTOR THIS CODE ASAP !
if ( $this->isLoaded() ) {
if ( null !== ( $join= $entityResolver->searchFieldAnnotation($name, new Join() ) ) ) {
$vars = [];
@ -60,7 +59,7 @@ trait EntityTrait {
}
if ( null !== ( $relation = $entityResolver->searchFieldAnnotation($name, new Relation() ) ) ) {
$relationType = strtolower(str_replace(['-', '_'], '', $relation->type));
$relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type));
$order = $entityResolver->searchFieldAnnotationList($name, new OrderBy() );
$where = $entityResolver->searchFieldAnnotationList($name, new Where() );
@ -117,7 +116,7 @@ trait EntityTrait {
if ($relationRelation === null) {
throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}");
}
$repository = $relationRelation->entity()->repository();
$bridgeAlias = uniqid("bridge_");
@ -168,10 +167,7 @@ trait EntityTrait {
return;
}
}
else {
}
throw new \Exception(sprintf("[%s] - Undefined variable: %s", static::class, $name));
}
@ -183,7 +179,7 @@ trait EntityTrait {
if ( $this->isLoaded() && static::resolveEntity()->searchFieldAnnotation($name, new Relation() ) ) {
return true;
}
return isset($this->$name);
}
@ -250,6 +246,21 @@ trait EntityTrait {
return $this;
}
public function resetVirtualProperties() : self
{
foreach($this->resolveEntity()->properties as $prop => $property) {
if ( ! $property['builtin'] ) {
foreach($property['tags'] as $tag) {
if ( in_array(strtolower($tag['tag']), [ 'relation', 'join' ] ) ) {
unset($this->$prop);
}
}
}
}
return $this;
}
/**
* @Ignore
@ -391,7 +402,7 @@ trait EntityTrait {
{
$collection = new EntityCollection(...$arguments);
$collection->entityClass = static::class;
return $collection;
}

View File

@ -0,0 +1,143 @@
<?php
namespace Ulmus\Migration;
use Ulmus\Annotation\Property\Field;
class FieldDefinition {
public bool $nullable;
public bool $builtIn;
public string $type;
public array $tags;
public /* ? string */ $default;
public ? int $precision;
public ? int $length;
public ? string $update;
public function __construct(array $data)
{
$this->name = $data['name'];
$this->builtIn = $data['builtin'];
$this->tags = $data['tags'];
$field = $this->getFieldTag();
$this->type = $field->type ?? $data['type'];
$this->length = $field->length ?? null;
$this->precision = $field->precision ?? null;
$this->nullable = isset($field->nullable) ? $field->nullable : $data['nullable'];
$this->update = $field->attributes['update'] ?? null;
}
public function getSqlName() : string
{
return $this->getColumnName();
}
public function getSqlType(bool $typeOnly = false) : string
{
$type = $this->type;
$length = $this->length;
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" : "" ) . ")" : "" );
}
public function getSqlParams() : string
{
$default = $this->getDefault();
return implode(' ', array_filter([
$this->isUnsigned() ? "UNSIGNED" : null,
$this->nullable ? "NULL" : "NOT NULL",
$default ? "DEFAULT $default" : null,
$this->isAutoIncrement() ? "AUTO_INCREMENT" : null,
$this->isPrimaryKey() ? "PRIMARY KEY" : null,
]));
}
protected function getFieldTag() : ? Field
{
$field = array_filter($this->tags, function($item) {
return $item['object'] instanceof Field;
});
return array_pop($field)['object'];
}
protected function getColumnName() : ? string
{
return $this->getFieldTag()->name ?? $this->name;
}
protected function getDefault() : ? string
{
return $this->getFieldTag()->attributes['default'] ?? ( $this->nullable ? "NULL" : null ) ;
}
protected function isPrimaryKey() : bool
{
return $this->getFieldTag()->attributes['primary_key'] ?? false;
}
protected function isAutoIncrement() : bool
{
return $this->getFieldTag()->attributes['auto_increment'] ?? false;
}
protected function isUnique() : bool
{
return $this->getFieldTag()->attributes['unique'] ?? false;
}
protected function isUnsigned() : bool
{
return $this->getFieldTag()->attributes['unsigned'] ?? false;
}
}

View File

View File

@ -1,404 +0,0 @@
<?php namespace Alive\Storage\Sql;
use Alive\{
constructor,
Arrayobj
};
class QueryBuilder {
protected $fields;
protected $where;
protected $order_by;
protected $group_by;
protected $limit;
public static $syntax = [
'against' => 'AGAINST',
'and' => 'AND',
'as' => 'AS',
'charset' => 'CHARACTER SET',
'collate' => 'COLLATE',
'create' => 'CREATE',
'database' => 'DATABASE',
'delete' => 'DELETE FROM',
'distinct' => 'DISTINCT',
'drop' => 'DROP',
'engine' => 'ENGINE',
'!exist' => 'IF NOT EXISTS',
'exist' => 'IF EXISTS',
'explain' => 'EXPLAIN',
'from' => 'FROM',
'grant' => 'GRANT',
'grant_option' => 'GRANT OPTION',
'group_by' => 'GROUP BY',
'having' => 'HAVING',
'in' => 'IN',
'insert' => 'INSERT INTO',
'join' => 'JOIN',
'join-left' => 'LEFT',
'join-right' => 'RIGHT',
'join-inner' => 'INNER',
'join-full' => 'FULL',
'join-self' => 'SELF',
#'join-outer' => 'OUTER',
'join-cross' => 'CROSS',
'like' => 'LIKE',
'limit' => 'LIMIT',
'match' => 'MATCH',
'not_in' => 'NOT IN',
'on' => 'ON',
'on_table' => 'ON TABLE',
'or' => 'OR',
'order_by' => 'ORDER BY',
'offset' => 'OFFSET',
'revoke' => 'REVOKE',
'select' => 'SELECT',
'set' => 'SET',
'table' => 'TABLE',
'table_charset' => 'DEFAULT CHARSET',
'to' => 'TO',
'update' => 'UPDATE',
'values' => 'VALUES',
'where' => 'WHERE'
];
static $escape_char = '`';
protected $compiled = [];
public static function select($param) {
$param = Arrayobj::make($param);
return static::prepare_array([
$param->if_has('explain' , static::$syntax['explain']),
static::$syntax['select'],
$param->if_has('distinct' , static::$syntax['distinct']),
static::group_fields($param['fields'] ?: '*'),
static::$syntax['from'],
static::full_tablename($param),
static::prepare_join($param['join']),
static::prepare_where($param['where'], false, $param->ternary('escaped', true)),
$param->if_has('group_by' , static::prepare_group($param['group_by'], $param['alias'] ?? null)),
$param->if_has('having' , static::$syntax['having']." {$param['having']}"),
/* @todo UNION | INTERSECT | EXCEPT GOES HERE !*/
$param->if_has('order_by' , static::prepare_order($param['order_by'], $param['alias'] ?? null)),
static::prepare_limit($param)
]);
}
/**
* This function will translate parameters into a "create database" or "create table", depending
* on given param.
*
* @param array $param 'subject': table or database
*
* @return Type Description
*/
public static function create($param) {
$param = Arrayobj::make($param);
return strtolower( $param['subject'] ) === 'table' ? static::create_table($param) : static::create_database($param);
}
public static function create_table($param) {
$param = is_array($param) ? Arrayobj::make($param) : $param;
return static::prepare_array([
static::$syntax['create'],
static::$syntax['table'],
$param->if_has('!exist', static::$syntax['!exist']),
static::full_tablename($param),
static::group_create_fields($param->mandatory('fields'), true),
$param->if_has('collation' , static::$syntax['collate']." {$param['collation']}" )
]);
}
public static function create_database($param) {
$param = is_array($param) ? Arrayobj::make($param) : $param;
return static::prepare_array([
static::$syntax['create'],
static::$syntax['database'],
$param->if_has('!exist', static::$syntax['!exist']),
static::escape( $param->mandatory('database') )
]);
}
public static function insert($param) {
$param = Arrayobj::make($param);
$field_label = static::group_fields( $param->mandatory('fields'), true, true );
$field_values = static::group_values( $param->mandatory('values'), $param['escaped'] ?: false );
return static::prepare_array([
static::$syntax['insert'],
static::full_tablename($param),
$field_label,
static::$syntax['values'],
$field_values
]);
}
public static function grant($param) {
$param = Arrayobj::make($param);
$field_label = static::group_fields( $param->mandatory('privileges') );
$users = static::group_fields( $param->mandatory('users') );
return static::prepare_array([
static::$syntax['grant'],
$field_label,
static::$syntax['on_table'],
static::full_tablename($param),
static::$syntax['to'],
$users,
$param->if_has('grant_option', static::$syntax['grant_option'])
]);
}
public static function delete($param) {
$param = Arrayobj::make($param);
return static::prepare_array([
static::$syntax['delete'],
static::full_tablename($param),
static::prepare_where($param['where'], false, $param->ternary('escaped', true)),
static::prepare_order($param),
static::prepare_limit($param)
]);
}
public static function update($param) {
$param = Arrayobj::make($param);
$fields = static::group_values_and_fields($param->mandatory('fields'), $param->mandatory('values'));
return static::prepare_array([
static::$syntax['update'],
static::full_tablename($param),
static::$syntax['set'],
$fields,
static::prepare_where($param['where'])
]);
}
public static function drop($param) {
$param = Arrayobj::make($param);
return static::prepare_array([
static::$syntax['drop'],
$param->exist('table_name') ? static::$syntax['table']." ".static::full_tablename($param) : static::$syntax['database']." ".static::escape($param->mandatory('database'))
]);
}
public static function full_tablename($param) {
is_array($param) && ($param = Arrayobj::make($param));
return $param->if_has('database', static::escape($param['database']).".") . static::escape($param->mandatory('table_name')) . $param->if_has('alias', " ".static::$syntax['as']." " . $param['alias']);
}
public static function group_fields($fields, $enclose = false, $escape = false) {
if (is_array($fields)) {
return ($enclose ? "(" : "") .implode(', ', $escape ? array_map(function($item){ return static::escape($item); }, $fields) : $fields).($enclose ? ")" : "");
}
else {
return $escape ? static::escape($fields) : $fields;
}
}
public static function group_create_fields($fields, $enclose = false) {
if (is_array($fields)) {
$retval = [];
foreach($fields as $key => $value) {
$retval[] = static::escape($key)." ".$value;
}
return ($enclose ? "(" : "") .implode(', ', $retval).($enclose ? ")" : "");
}
else {
return $fields;
}
}
public static function group_values($values, $escaped = false) {
$tmp = array_pop($values);
array_push($values, $tmp);
# Are we dealing with an array of values ?
if ( is_array($tmp) ) {
$retval = [];
foreach($values as $item) {
$retval[] = implode(', ', $escaped ? $item : static::escape_values($item) );
}
return "(".implode('), (', $retval).")";
}
else {
return "(".implode(', ', $escaped ? $values : static::escape_values($values)).")";
}
}
public static function escape_values($values) {
$type_function = function(& $item) {
switch( $t = gettype($item) ) {
case "boolean":
$item = $item ? 1 : 0;
break;
case "double":
case "integer":
break;
case "NULL":
$item = "NULL";
break;
case "string":
$item = "\"$item\"";
break;
}
return $item;
};
return is_array($values) ? array_map($type_function, $values) : $type_function($values);
}
public static function group_values_and_fields($fields, $values) {
$retval = [];
foreach($fields as $key => $item) {
$retval[] = "{$item} = {$values[$key]}";
}
return implode(', ', $retval);
}
public static function prepare_array($sql) {
return implode(" ", array_filter($sql)).";";
}
public static function prepare_where($where, $recursion = false, $escaped = false) {
$retval = [];
if (is_array($where)) {
$count = count($where);
for($i = 0; $i < $count; $i++) {
$item = $where[$i];
if ( ! Arrayobj::array_is_associative($item) ) {
$retval[] = "(".static::prepare_where($item, true, $escaped).")";
}
else {
$comparison = (isset($item['comparison']) ? $item['comparison'] : "=");
# are we having an IN comparison here ...
if ( $is_array = (is_array($item['value']) && count($item['value']) > 1) ) {
switch ($item['comparison']) {
case '=':
$comparison = '=';
break;
case '!=':
$comparison = 'not_in';
break;
}
}
$value = static::group_fields($item['value'], true);
switch($comparison) {
case 'match':
$retval[] = static::$syntax[$comparison].' ('.static::fieldname($item['field'], $item['alias'] ?? null).") ".static::$syntax['against'].
" (".(!$escaped || $is_array ? $value : static::escape_values($value))." IN BOOLEAN MODE)".
($i + 1 < $count ? " ".static::$syntax[ isset($item['operator']) ? $item['operator'] : "and" ] : "");
break;
default:
$retval[] = static::fieldname($item['field'], $item['alias'] ?? null)." " . ( isset(static::$syntax[$comparison]) ? static::$syntax[$comparison] : $comparison) .
" ".(!$escaped || $is_array ? $value : static::escape_values($value)).
($i + 1 < $count ? " ".static::$syntax[ isset($item['operator']) ? $item['operator'] : "and" ] : "");
break;
}
}
}
}
return $retval ? ($recursion ? "" : static::$syntax['where'] . " ") . implode(" ", $retval ) : "";
}
public static function prepare_join($joins) {
$retval = [];
if ( is_array($joins) ) {
$count = count($joins);
for($i = 0; $i < $count; $i++) {
$join = [];
$table = Arrayobj::make([
'table_name' => $joins[$i]['table'],
'alias' => $joins[$i]['alias_right']
]);
$join[] = static::$syntax[ "join-".$joins[$i]['type'] ] ?? $joins[$i]['type'];
$join[] = static::$syntax[ 'join' ];
$join[] = static::full_tablename($table);
$join[] = static::$syntax[ 'on' ];
foreach($joins[$i]['fields'] as $left_field => $right_field) {
#$join[] = $joins[$i]['alias_left'].".".static::escape($left_field);
$join[] = static::fieldname($left_field, $joins[$i]['alias_left']);
$join[] = $joins[$i]['comparison'];
$join[] = static::fieldname($right_field, $joins[$i]['alias_right']);
}
$retval[] = implode(' ', $join);
}
}
return implode(' ', $retval);
}
public static function prepare_order($order, $alias = null)
{
$retval = [];
if (is_array($order)) {
foreach($order as $item) {
$retval[] = static::fieldname($item['field'], $alias).( !empty($item['order']) ? " ".$item['order'] : "" );
}
}
return $retval ? static::$syntax['order_by']." ".implode(', ', $retval) : "";
}
public static function prepare_group($group)
{
return $group ? static::$syntax['group_by']." ".( is_array($group) ? implode(', ', $group) : $group ) : "";
}
public static function prepare_limit($param)
{
return implode(' ', array_filter([
$param->if_has('limit' , static::$syntax['limit'] ." {$param['limit']}"),
$param->if_has('offset', static::$syntax['offset']." {$param['offset']}")
]));
}
public static function fieldname($field, $alias = null)
{
return strpos($field, '.') ? $field : (!empty($alias) ? $alias."." : "").static::escape($field);
}
public static function escape($field)
{
return static::$escape_char . str_replace(static::$escape_char, '', $field) . static::$escape_char;
}
}

37
src/Query/Create.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace Ulmus\Query;
use Ulmus\Annotation,
Ulmus\Common\EntityField;
class Create extends Fragment {
const SQL_TOKEN = "CREATE TABLE";
public string $table;
public string $engine;
public int $order = -80;
public bool $skipExisting = true;
public array $fieldList;
public function render() : string
{
return $this->renderSegments([
static::SQL_TOKEN, $this->renderTables($this->table),
$this->renderFields(),
]);
}
public function renderFields() : string
{
return "(" . PHP_EOL . implode(",".PHP_EOL, array_map(function($field) {
return " ".EntityField::generateCreateColumn($field);
}, $this->fieldList)) . PHP_EOL . ")";
}
}

19
src/Query/Engine.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Query;
class Engine extends Fragment {
const SQL_TOKEN = "ENGINE";
public int $order = 80;
public string $engine;
public function render() : string
{
return $this->renderSegments([
static::SQL_TOKEN, $this->engine,
], "=");
}
}

View File

@ -322,6 +322,42 @@ class QueryBuilder
return $this;
}
public function create(array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
{
if ( null === $this->getFragment(Query\Create::class) ) {
if ( $schema ) {
$table = "\"$schema\".$table";
}
if ( $database ) {
$table = "\"$database\".$table";
}
$create = new Query\Create();
$this->push($create);
$create->fieldList = $fieldlist;
$create->table = $table;
}
else {
throw new \Exception("A create SQL fragment was already found within the query builder");
}
return $this;
}
public function engine(string $value) : self
{
if ( null === $engine = $this->getFragment(Query\Engine::class) ) {
$engine = new Query\Engine();
$this->push($engine);
}
$engine->engine = $value;
return $this;
}
public function push(Query\Fragment $queryFragment) : self
{

View File

@ -7,7 +7,7 @@ use Ulmus\Common\EntityResolver;
class Repository
{
use EventTrait, Repository\ConditionTrait;
const DEFAULT_ALIAS = "this";
public ? ConnectionAdapter $adapter;
@ -19,14 +19,14 @@ class Repository
public string $alias;
public string $entityClass;
public array $events = [];
public function __construct(string $entity, string $alias = self::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) {
$this->entityClass = $entity;
$this->alias = $alias;
$this->entityResolver = Ulmus::resolveEntity($entity);
$this->adapter = $adapter ?? $this->entityResolver->databaseAdapter() ?? Ulmus::$defaultAdapter;
$this->adapter = $adapter ?? $this->entityResolver->databaseAdapter();
$this->queryBuilder = new QueryBuilder();
}
@ -44,12 +44,12 @@ class Repository
{
return $this->where($field, $value)->loadOne();
}
public function loadFromPk($value, /* ? stringable */ $primaryKey = null) : ? object
{
return $primaryKey ? $this->loadOneFromField($primaryKey, $value) : $this->wherePrimaryKey($value)->loadOne();
}
public function loadAll() : EntityCollection
{
return $this->collectionFromQuery();
@ -59,20 +59,20 @@ class Repository
{
return $this->where($field, $value)->collectionFromQuery();
}
public function count() : int
{
if ( null !== $select = $this->queryBuilder->getFragment(Query\Select::class) ) {
$this->queryBuilder->removeFragment($select);
}
if ( $this->queryBuilder->getFragment(Query\GroupBy::class) ) {
$this->select( "DISTINCT COUNT(*) OVER ()" );
}
else {
$this->select(Common\Sql::function("COUNT", "*"));
}
$this->selectSqlQuery();
$this->finalizeQuery();
@ -80,13 +80,13 @@ class Repository
return Ulmus::runSelectQuery($this->queryBuilder, $this->adapter)->fetchColumn(0);
}
protected function deleteOne()
protected function deleteOne()
{
return $this->limit(1)->deleteSqlQuery()->runQuery();
}
protected function deleteAll()
{
protected function deleteAll()
{
return $this->deleteSqlQuery()->runQuery();
}
@ -95,16 +95,16 @@ class Repository
if ( $value !== 0 && empty($value) ) {
throw new Exception\EntityPrimaryKeyUnknown("A primary key value has to be defined to delete an item.");
}
return (bool) $this->wherePrimaryKey($value)->deleteOne()->rowCount();
}
public function destroy(object $entity) : bool
{
if ( ! $this->matchEntity($entity) ) {
throw new \Exception("Your entity class `" . get_class($entity) . "` cannot match entity type of repository `{$this->entityClass}`");
}
$primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
if ( $primaryKeyDefinition === null ) {
@ -112,42 +112,41 @@ class Repository
}
else {
$pkField = key($primaryKeyDefinition);
return $this->deleteFromPk($entity->$pkField);
}
return false;
}
public function destroyAll(EntityCollection $collection) : void
public function destroyAll(EntityCollection $collection) : void
{
foreach($collection as $entity) {
$this->destroy($entity);
}
}
public function save(object $entity) : bool
{
{
if ( ! $this->matchEntity($entity) ) {
throw new \Exception("Your entity class `" . get_class($entity) . "` cannot match entity type of repository `{$this->entityClass}`");
}
$dataset = $entity->toArray();
$primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
if ( ! $entity->isLoaded() ) {
$statement = $this->insertSqlQuery($dataset)->runQuery();
if ( ( 0 !== $statement->lastInsertId ) &&
( null !== $primaryKeyDefinition )) {
$pkField = key($primaryKeyDefinition);
$dataset[$pkField] = $statement->lastInsertId;
}
$entity->entityFillFromDataset($dataset, true);
return true;
}
else {
if ( $primaryKeyDefinition === null ) {
@ -160,46 +159,52 @@ class Repository
$this->where($pkFieldName, $dataset[$pkFieldName]);
$update = $this->updateSqlQuery($diff)->runQuery();
$entity->entityFillFromDataset($dataset);
return $update ? (bool) $update->rowCount() : false;
}
}
return false;
}
public function saveAll(EntityCollection $collection) : void
public function saveAll(EntityCollection $collection) : void
{
foreach($collection as $entity) {
$this->save($entity);
}
}
public function truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self
{
$schema = $schema ?: $this->entityResolver->schemaName();
$this->queryBuilder->truncate($this->escapeTable($table ?: $this->entityResolver->tableName()), $alias ?: $this->alias, $this->escapeDatabase($this->adapter->adapter()->database), $schema ? $this->escapeSchema($schema) : null);
$this->queryBuilder->truncate($this->escapeTable($table ?: $this->entityResolver->tableName()), $alias ?: $this->alias, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null);
$this->finalizeQuery();
$result = Ulmus::runSelectQuery($this->queryBuilder, $this->adapter);
return $this;
}
public function createTable()
{
return $this->createSqlQuery()->runQuery();
}
public function generateDatasetDiff(object $entity) : array
{
return array_diff_assoc( array_change_key_case($entity->toArray()), array_change_key_case($entity->entityGetDataset(false, true)) );
}
public function yield() : \Generator
{
$class = $this->entityClass;
$this->selectSqlQuery();
$this->finalizeQuery();
foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) {
@ -210,74 +215,73 @@ class Repository
public function select(/*array|Stringable*/ $fields) : self
{
$this->queryBuilder->select($fields);
return $this;
}
public function insert(array $fieldlist, string $table, string $alias, ? string $schema) : self
{
$this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $alias, $schema);
$this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $alias, $this->escapedDatabase(), $schema);
return $this;
}
public function values(array $dataset) : self
{
$this->queryBuilder->values($dataset);
return $this;
}
public function update(string $table, string $alias, ? string $schema) : self
{
$this->queryBuilder->update($this->escapeTable($table), $alias, $schema);
$this->queryBuilder->update($this->escapeTable($table), $alias, $this->escapedDatabase(), $schema);
return $this;
}
public function set(array $dataset) : self
{
$this->queryBuilder->set($dataset);
return $this;
}
public function delete() : self
{
$this->queryBuilder->delete($this->alias);
return $this;
}
public function from(string $table, ? string $alias, ? string $schema) : self
{
$this->queryBuilder->from($this->escapeTable($table), $alias, $this->escapeDatabase($this->adapter->adapter()->database), $schema ? $this->escapeSchema($schema) : null);
$this->queryBuilder->from($this->escapeTable($table), $alias, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null);
return $this;
}
public function join(string $type, $table, $field, $value, ? string $alias = null, ? callable $callback = null) : self
{
$join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, false, $alias);
if ( $callback ) {
$callback($join);
}
return $this;
}
public function outerJoin(string $type, $table, $field, $value, ? string $alias = null, ? callable $callback = null) : self
{
$join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, true, $alias);
if ( $callback ) {
$callback($join);
}
return $this;
}
public function match() : self
{
@ -297,11 +301,11 @@ class Repository
{
}
public function groupBy($field) : self
{
$this->queryBuilder->groupBy($field);
return $this;
}
@ -310,14 +314,14 @@ class Repository
foreach($groups as $field ) {
$this->groupBy($field);
}
return $this;
}
public function orderBy($field, ? string $direction = null) : self
{
$this->queryBuilder->orderBy($field, $direction);
return $this;
}
@ -334,26 +338,24 @@ class Repository
foreach($orderList as $field => $direction) {
$this->orderBy($field, $direction);
}
return $this;
}
public function limit(int $value) : self
{
$this->queryBuilder->limit($value);
return $this;
}
public function offset(int $value) : self
{
$this->queryBuilder->offset($value);
return $this;
}
/* @TODO */
public function commit() : self
{
@ -371,88 +373,83 @@ class Repository
if ( null === $primaryKeyField = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField() ) {
throw new Exception\EntityPrimaryKeyUnknown("Entity has no field containing attributes 'primary_key'");
}
$pkField = key($primaryKeyField);
return $this->where($primaryKeyField[$pkField]->name ?? $pkField, $value);
}
public function withJoin(/*stringable|array*/ $fields) : self
public function withJoin(/*string|array*/ $fields) : self
{
$resolvedEntity = Ulmus::resolveEntity($this->entityClass);
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
$this->select("{$this->alias}.*");
}
foreach((array) $fields as $item) {
if ( null !== $join = $resolvedEntity->searchFieldAnnotation($item, new Annotation\Property\Join) ) {
if ( null !== $join = $this->entityResolver->searchFieldAnnotation($item, new Annotation\Property\Join) ) {
$alias = $join->alias ?? $item;
$entity = $join->entity ?? $resolvedEntity->properties[$item]['type'];
$entity = $join->entity ?? $this->entityResolver->properties[$item]['type'];
foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
$this->select("$alias.$key as {$alias}\${$field['name']}");
}
$key = is_string($join->key) ? $this->entityClass::field($join->key) : $join->key;
$foreignKey = is_string($join->foreignKey) ? $entity::field($join->foreignKey, $alias) : $join->foreignKey;
$this->join($join->type, $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias);
}
else {
throw new \Exception("You referenced field `$item` which do not exist or do not contain a valid @Join annotation.");
}
}
return $this;
}
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self
{
$likes = $searchRequest->likes();
$wheres = $searchRequest->wheres();
$groups = $searchRequest->groups();
$searchRequest->filter( $this )
->wheres($wheres, Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
->likes($likes, Query\Where::CONDITION_OR)
->groups($groups);
$searchRequest->count = $searchRequest->skipCount ? 0 : (clone $this)->count();
return $searchRequest
->filter($this)
->wheres($wheres, Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
->likes($likes, Query\Where::CONDITION_OR)
->groups($groups)
$searchRequest->count = $searchRequest->filter( clone $this )
->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
->groups($searchRequest->groups())
->count();
return $searchRequest->filter($this)
->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
->orders($searchRequest->orders())
->groups($searchRequest->groups())
->offset($searchRequest->offset())
->limit($searchRequest->limit());
}
public function collectionFromQuery(? string $entityClass = null) : EntityCollection
{
$class = $entityClass ?: $this->entityClass;
$entityCollection = $class::entityCollection();
$entityCollection = $this->instanciateEntityCollection();
$this->selectSqlQuery();
$this->finalizeQuery();
foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) {
$entityCollection->append( ( new $class() )->entityFillFromDataset($entityData) );
$entityCollection->append( ( new $class() )->resetVirtualProperties()->entityFillFromDataset($entityData) );
}
$this->eventExecute(Event\Repository\CollectionFromQueryInterface::class, $entityCollection);
$this->eventExecute(Event\RepositoryCollectionFromQueryInterface::class, $entityCollection);
return $entityCollection;
}
public function arrayFromQuery() : array
{
$this->selectSqlQuery();
$this->finalizeQuery();
return Ulmus::datasetQueryBuilder($this->queryBuilder, $this->adapter);
@ -461,7 +458,7 @@ class Repository
public function runQuery() : ? \PDOStatement
{
$this->finalizeQuery();
return Ulmus::runQuery($this->queryBuilder, $this->adapter);
}
@ -482,18 +479,18 @@ class Repository
return $this;
}
protected function updateSqlQuery(array $dataset) : self
{
if ( null === $this->queryBuilder->getFragment(Query\Update::class) ) {
$this->update($this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
}
$this->set($dataset);
return $this;
}
protected function selectSqlQuery() : self
{
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
@ -519,7 +516,22 @@ class Repository
return $this;
}
public function createSqlQuery() : self
{
if ( null === $this->queryBuilder->getFragment(Query\Create::class) ) {
$this->queryBuilder->create($this->entityResolver->fieldList(), $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);
}
}
return $this;
}
protected function fromRow($row) : self
{
@ -529,30 +541,51 @@ class Repository
{
}
public function getSqlQuery(bool $flush = true) : string
{
$result = $this->queryBuilder->render();
$flush and $this->queryBuilder->reset();
return $result;
}
public function instanciateEntityCollection(...$arguments) : EntityCollection
{
return $this->entityClass::entityCollection(...$arguments);
}
public function instanciateEntity() : object
{
return new $this->entityClass();
}
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);
}
protected function matchEntity(object $entity) {
return get_class($entity) === $this->entityClass;
}
protected function finalizeQuery() : void {}
}
}