From 7083cf5d2735fc8ae4e7bcb6f29d912def89afe3 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Sun, 26 Jul 2020 23:40:25 +0200 Subject: [PATCH 001/151] [update] migrate aggregate and union models to data package --- src/Model/Aggregate.php | 334 +++++++++++++++++++++++++ src/Model/Union.php | 462 +++++++++++++++++++++++++++++++++++ tests/Model/Client.php | 2 + tests/Model/Invoice.php | 21 ++ tests/Model/Payment.php | 22 ++ tests/Model/Transaction.php | 26 ++ tests/Model/Transaction2.php | 30 +++ tests/ModelAggregateTest.php | 230 +++++++++++++++++ tests/ModelUnionExprTest.php | 276 +++++++++++++++++++++ tests/ModelUnionTest.php | 240 ++++++++++++++++++ tests/ReportTest.php | 57 +++++ 11 files changed, 1700 insertions(+) create mode 100644 src/Model/Aggregate.php create mode 100644 src/Model/Union.php create mode 100644 tests/Model/Invoice.php create mode 100644 tests/Model/Payment.php create mode 100644 tests/Model/Transaction.php create mode 100644 tests/Model/Transaction2.php create mode 100644 tests/ModelAggregateTest.php create mode 100644 tests/ModelUnionExprTest.php create mode 100644 tests/ModelUnionTest.php create mode 100644 tests/ReportTest.php diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php new file mode 100644 index 000000000..d3bb68a93 --- /dev/null +++ b/src/Model/Aggregate.php @@ -0,0 +1,334 @@ +groupBy(['first','last'], ['salary'=>'sum([])']; + * + * your resulting model will have 3 fields: + * first, last, salary + * + * but when querying it will use the original model to calculate the query, then add grouping and aggregates. + * + * If you wish you can add more fields, which will be passed through: + * $aggregate->addField('middle'); + * + * If this field exist in the original model it will be added and you'll get exception otherwise. Finally you are + * permitted to add expressions. + * + * The base model must not be Union model or another Aggregate model, however it's possible to use Aggregate model as nestedModel inside Union model. + * Union model implements identical grouping rule on its own. + * + * You can also pass seed (for example field type) when aggregating: + * $aggregate->groupBy(['first','last'], ['salary' => ['sum([])', 'type'=>'money']]; + */ +class Aggregate extends Model +{ + /** @const string */ + public const HOOK_AFTER_SELECT = self::class . '@afterSelect'; + + /** + * @deprecated use HOOK_AFTER_SELECT instead - will be removed dec-2020 + */ + public const HOOK_AFTER_GROUP_SELECT = self::HOOK_AFTER_SELECT; + + /** + * Aggregate model should always be read-only. + * + * @var bool + */ + public $read_only = true; + + /** @var Model */ + public $master_model; + + /** @var string */ + public $id_field; + + /** @var array */ + public $group = []; + + /** @var array */ + public $aggregate = []; + + /** @var array */ + public $system_fields = []; + + /** + * Constructor. + */ + public function __construct(Model $model, array $defaults = []) + { + $this->master_model = $model; + $this->table = $model->table; + + //$this->_default_class_addExpression = $model->_default_class_addExpression; + parent::__construct($model->persistence, $defaults); + + // always use table prefixes for this model + $this->persistence_data['use_table_prefixes'] = true; + } + + /** + * Specify a single field or array of fields on which we will group model. + * + * @param array $group Array of field names + * @param array $aggregate Array of aggregate mapping + * + * @return $this + */ + public function groupBy(array $group, array $aggregate = []) + { + $this->group = $group; + $this->aggregate = $aggregate; + + $this->system_fields = array_unique($this->system_fields + $group); + foreach ($group as $fieldName) { + $this->addField($fieldName); + } + + foreach ($aggregate as $fieldName => $expr) { + $seed = is_array($expr) ? $expr : [$expr]; + + $args = []; + // if field originally defined in the parent model, then it can be used as part of expression + if ($this->master_model->hasField($fieldName)) { + $args = [$this->master_model->getField($fieldName)]; // @TODO Probably need cloning here + } + + $seed['expr'] = $this->master_model->expr($seed[0] ?? $seed['expr'], $args); + + // now add the expressions here + $this->addExpression($fieldName, $seed); + } + + return $this; + } + + /** + * Return reference field. + * + * @param string $link + */ + public function getRef($link): Reference + { + return $this->master_model->getRef($link); + } + + /** + * Adds new field into model. + * + * @param string $name + * @param array|object $seed + * + * @return Field + */ + public function addField($name, $seed = []) + { + $seed = is_array($seed) ? $seed : [$seed]; + + if (isset($seed[0]) && $seed[0] instanceof FieldSqlExpression) { + return parent::addField($name, $seed[0]); + } + + if ($seed['never_persist'] ?? false) { + return parent::addField($name, $seed); + } + + if ($this->master_model->hasField($name)) { + $field = clone $this->master_model->getField($name); + $field->owner = null; // will be new owner + } else { + $field = null; + } + + return $field + ? parent::addField($name, $field)->setDefaults($seed) + : parent::addField($name, $seed); + } + + /** + * Given a query, will add safe fields in. + */ + public function queryFields(Query $query, array $fields = []): Query + { + $this->persistence->initQueryFields($this, $query, $fields); + + return $query; + } + + /** + * Adds grouping in query. + */ + public function addGrouping(Query $query) + { + // use table alias of master model + $this->table_alias = $this->master_model->table_alias; + + foreach ($this->group as $field) { + if ($this->master_model->hasField($field)) { + $expression = $this->master_model->getField($field); + } else { + $expression = $this->expr($field); + } + + $query->group($expression); + } + } + + /** + * Sets limit. + * + * @param int $count + * @param int|null $offset + * + * @return $this + * + * @todo Incorrect implementation + */ + public function setLimit($count, $offset = null) + { + $this->master_model->setLimit($count, $offset); + + return $this; + } + + /** + * Sets order. + * + * @param mixed $field + * @param bool|null $desc + * + * @return $this + * + * @todo Incorrect implementation + */ + public function setOrder($field, $desc = null) + { + $this->master_model->setOrder($field, $desc); + + return $this; + } + + /** + * Execute action. + * + * @param string $mode + * @param array $args + * + * @return Query + */ + public function action($mode, $args = []) + { + $subquery = null; + switch ($mode) { + case 'select': + $fields = $this->only_fields ?: array_keys($this->getFields()); + + // select but no need your fields + $query = $this->master_model->action($mode, [false]); + $query = $this->queryFields($query, array_unique($fields + $this->system_fields)); + + $this->addGrouping($query); + $this->initQueryConditions($query); + + $this->hook(self::HOOK_AFTER_SELECT, [$query]); + + return $query; + case 'count': + $query = $this->master_model->action($mode, $args); + + $query->reset('field')->field($this->expr('1')); + $this->addGrouping($query); + + $this->hook(self::HOOK_AFTER_SELECT, [$query]); + + return $query->dsql()->field('count(*)')->table($this->expr('([]) der', [$query])); + case 'field': + if (!isset($args[0])) { + throw (new Exception('This action requires one argument with field name')) + ->addMoreInfo('mode', $mode); + } + + if (!is_string($args[0])) { + throw (new Exception('action "field" only support string fields')) + ->addMoreInfo('field', $args[0]); + } + + $subquery = $this->getSubQuery([$args[0]]); + + break; + case 'fx': + $subquery = $this->getSubAction('fx', [$args[0], $args[1], 'alias' => 'val']); + + $args = [$args[0], $this->expr('val')]; + + break; + default: + throw (new Exception('Aggregate model does not support this action')) + ->addMoreInfo('mode', $mode); + } + + // Substitute FROM table with our subquery expression + return parent::action($mode, $args)->reset('table')->table($subquery); + } + + /** + * Our own way applying conditions, where we use "having" for + * fields. + */ + public function initQueryConditions(Query $query, Model\Scope\AbstractScope $condition = null): void + { + $condition = $condition ?? $this->scope(); + + if (!$condition->isEmpty()) { + // peel off the single nested scopes to convert (((field = value))) to field = value + $condition = $condition->simplify(); + + // simple condition + if ($condition instanceof Model\Scope\Condition) { + $query = $query->having(...$condition->toQueryArguments()); + } + + // nested conditions + if ($condition instanceof Model\Scope) { + $expression = $condition->isOr() ? $query->orExpr() : $query->andExpr(); + + foreach ($condition->getNestedConditions() as $nestedCondition) { + $this->initQueryConditions($expression, $nestedCondition); + } + + $query = $query->having($expression); + } + } + } + + // {{{ Debug Methods + + /** + * Returns array with useful debug info for var_dump. + */ + public function __debugInfo(): array + { + return array_merge(parent::__debugInfo(), [ + 'group' => $this->group, + 'aggregate' => $this->aggregate, + 'master_model' => $this->master_model->__debugInfo(), + ]); + } + + // }}} +} diff --git a/src/Model/Union.php b/src/Model/Union.php new file mode 100644 index 000000000..e10781d10 --- /dev/null +++ b/src/Model/Union.php @@ -0,0 +1,462 @@ +'total_gross'] ] , [$m2, []] ]; + * + * @var array + */ + public $union = []; + + /** + * Union normally does not have ID field. Setting this to null will + * disable various per-id operations, such as load(). + * + * If you can define unique ID field, you can specify it inside your + * union model. + * + * @var string + */ + public $id_field; + + /** + * When aggregation happens, this field will contain list of fields + * we use in groupBy. Multiple fields can be in the array. All + * the remaining fields will be hidden (marked as system()) and + * have their "aggregates" added into the selectQuery (if possible). + * + * @var array|string + */ + public $group; + + /** + * When grouping, the functions will be applied as per aggregate + * fields, e.g. 'balance'=>['sum', 'amount']. + * + * You can also use Expression instead of array. + * + * @var array + */ + public $aggregate = []; + + /** @var string Derived table alias */ + public $table = 'derivedTable'; + + /** + * For a sub-model with a specified mapping, return expression + * that represents a field. + * + * @return Field|Expression + */ + public function getFieldExpr(Model $model, string $fieldName, string $expr = null) + { + if ($model->hasField($fieldName)) { + $field = $model->getField($fieldName); + } else { + $field = $this->expr('NULL'); + } + + // Some fields are re-mapped for this nested model + if ($expr !== null) { + $field = $model->expr($expr, [$field]); + } + + return $field; + } + + /** + * Configures nested models to have a specified set of fields + * available. + */ + public function getSubQuery(array $fields): Expression + { + $cnt = 0; + $expr = []; + $args = []; + + foreach ($this->union as $n => [$nestedModel, $fieldMap]) { + // map fields for related model + $queryFieldMap = []; + foreach ($fields as $fieldName) { + try { + // Union can be joined with additional + // table/query and we don't touch those + // fields + + if (!$this->hasField($fieldName)) { + $queryFieldMap[$fieldName] = $nestedModel->expr('NULL'); + + continue; + } + + if ($this->getField($fieldName)->join || $this->getField($fieldName)->never_persist) { + continue; + } + + // Union can have some fields defined as expressions. We don't touch those either. + // Imants: I have no idea why this condition was set, but it's limiting our ability + // to use expression fields in mapping + if ($this->getField($fieldName) instanceof FieldSqlExpression && !isset($this->aggregate[$fieldName])) { + continue; + } + + $field = $this->getFieldExpr($nestedModel, $fieldName, $fieldMap[$fieldName] ?? null); + + if (isset($this->aggregate[$fieldName])) { + $seed = (array) $this->aggregate[$fieldName]; + + // first element of seed should be expression itself + $field = $nestedModel->expr($seed[0], [$field]); + } + + $queryFieldMap[$fieldName] = $field; + } catch (\atk4\core\Exception $e) { + throw $e->addMoreInfo('model', $n); + } + } + + // now prepare query + $expr[] = '[' . $cnt . ']'; + $query = $this->persistence->action($nestedModel, 'select', [false]); + + if ($nestedModel instanceof self) { + $subquery = $nestedModel->getSubQuery($fields); + //$query = parent::action($mode, $args); + $query->reset('table')->table($subquery); + + if (isset($nestedModel->group)) { + $query->group($nestedModel->group); + } + } + + $query->field($queryFieldMap); + + // also for sub-queries + if ($this->group) { + if (is_array($this->group)) { + foreach ($this->group as $group) { + if (isset($fieldMap[$group])) { + $query->group($nestedModel->expr($fieldMap[$group])); + } elseif ($nestedModel->hasField($group)) { + $query->group($nestedModel->getField($group)); + } + } + } elseif (isset($fieldMap[$this->group])) { + $query->group($nestedModel->expr($fieldMap[$this->group])); + } else { + $query->group($this->group); + } + } + + // subquery should not be wrapped in parenthesis, SQLite is especially picky about that + $query->allowToWrapInParenthesis = false; + + $args[$cnt++] = $query; + } + + // last element is table name itself + $args[$cnt] = $this->table; + + return $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ') {' . $cnt . '}', $args); + } + + /** + * No description. + */ + public function getSubAction(string $action, array $act_arg = []): Expression + { + $cnt = 0; + $expr = []; + $args = []; + + foreach ($this->union as [$model, $mapping]) { + // now prepare query + $expr[] = '[' . $cnt . ']'; + if ($act_arg && isset($act_arg[1])) { + $a = $act_arg; + $a[1] = $this->getFieldExpr( + $model, + $a[1], + $mapping[$a[1]] ?? null + ); + $query = $model->action($action, $a); + } else { + $query = $model->action($action, $act_arg); + } + + // subquery should not be wrapped in parenthesis, SQLite is especially picky about that + $query->allowToWrapInParenthesis = false; + + $args[$cnt++] = $query; + } + + // last element is table name itself + $args[$cnt] = $this->table; + + return $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ') {' . $cnt . '}', $args); + } + + /** + * Execute action. + * + * @param string $mode + * @param array $args + * + * @return Query + */ + public function action($mode, $args = []) + { + $subquery = null; + switch ($mode) { + case 'select': + // get list of available fields + $fields = $this->only_fields ?: array_keys($this->getFields()); + foreach ($fields as $k => $field) { + if ($this->getField($field)->never_persist) { + unset($fields[$k]); + } + } + $subquery = $this->getSubQuery($fields); + $query = parent::action($mode, $args)->reset('table')->table($subquery); + + if (isset($this->group)) { + $query->group($this->group); + } + $this->hook(self::HOOK_AFTER_SELECT, [$query]); + + return $query; + case 'count': + $subquery = $this->getSubAction('count', ['alias' => 'cnt']); + + $mode = 'fx'; + $args = ['sum', $this->expr('{}', ['cnt'])]; + + break; + case 'field': + if (!isset($args[0])) { + throw (new Exception('This action requires one argument with field name')) + ->addMoreInfo('mode', $mode); + } + + if (!is_string($args[0])) { + throw (new Exception('action "field" only support string fields')) + ->addMoreInfo('field', $args[0]); + } + + $subquery = $this->getSubQuery([$args[0]]); + + break; + case 'fx': + $subquery = $this->getSubAction('fx', [$args[0], $args[1], 'alias' => 'val']); + + $args = [$args[0], $this->expr('{}', ['val'])]; + + break; + default: + throw (new Exception('Union model does not support this action')) + ->addMoreInfo('mode', $mode); + } + + // Substitute FROM table with our subquery expression + return parent::action($mode, $args)->reset('table')->table($subquery); + } + + /** + * Export model. + * + * @param array|null $fields Names of fields to export + * @param string $key_field Optional name of field which value we will use as array key + * @param bool $typecast_data Should we typecast exported data + */ + public function export($fields = null, $key_field = null, $typecast_data = true): array + { + if ($fields) { + $this->onlyFields($fields); + } + + $data = []; + foreach ($this->getIterator() as $row) { + $data[] = $row->get(); + } + + return $data; + } + + /** + * Adds nested model in union. + * + * @param string|Model $class model + * @param array $fieldMap Array of field mapping + */ + public function addNestedModel($class, array $fieldMap = []): Model + { + $nestedModel = $this->persistence->add($class); + + $this->union[] = [$nestedModel, $fieldMap]; + + return $nestedModel; + } + + /** + * Specify a single field or array of fields. + * + * @param string|array $group + * + * @return $this + */ + public function groupBy($group, array $aggregate = []) + { + $this->aggregate = $aggregate; + $this->group = $group; + + foreach ($aggregate as $fieldName => $seed) { + $seed = (array) $seed; + + $field = $this->hasField($fieldName) ? $this->getField($fieldName) : null; + + // first element of seed should be expression itself + if (isset($seed[0]) && is_string($seed[0])) { + $seed[0] = $this->expr($seed[0], $field ? [$field] : null); + } + + if ($field) { + $this->removeField($fieldName); + } + + $this->addExpression($fieldName, $seed); + } + + foreach ($this->union as [$nestedModel, $fieldMap]) { + if ($nestedModel instanceof self) { + $nestedModel->aggregate = $aggregate; + $nestedModel->group = $group; + } + } + + return $this; + } + + /** + * Adds condition. + * + * If Union model has such field, then add condition to it. + * Otherwise adds condition to all nested models. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @param bool $forceNested Should we add condition to all nested models? + * + * @return $this + */ + public function addCondition($key, $operator = null, $value = null, $forceNested = false) + { + if (func_num_args() === 1) { + return parent::addCondition($key); + } + + // if Union model has such field, then add condition to it + if ($this->hasField($key) && !$forceNested) { + return parent::addCondition(...func_get_args()); + } + + // otherwise add condition in all sub-models + foreach ($this->union as $n => [$nestedModel, $fieldMap]) { + try { + $field = $key; + + if (isset($fieldMap[$key])) { + // field is included in mapping - use mapping expression + $field = $fieldMap[$key] instanceof Expression + ? $fieldMap[$key] + : $this->expr($fieldMap[$key], $nestedModel->getFields()); + } elseif (is_string($key) && $nestedModel->hasField($key)) { + // model has such field - use that field directly + $field = $nestedModel->getField($key); + } else { + // we don't know what to do, so let's do nothing + continue; + } + + switch (func_num_args()) { + case 2: + $nestedModel->addCondition($field, $operator); + + break; + case 3: + case 4: + $nestedModel->addCondition($field, $operator, $value); + + break; + } + } catch (\atk4\core\Exception $e) { + throw $e->addMoreInfo('sub_model', $n); + } + } + + return $this; + } + + // {{{ Debug Methods + + /** + * Returns array with useful debug info for var_dump. + */ + public function __debugInfo(): array + { + $unionModels = []; + foreach ($this->union as [$nestedModel, $fieldMap]) { + $unionModels[get_class($nestedModel)] = array_merge( + ['fieldMap' => $fieldMap], + $nestedModel->__debugInfo() + ); + } + + return array_merge( + parent::__debugInfo(), + [ + 'group' => $this->group, + 'aggregate' => $this->aggregate, + 'unionModels' => $unionModels, + ] + ); + } + + // }}} +} diff --git a/tests/Model/Client.php b/tests/Model/Client.php index cae763c11..c7917706e 100644 --- a/tests/Model/Client.php +++ b/tests/Model/Client.php @@ -6,6 +6,8 @@ class Client extends User { + public $table = 'client'; + public function init(): void { parent::init(); diff --git a/tests/Model/Invoice.php b/tests/Model/Invoice.php new file mode 100644 index 000000000..510d02c44 --- /dev/null +++ b/tests/Model/Invoice.php @@ -0,0 +1,21 @@ +addField('name'); + + $this->hasOne('client_id', [Client::class]); + $this->addField('amount', ['type' => 'money']); + } +} diff --git a/tests/Model/Payment.php b/tests/Model/Payment.php new file mode 100644 index 000000000..1333cf23c --- /dev/null +++ b/tests/Model/Payment.php @@ -0,0 +1,22 @@ +addField('name'); + + $this->hasOne('client_id', [Client::class]); + $this->addField('amount', ['type' => 'money']); + } +} diff --git a/tests/Model/Transaction.php b/tests/Model/Transaction.php new file mode 100644 index 000000000..7c4f17bf6 --- /dev/null +++ b/tests/Model/Transaction.php @@ -0,0 +1,26 @@ +nestedInvoice = $this->addNestedModel(new Invoice()); + $this->nestedPayment = $this->addNestedModel(new Payment()); + + // next, define common fields + $this->addField('name'); + $this->addField('amount', ['type' => 'money']); + } +} diff --git a/tests/Model/Transaction2.php b/tests/Model/Transaction2.php new file mode 100644 index 000000000..e31cf8525 --- /dev/null +++ b/tests/Model/Transaction2.php @@ -0,0 +1,30 @@ +nestedInvoice = $this->addNestedModel(new Invoice(), ['amount' => '-[]']); + $this->nestedPayment = $this->addNestedModel(new Payment()); + + //$this->nestedInvoice->hasOne('client_id', [new Client()]); + //$this->nestedPayment->hasOne('client_id', [new Client()]); + + // next, define common fields + $this->addField('name'); + $this->addField('amount', ['type' => 'money']); + //$this->hasOne('client_id', [new Client()]); + } +} diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php new file mode 100644 index 000000000..e5fb97d92 --- /dev/null +++ b/tests/ModelAggregateTest.php @@ -0,0 +1,230 @@ + [ + ['name' => 'Vinny'], + ['name' => 'Zoe'], + ], + 'invoice' => [ + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], + ], + 'payment' => [ + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], + ], + ]; + + /** @var Aggregate */ + protected $aggregate; + + protected function setUp(): void + { + parent::setUp(); + $this->setDB($this->init_db); + + $m_invoice = new Model\Invoice($this->db); + $m_invoice->getRef('client_id')->addTitle(); + + $this->aggregate = new Aggregate($m_invoice); + $this->aggregate->addField('client'); + } + + public function testGroupSelect() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 'c' => 2], + ['client' => 'Zoe', 'client_id' => '2', 'c' => 1], + ], + $aggregate->export() + ); + } + + public function testGroupSelect2() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 'amount' => 19.0], + ['client' => 'Zoe', 'client_id' => '2', 'amount' => 4.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelect3() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'min' => ['expr' => 'min([amount])', 'type' => 'money'], + 'max' => ['expr' => 'max([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], // same as `s`, but reuse name `amount` + ]); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], + ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'min' => 4.0, 'max' => 4.0, 'amount' => 4.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelectExpr() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelectCondition() + { + $aggregate = $this->aggregate; + $aggregate->master_model->addCondition('name', 'chair purchase'); + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelectCondition2() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addCondition('double', '>', 10); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelectCondition3() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addCondition('double', 38); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelectCondition4() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addCondition('client_id', 2); + + $this->assertSame( + [ + ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], + $aggregate->export() + ); + } + + public function testGroupLimit() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + $aggregate->setLimit(1); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 'amount' => 19.0], + ], + $aggregate->export() + ); + } + + public function testGroupLimit2() + { + $aggregate = $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + $aggregate->setLimit(1, 1); + + $this->assertSame( + [ + ['client' => 'Zoe', 'client_id' => '2', 'amount' => 4.0], + ], + $aggregate->export() + ); + } +} diff --git a/tests/ModelUnionExprTest.php b/tests/ModelUnionExprTest.php new file mode 100644 index 000000000..00de31229 --- /dev/null +++ b/tests/ModelUnionExprTest.php @@ -0,0 +1,276 @@ + [ + ['name' => 'Vinny'], + ['name' => 'Zoe'], + ], + 'invoice' => [ + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], + ], + 'payment' => [ + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], + ], + ]; + + protected function setUp(): void + { + parent::setUp(); + $this->setDB($this->init_db); + + $this->transaction = new Model\Transaction2($this->db); + $this->client = new Model\Client($this->db, 'client'); + + $this->client->hasMany('Payment', [Model\Payment::class]); + $this->client->hasMany('Invoice', [Model\Invoice::class]); + } + + public function testFieldExpr() + { + $transaction = $this->transaction; + + $e = $this->getEscapeChar(); + $this->assertSame(str_replace('"', $e, '"amount"'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount')])->render()); + $this->assertSame(str_replace('"', $e, '-"amount"'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount', '-[]')])->render()); + $this->assertSame(str_replace('"', $e, '-NULL'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'blah', '-[]')])->render()); + } + + public function testNestedQuery1() + { + $transaction = $this->transaction; + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), + $transaction->getSubQuery(['name'])->render() + ); + + $this->assertSame( + str_replace('"', $e, '(select "name" "name",-"amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"'), + $transaction->getSubQuery(['name', 'amount'])->render() + ); + + $this->assertSame( + str_replace('"', $e, '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), + $transaction->getSubQuery(['name'])->render() + ); + } + + /** + * If field is not set for one of the nested model, instead of generating exception, NULL will be filled in. + */ + public function testMissingField() + { + $transaction = $this->transaction; + $transaction->nestedInvoice->addExpression('type', '\'invoice\''); + $transaction->addField('type'); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('`', $e, '(select (\'invoice\') `type`,-`amount` `amount` from `invoice` UNION ALL select NULL `type`,`amount` `amount` from `payment`) `derivedTable`'), + $transaction->getSubQuery(['type', 'amount'])->render() + ); + } + + public function testActions() + { + $transaction = $this->transaction; + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, 'select "name","amount" from (select "name" "name",-"amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"'), + $transaction->action('select')->render() + ); + + $this->assertSame( + str_replace('"', $e, 'select "name" from (select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), + $transaction->action('field', ['name'])->render() + ); + + $this->assertSame( + str_replace('"', $e, 'select sum("cnt") from (select count(*) "cnt" from "invoice" UNION ALL select count(*) "cnt" from "payment") "derivedTable"'), + $transaction->action('count')->render() + ); + + $this->assertSame( + str_replace('"', $e, 'select sum("val") from (select sum(-"amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"'), + $transaction->action('fx', ['sum', 'amount'])->render() + ); + } + + public function testActions2() + { + $transaction = $this->transaction; + $this->assertSame(5, (int) $transaction->action('count')->getOne()); + $this->assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); + } + + public function testSubAction1() + { + $transaction = $this->transaction; + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, '(select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment") "derivedTable"'), + $transaction->getSubAction('fx', ['sum', 'amount'])->render() + ); + } + + public function testBasics() + { + $this->setDB($this->init_db); + + $client = $this->client; + + // There are total of 2 clients + $this->assertSame(2, (int) $client->action('count')->getOne()); + + // Client with ID=1 has invoices for 19 + $client->load(1); + $this->assertSame(19.0, (float) $client->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + + $transaction = new Model\Transaction2($this->db); + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'table purchase', 'amount' => -15.0], + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'prepay', 'amount' => 10.0], + ['name' => 'full pay', 'amount' => 4.0], + ], $transaction->export()); + + // Transaction is Union Model + $client->hasMany('Transaction', new Model\Transaction2()); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'table purchase', 'amount' => -15.0], + ['name' => 'prepay', 'amount' => 10.0], + ], $client->ref('Transaction')->export()); + } + + public function testGrouping1() + { + $transaction = $this->transaction; + + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, '(select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"'), + $transaction->getSubQuery(['name', 'amount'])->render() + ); + } + + public function testGrouping2() + { + $transaction = $this->transaction; + + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, 'select "name",sum("amount") "amount" from (select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"'), + $transaction->action('select', [['name', 'amount']])->render() + ); + } + + /** + * If all nested models have a physical field to which a grouped column can be mapped into, then we should group all our + * sub-queries. + */ + public function testGrouping3() + { + $transaction = $this->transaction; + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + $transaction->setOrder('name'); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -8.0], + ['name' => 'full pay', 'amount' => 4.0], + ['name' => 'prepay', 'amount' => 10.0], + ['name' => 'table purchase', 'amount' => -15.0], + ], $transaction->export()); + } + + /** + * If a nested model has a field defined through expression, it should be still used in grouping. We should test this + * with both expressions based off the fields and static expressions (such as "blah"). + */ + public function testSubGroupingByExpressions() + { + $transaction = $this->transaction; + $transaction->nestedInvoice->addExpression('type', '\'invoice\''); + $transaction->nestedPayment->addExpression('type', '\'payment\''); + $transaction->addField('type'); + + $transaction->groupBy('type', ['amount' => ['sum([])', 'type' => 'money']]); + + $this->assertSame([ + ['type' => 'invoice', 'amount' => -23.0], + ['type' => 'payment', 'amount' => 14.0], + ], $transaction->export(['type', 'amount'])); + } + + public function testReference() + { + $client = $this->client; + $client->hasMany('tr', new Model\Transaction2()); + + $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); + $this->assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a ' . + 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"'), + $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render() + ); + } + + /** + * Aggregation is supposed to work in theory, but MySQL uses "semi-joins" for this type of query which does not support UNION, + * and therefore it complains about `client`.`id` field. + * + * See also: http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 + */ + public function testFieldAggregate() + { + $this->client->hasMany('tr', new Model\Transaction2()) + ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); + + $this->assertTrue(true); // fake assert + //select "client"."id","client"."name",(select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1 + //$c->load(1); + } + + /** + * Model's conditions can still be placed on the original field values. + */ + public function testConditionOnMappedField() + { + $transaction = new Model\Transaction2($this->db); + $transaction->nestedInvoice->addCondition('amount', 4); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'prepay', 'amount' => 10.0], + ['name' => 'full pay', 'amount' => 4.0], + ], $transaction->export()); + } +} diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php new file mode 100644 index 000000000..96deef3fd --- /dev/null +++ b/tests/ModelUnionTest.php @@ -0,0 +1,240 @@ + [ + ['name' => 'Vinny'], + ['name' => 'Zoe'], + ], + 'invoice' => [ + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], + ], + 'payment' => [ + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], + ], + ]; + + protected function setUp(): void + { + parent::setUp(); + $this->setDB($this->init_db); + + $this->transaction = new Model\Transaction($this->db); + + $this->client = new Model\Client($this->db); + + $this->client->hasMany('Payment', [Model\Payment::class]); + $this->client->hasMany('Invoice', [Model\Invoice::class]); + } + + public function testNestedQuery1() + { + $transaction = $this->transaction; + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), + $transaction->getSubQuery(['name'])->render() + ); + + $this->assertSame( + str_replace('"', $e, '(select "name" "name","amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"'), + $transaction->getSubQuery(['name', 'amount'])->render() + ); + + $this->assertSame( + str_replace('"', $e, '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), + $transaction->getSubQuery(['name'])->render() + ); + } + + /** + * If field is not set for one of the nested model, instead of generating exception, NULL will be filled in. + */ + public function testMissingField() + { + $transaction = $this->transaction; + $transaction->nestedInvoice->addExpression('type', '\'invoice\''); + $transaction->addField('type'); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, '(select (\'invoice\') "type","amount" "amount" from "invoice" UNION ALL select NULL "type","amount" "amount" from "payment") "derivedTable"'), + $transaction->getSubQuery(['type', 'amount'])->render() + ); + } + + public function testActions() + { + $transaction = $this->transaction; + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, 'select "name","amount" from (select "name" "name","amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"'), + $transaction->action('select')->render() + ); + + $this->assertSame( + str_replace('"', $e, 'select "name" from (select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), + $transaction->action('field', ['name'])->render() + ); + + $this->assertSame( + str_replace('"', $e, 'select sum("cnt") from (select count(*) "cnt" from "invoice" UNION ALL select count(*) "cnt" from "payment") "derivedTable"'), + $transaction->action('count')->render() + ); + + $this->assertSame( + str_replace('"', $e, 'select sum("val") from (select sum("amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"'), + $transaction->action('fx', ['sum', 'amount'])->render() + ); + } + + public function testActions2() + { + $transaction = $this->transaction; + $this->assertSame(5, (int) $transaction->action('count')->getOne()); + $this->assertSame(37.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); + } + + public function testBasics() + { + $client = $this->client; + + // There are total of 2 clients + $this->assertSame(2, (int) $client->action('count')->getOne()); + + // Client with ID=1 has invoices for 19 + $client->load(1); + $this->assertSame(19.0, (float) $client->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + + $transaction = new Model\Transaction($this->db); + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => 4.0], + ['name' => 'table purchase', 'amount' => 15.0], + ['name' => 'chair purchase', 'amount' => 4.0], + ['name' => 'prepay', 'amount' => 10.0], + ['name' => 'full pay', 'amount' => 4.0], + ], $transaction->export()); + + // Transaction is Union Model + $client->hasMany('Transaction', new Model\Transaction()); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => 4.0], + ['name' => 'table purchase', 'amount' => 15.0], + ['name' => 'prepay', 'amount' => 10.0], + ], $client->ref('Transaction')->export()); + } + + public function testGrouping1() + { + $transaction = $this->transaction; + + $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, '(select "name" "name",sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"'), + $transaction->getSubQuery(['name', 'amount'])->render() + ); + } + + public function testGrouping2() + { + $transaction = $this->transaction; + + $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, 'select "name",sum("amount") "amount" from (select "name" "name",sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"'), + $transaction->action('select', [['name', 'amount']])->render() + ); + } + + /** + * If all nested models have a physical field to which a grouped column can be mapped into, then we should group all our + * sub-queries. + */ + public function testGrouping3() + { + $transaction = $this->transaction; + $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); + $transaction->setOrder('name'); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => 8.0], + ['name' => 'full pay', 'amount' => 4.0], + ['name' => 'prepay', 'amount' => 10.0], + ['name' => 'table purchase', 'amount' => 15.0], + ], $transaction->export()); + } + + /** + * If a nested model has a field defined through expression, it should be still used in grouping. We should test this + * with both expressions based off the fields and static expressions (such as "blah"). + */ + public function testSubGroupingByExpressions() + { + $transaction = $this->transaction; + $transaction->nestedInvoice->addExpression('type', '\'invoice\''); + $transaction->nestedPayment->addExpression('type', '\'payment\''); + $transaction->addField('type'); + + $transaction->groupBy('type', ['amount' => ['sum([amount])', 'type' => 'money']]); + + $this->assertSame([ + ['type' => 'invoice', 'amount' => 23.0], + ['type' => 'payment', 'amount' => 14.0], + ], $transaction->export(['type', 'amount'])); + } + + public function testReference() + { + $client = $this->client; + $client->hasMany('tr', new Model\Transaction()); + + $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); + $this->assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, 'select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = :a ' . + 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"'), + $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render() + ); + } + + /** + * Aggregation is supposed to work in theory, but MySQL uses "semi-joins" for this type of query which does not support UNION, + * and therefore it complains about "client"."id" field. + * + * See also: http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 + */ + public function testFieldAggregate() + { + $client = $this->client; + $client->hasMany('tr', new Model\Transaction2()) + ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); + + $this->assertTrue(true); // fake assert + //select "client"."id","client"."name",(select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1 + //$c->load(1); + } +} diff --git a/tests/ReportTest.php b/tests/ReportTest.php new file mode 100644 index 000000000..395cd93c9 --- /dev/null +++ b/tests/ReportTest.php @@ -0,0 +1,57 @@ + [ + ['name' => 'Vinny'], + ['name' => 'Zoe'], + ], + 'invoice' => [ + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], + ], + 'payment' => [ + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], + ], + ]; + + /** @var Aggregate */ + protected $g; + + protected function setUp(): void + { + parent::setUp(); + $this->setDB($this->init_db); + + $m1 = new Model\Invoice($this->db); + $m1->getRef('client_id')->addTitle(); + $this->g = new Aggregate($m1); + $this->g->addField('client'); + } + + public function testAliasGroupSelect() + { + $g = $this->g; + + $g->groupBy(['client_id'], ['c' => ['count(*)', 'type' => 'integer']]); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => '1', 'c' => 2], + ['client' => 'Zoe', 'client_id' => '2', 'c' => 1], + ], + $g->export() + ); + } +} From 9f955936f94078170bea0eecc014ed035c2d8081 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 00:43:20 +0200 Subject: [PATCH 002/151] [update] combine union test in single class --- tests/Model/Client.php | 2 +- tests/Model/Payment.php | 1 - tests/Model/Transaction.php | 6 +- tests/Model/Transaction2.php | 30 ---- tests/ModelAggregateTest.php | 6 +- tests/ModelUnionExprTest.php | 276 ----------------------------------- tests/ModelUnionTest.php | 176 ++++++++++++++++++++-- 7 files changed, 170 insertions(+), 327 deletions(-) delete mode 100644 tests/Model/Transaction2.php delete mode 100644 tests/ModelUnionExprTest.php diff --git a/tests/Model/Client.php b/tests/Model/Client.php index c7917706e..f04200b51 100644 --- a/tests/Model/Client.php +++ b/tests/Model/Client.php @@ -7,7 +7,7 @@ class Client extends User { public $table = 'client'; - + public function init(): void { parent::init(); diff --git a/tests/Model/Payment.php b/tests/Model/Payment.php index 1333cf23c..6bf8e0ef9 100644 --- a/tests/Model/Payment.php +++ b/tests/Model/Payment.php @@ -5,7 +5,6 @@ namespace atk4\data\tests\Model; use atk4\data\Model; -use atk4\data\tests\Model\Client; class Payment extends Model { diff --git a/tests/Model/Transaction.php b/tests/Model/Transaction.php index 7c4f17bf6..ab364c127 100644 --- a/tests/Model/Transaction.php +++ b/tests/Model/Transaction.php @@ -10,13 +10,15 @@ class Transaction extends Union { public $nestedInvoice; public $nestedPayment; - + + public $subtractInvoice; + public function init(): void { parent::init(); // first lets define nested models - $this->nestedInvoice = $this->addNestedModel(new Invoice()); + $this->nestedInvoice = $this->addNestedModel(new Invoice(), $this->subtractInvoice ? ['amount' => '-[]'] : []); $this->nestedPayment = $this->addNestedModel(new Payment()); // next, define common fields diff --git a/tests/Model/Transaction2.php b/tests/Model/Transaction2.php deleted file mode 100644 index e31cf8525..000000000 --- a/tests/Model/Transaction2.php +++ /dev/null @@ -1,30 +0,0 @@ -nestedInvoice = $this->addNestedModel(new Invoice(), ['amount' => '-[]']); - $this->nestedPayment = $this->addNestedModel(new Payment()); - - //$this->nestedInvoice->hasOne('client_id', [new Client()]); - //$this->nestedPayment->hasOne('client_id', [new Client()]); - - // next, define common fields - $this->addField('name'); - $this->addField('amount', ['type' => 'money']); - //$this->hasOne('client_id', [new Client()]); - } -} diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index e5fb97d92..2ede199ac 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -34,10 +34,10 @@ protected function setUp(): void parent::setUp(); $this->setDB($this->init_db); - $m_invoice = new Model\Invoice($this->db); - $m_invoice->getRef('client_id')->addTitle(); + $invoice = new Model\Invoice($this->db); + $invoice->getRef('client_id')->addTitle(); - $this->aggregate = new Aggregate($m_invoice); + $this->aggregate = new Aggregate($invoice); $this->aggregate->addField('client'); } diff --git a/tests/ModelUnionExprTest.php b/tests/ModelUnionExprTest.php deleted file mode 100644 index 00de31229..000000000 --- a/tests/ModelUnionExprTest.php +++ /dev/null @@ -1,276 +0,0 @@ - [ - ['name' => 'Vinny'], - ['name' => 'Zoe'], - ], - 'invoice' => [ - ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], - ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], - ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], - ], - 'payment' => [ - ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], - ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], - ], - ]; - - protected function setUp(): void - { - parent::setUp(); - $this->setDB($this->init_db); - - $this->transaction = new Model\Transaction2($this->db); - $this->client = new Model\Client($this->db, 'client'); - - $this->client->hasMany('Payment', [Model\Payment::class]); - $this->client->hasMany('Invoice', [Model\Invoice::class]); - } - - public function testFieldExpr() - { - $transaction = $this->transaction; - - $e = $this->getEscapeChar(); - $this->assertSame(str_replace('"', $e, '"amount"'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount')])->render()); - $this->assertSame(str_replace('"', $e, '-"amount"'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount', '-[]')])->render()); - $this->assertSame(str_replace('"', $e, '-NULL'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'blah', '-[]')])->render()); - } - - public function testNestedQuery1() - { - $transaction = $this->transaction; - - $e = $this->getEscapeChar(); - $this->assertSame( - str_replace('"', $e, '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), - $transaction->getSubQuery(['name'])->render() - ); - - $this->assertSame( - str_replace('"', $e, '(select "name" "name",-"amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"'), - $transaction->getSubQuery(['name', 'amount'])->render() - ); - - $this->assertSame( - str_replace('"', $e, '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), - $transaction->getSubQuery(['name'])->render() - ); - } - - /** - * If field is not set for one of the nested model, instead of generating exception, NULL will be filled in. - */ - public function testMissingField() - { - $transaction = $this->transaction; - $transaction->nestedInvoice->addExpression('type', '\'invoice\''); - $transaction->addField('type'); - - $e = $this->getEscapeChar(); - $this->assertSame( - str_replace('`', $e, '(select (\'invoice\') `type`,-`amount` `amount` from `invoice` UNION ALL select NULL `type`,`amount` `amount` from `payment`) `derivedTable`'), - $transaction->getSubQuery(['type', 'amount'])->render() - ); - } - - public function testActions() - { - $transaction = $this->transaction; - - $e = $this->getEscapeChar(); - $this->assertSame( - str_replace('"', $e, 'select "name","amount" from (select "name" "name",-"amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"'), - $transaction->action('select')->render() - ); - - $this->assertSame( - str_replace('"', $e, 'select "name" from (select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"'), - $transaction->action('field', ['name'])->render() - ); - - $this->assertSame( - str_replace('"', $e, 'select sum("cnt") from (select count(*) "cnt" from "invoice" UNION ALL select count(*) "cnt" from "payment") "derivedTable"'), - $transaction->action('count')->render() - ); - - $this->assertSame( - str_replace('"', $e, 'select sum("val") from (select sum(-"amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"'), - $transaction->action('fx', ['sum', 'amount'])->render() - ); - } - - public function testActions2() - { - $transaction = $this->transaction; - $this->assertSame(5, (int) $transaction->action('count')->getOne()); - $this->assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); - } - - public function testSubAction1() - { - $transaction = $this->transaction; - $e = $this->getEscapeChar(); - $this->assertSame( - str_replace('"', $e, '(select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment") "derivedTable"'), - $transaction->getSubAction('fx', ['sum', 'amount'])->render() - ); - } - - public function testBasics() - { - $this->setDB($this->init_db); - - $client = $this->client; - - // There are total of 2 clients - $this->assertSame(2, (int) $client->action('count')->getOne()); - - // Client with ID=1 has invoices for 19 - $client->load(1); - $this->assertSame(19.0, (float) $client->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); - - $transaction = new Model\Transaction2($this->db); - $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'table purchase', 'amount' => -15.0], - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'prepay', 'amount' => 10.0], - ['name' => 'full pay', 'amount' => 4.0], - ], $transaction->export()); - - // Transaction is Union Model - $client->hasMany('Transaction', new Model\Transaction2()); - - $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'table purchase', 'amount' => -15.0], - ['name' => 'prepay', 'amount' => 10.0], - ], $client->ref('Transaction')->export()); - } - - public function testGrouping1() - { - $transaction = $this->transaction; - - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); - - $e = $this->getEscapeChar(); - $this->assertSame( - str_replace('"', $e, '(select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"'), - $transaction->getSubQuery(['name', 'amount'])->render() - ); - } - - public function testGrouping2() - { - $transaction = $this->transaction; - - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); - - $e = $this->getEscapeChar(); - $this->assertSame( - str_replace('"', $e, 'select "name",sum("amount") "amount" from (select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"'), - $transaction->action('select', [['name', 'amount']])->render() - ); - } - - /** - * If all nested models have a physical field to which a grouped column can be mapped into, then we should group all our - * sub-queries. - */ - public function testGrouping3() - { - $transaction = $this->transaction; - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); - $transaction->setOrder('name'); - - $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -8.0], - ['name' => 'full pay', 'amount' => 4.0], - ['name' => 'prepay', 'amount' => 10.0], - ['name' => 'table purchase', 'amount' => -15.0], - ], $transaction->export()); - } - - /** - * If a nested model has a field defined through expression, it should be still used in grouping. We should test this - * with both expressions based off the fields and static expressions (such as "blah"). - */ - public function testSubGroupingByExpressions() - { - $transaction = $this->transaction; - $transaction->nestedInvoice->addExpression('type', '\'invoice\''); - $transaction->nestedPayment->addExpression('type', '\'payment\''); - $transaction->addField('type'); - - $transaction->groupBy('type', ['amount' => ['sum([])', 'type' => 'money']]); - - $this->assertSame([ - ['type' => 'invoice', 'amount' => -23.0], - ['type' => 'payment', 'amount' => 14.0], - ], $transaction->export(['type', 'amount'])); - } - - public function testReference() - { - $client = $this->client; - $client->hasMany('tr', new Model\Transaction2()); - - $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); - $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); - $this->assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); - - $e = $this->getEscapeChar(); - $this->assertSame( - str_replace('"', $e, 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a ' . - 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"'), - $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render() - ); - } - - /** - * Aggregation is supposed to work in theory, but MySQL uses "semi-joins" for this type of query which does not support UNION, - * and therefore it complains about `client`.`id` field. - * - * See also: http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 - */ - public function testFieldAggregate() - { - $this->client->hasMany('tr', new Model\Transaction2()) - ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); - - $this->assertTrue(true); // fake assert - //select "client"."id","client"."name",(select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1 - //$c->load(1); - } - - /** - * Model's conditions can still be placed on the original field values. - */ - public function testConditionOnMappedField() - { - $transaction = new Model\Transaction2($this->db); - $transaction->nestedInvoice->addCondition('amount', 4); - - $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'prepay', 'amount' => 10.0], - ['name' => 'full pay', 'amount' => 4.0], - ], $transaction->export()); - } -} diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 96deef3fd..b4951a221 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -6,10 +6,13 @@ class ModelUnionTest extends \atk4\schema\PhpunitTestCase { + /** @var Model\Client */ + protected $client; /** @var Model\Transaction */ protected $transaction; - protected $client; - + /** @var Model\Transaction */ + protected $subtractInvoiceTransaction; + /** @var array */ private $init_db = [ @@ -33,12 +36,39 @@ protected function setUp(): void parent::setUp(); $this->setDB($this->init_db); - $this->transaction = new Model\Transaction($this->db); - - $this->client = new Model\Client($this->db); - - $this->client->hasMany('Payment', [Model\Payment::class]); - $this->client->hasMany('Invoice', [Model\Invoice::class]); + $this->client = $this->createClient($this->db); + $this->transaction = $this->createTransaction($this->db); + $this->subtractInvoiceTransaction = $this->createSubtractInvoiceTransaction($this->db); + } + + protected function createTransaction($persistence = null) + { + return new Model\Transaction($persistence); + } + + protected function createSubtractInvoiceTransaction($persistence = null) + { + return new Model\Transaction($persistence, ['subtractInvoice' => true]); + } + + protected function createClient($persistence = null) + { + $client = new Model\Client($this->db); + + $client->hasMany('Payment', [Model\Payment::class]); + $client->hasMany('Invoice', [Model\Invoice::class]); + + return $client; + } + + public function testFieldExpr() + { + $transaction = $this->subtractInvoiceTransaction; + + $e = $this->getEscapeChar(); + $this->assertSame(str_replace('"', $e, '"amount"'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount')])->render()); + $this->assertSame(str_replace('"', $e, '-"amount"'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount', '-[]')])->render()); + $this->assertSame(str_replace('"', $e, '-NULL'), $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'blah', '-[]')])->render()); } public function testNestedQuery1() @@ -102,6 +132,13 @@ public function testActions() str_replace('"', $e, 'select sum("val") from (select sum("amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"'), $transaction->action('fx', ['sum', 'amount'])->render() ); + + $transaction = $this->subtractInvoiceTransaction; + + $this->assertSame( + str_replace('"', $e, 'select sum("val") from (select sum(-"amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"'), + $transaction->action('fx', ['sum', 'amount'])->render() + ); } public function testActions2() @@ -109,20 +146,35 @@ public function testActions2() $transaction = $this->transaction; $this->assertSame(5, (int) $transaction->action('count')->getOne()); $this->assertSame(37.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); + + $transaction = $this->subtractInvoiceTransaction; + $this->assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); + } + + public function testSubAction1() + { + $transaction = $this->subtractInvoiceTransaction; + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, '(select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment") "derivedTable"'), + $transaction->getSubAction('fx', ['sum', 'amount'])->render() + ); } public function testBasics() { - $client = $this->client; + $client = clone $this->client; // There are total of 2 clients $this->assertSame(2, (int) $client->action('count')->getOne()); // Client with ID=1 has invoices for 19 $client->load(1); + $this->assertSame(19.0, (float) $client->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); - $transaction = new Model\Transaction($this->db); + $transaction = $this->transaction; + $this->assertSame([ ['name' => 'chair purchase', 'amount' => 4.0], ['name' => 'table purchase', 'amount' => 15.0], @@ -132,13 +184,36 @@ public function testBasics() ], $transaction->export()); // Transaction is Union Model - $client->hasMany('Transaction', new Model\Transaction()); + $client->hasMany('Transaction', $transaction); $this->assertSame([ ['name' => 'chair purchase', 'amount' => 4.0], ['name' => 'table purchase', 'amount' => 15.0], ['name' => 'prepay', 'amount' => 10.0], ], $client->ref('Transaction')->export()); + + $client = clone $this->client; + + $client->load(1); + + $transaction = $this->subtractInvoiceTransaction; + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'table purchase', 'amount' => -15.0], + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'prepay', 'amount' => 10.0], + ['name' => 'full pay', 'amount' => 4.0], + ], $transaction->export()); + + // Transaction is Union Model + $client->hasMany('Transaction', $transaction); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'table purchase', 'amount' => -15.0], + ['name' => 'prepay', 'amount' => 10.0], + ], $client->ref('Transaction')->export()); } public function testGrouping1() @@ -152,6 +227,16 @@ public function testGrouping1() str_replace('"', $e, '(select "name" "name",sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"'), $transaction->getSubQuery(['name', 'amount'])->render() ); + + $transaction = $this->subtractInvoiceTransaction; + + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, '(select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"'), + $transaction->getSubQuery(['name', 'amount'])->render() + ); } public function testGrouping2() @@ -165,6 +250,16 @@ public function testGrouping2() str_replace('"', $e, 'select "name",sum("amount") "amount" from (select "name" "name",sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"'), $transaction->action('select', [['name', 'amount']])->render() ); + + $transaction = $this->subtractInvoiceTransaction; + + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, 'select "name",sum("amount") "amount" from (select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"'), + $transaction->action('select', [['name', 'amount']])->render() + ); } /** @@ -183,6 +278,17 @@ public function testGrouping3() ['name' => 'prepay', 'amount' => 10.0], ['name' => 'table purchase', 'amount' => 15.0], ], $transaction->export()); + + $transaction = $this->subtractInvoiceTransaction; + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + $transaction->setOrder('name'); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -8.0], + ['name' => 'full pay', 'amount' => 4.0], + ['name' => 'prepay', 'amount' => 10.0], + ['name' => 'table purchase', 'amount' => -15.0], + ], $transaction->export()); } /** @@ -202,12 +308,24 @@ public function testSubGroupingByExpressions() ['type' => 'invoice', 'amount' => 23.0], ['type' => 'payment', 'amount' => 14.0], ], $transaction->export(['type', 'amount'])); + + $transaction = $this->subtractInvoiceTransaction; + $transaction->nestedInvoice->addExpression('type', '\'invoice\''); + $transaction->nestedPayment->addExpression('type', '\'payment\''); + $transaction->addField('type'); + + $transaction->groupBy('type', ['amount' => ['sum([])', 'type' => 'money']]); + + $this->assertSame([ + ['type' => 'invoice', 'amount' => -23.0], + ['type' => 'payment', 'amount' => 14.0], + ], $transaction->export(['type', 'amount'])); } public function testReference() { - $client = $this->client; - $client->hasMany('tr', new Model\Transaction()); + $client = clone $this->client; + $client->hasMany('tr', $this->createTransaction()); $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); @@ -219,6 +337,20 @@ public function testReference() 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"'), $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render() ); + + $client = clone $this->client; + $client->hasMany('tr', $this->createSubtractInvoiceTransaction()); + + $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); + $this->assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); + + $e = $this->getEscapeChar(); + $this->assertSame( + str_replace('"', $e, 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a ' . + 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"'), + $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render() + ); } /** @@ -230,11 +362,27 @@ public function testReference() public function testFieldAggregate() { $client = $this->client; - $client->hasMany('tr', new Model\Transaction2()) + $client->hasMany('tr', $this->createTransaction()) ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); $this->assertTrue(true); // fake assert //select "client"."id","client"."name",(select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1 //$c->load(1); } + + /** + * Model's conditions can still be placed on the original field values. + */ + public function testConditionOnMappedField() + { + $transaction = $this->subtractInvoiceTransaction; + $transaction->nestedInvoice->addCondition('amount', 4); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'prepay', 'amount' => 10.0], + ['name' => 'full pay', 'amount' => 4.0], + ], $transaction->export()); + } } From 691d9bda1d85d66f878e18a5c27fc3d17674fa83 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 09:12:44 +0200 Subject: [PATCH 003/151] [fix] create db tables with all columns --- tests/ModelUnionTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index b4951a221..65291a20c 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -17,7 +17,8 @@ class ModelUnionTest extends \atk4\schema\PhpunitTestCase private $init_db = [ 'client' => [ - ['name' => 'Vinny'], + // allow of migrator to create all columns + ['name' => 'Vinny', 'surname' => null, 'order' => null], ['name' => 'Zoe'], ], 'invoice' => [ From ee88bb57b6b07d408cc192fa36b126615ba4b30a Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 10:50:16 +0200 Subject: [PATCH 004/151] [update] migrate and update docs --- docs/aggregates.rst | 51 +++++++++++++++++++ docs/index.rst | 4 +- docs/joins.rst | 2 + docs/unions.rst | 120 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 docs/aggregates.rst create mode 100644 docs/unions.rst diff --git a/docs/aggregates.rst b/docs/aggregates.rst new file mode 100644 index 000000000..09a0fc4e4 --- /dev/null +++ b/docs/aggregates.rst @@ -0,0 +1,51 @@ + +.. _Aggregates: + +================ +Model Aggregates +================ + +.. php:namespace:: atk4\data\Model + +.. php:class:: Aggregate + +In order to create model aggregates the Aggregate model needs to be used: + +Grouping +-------- + +Aggregate model can be used for grouping:: + + $orders->add(new \atk4\data\Model\Aggregate()); + + $aggregate = $orders->action('group'); + +`$aggregate` above will return a new object that is most appropriate for the model persistence and which can be manipulated +in various ways to fine-tune aggregation. Below is one sample use:: + + $aggregate = $orders->action( + 'group', + 'country_id', + [ + 'country', + 'count'=>'count', + 'total_amount'=>['sum', 'amount'] + ], + ); + + foreach($aggregate as $row) { + var_dump(json_encode($row)); + // ['country'=>'UK', 'count'=>20, 'total_amount'=>123.20]; + // .. + } + +Below is how opening balance can be build:: + + $ledger = new GeneralLedger($db); + $ledger->addCondition('date', '<', $from); + + // we actually need grouping by nominal + $ledger->add(new \atk4\data\Model\Aggregate()); + $byNominal = $ledger->action('group', 'nominal_id'); + $byNominal->addField('opening_balance', ['sum', 'amount']); + $byNominal->join() \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index e17fcc5f5..3c24da636 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,14 +21,14 @@ Contents: references expressions joins + unions + aggregates hooks deriving advanced extensions persistence/csv - - Indices and tables ================== diff --git a/docs/joins.rst b/docs/joins.rst index 5e6f8a9ee..4fed0793e 100644 --- a/docs/joins.rst +++ b/docs/joins.rst @@ -1,4 +1,6 @@ +.. _Joins: + ================================ Model from multiple joined table ================================ diff --git a/docs/unions.rst b/docs/unions.rst new file mode 100644 index 000000000..eb4d69d87 --- /dev/null +++ b/docs/unions.rst @@ -0,0 +1,120 @@ + +.. _Unions: + +============ +Model Unions +============ + +.. php:namespace:: atk4\data\Model + +.. php:class:: Union + +In some cases data from multiple models need to be combined. In this case the Union model comes very handy. +In the case used below Client model schema may have multiple invoices and multiple payments. Payment is not related to the invoice.:: + + class Client extends \atk4\data\Model { + public $table = 'client'; + + function init() { + parent::init(); + $this->addField('name'); + + $this->hasMany('Payment'); + $this->hasMany('Invoice'); + } + } + +(see tests/ModelUnionTest.php, tests/Client.php, tests/Payment.php and tests/Invoice.php files). + +Union Model Definition +---------------------- + +Normally a model is associated with a single table. Union model can have multiple nested models defined and it fetches +results from that. As a result, Union model will have no "id" field. Below is an example of inline definition of Union model. +The Union model can be separated in a designated class and nested model added within the init() method body of the new class:: + + $unionPaymentInvoice = new \atk4\data\Model\Union(); + + $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); + $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment()); + +Next, assuming that both models have common fields "name" and "amount", `$unionPaymentInvoice` fields can be set:: + + $unionPaymentInvoice->addField('name'); + $unionPaymentInvoice->addFiled('amount', ['type'=>'money']); + +Then data can be queried:: + + $unionPaymentInvoice->export(); + +Union Model Fields +------------------ + +Below is an example of 3 different ways to define fields for the Union model:: + + // Will link the "name" field will all the nested models. + $unionPaymentInvoice->addField('client_id'); + + // Expression will not affect nested models in any way + $unionPaymentInvoice->addExpression('name_capital','upper([name])'); + + // Union model can be joined with extra tables and define some fields from those joins + $unionPaymentInvoice + ->join('client','client_id') + ->addField('client_name', 'name'); + +:ref:`Expressions` and :ref:`Joins` are working just as they would on any other model. + +Field Mapping +------------- + +Sometimes the field that is defined in the Union model may be named differently inside nested models. +E.g. Invoice has field "description" and payment has field "note". +When defining a nested model a field map array needs to be specified:: + + $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); + $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description'=>'[note]']); + $unionPaymentInvoice->addField('description'); + +The key of the field map array must match the Union field. The value is an expression. (See :ref:`Model`). +This format can also be used to reverse sign on amounts. When we are creating "Transactions", then invoices would be subtracted from the amount, +while payments will be added:: + + $nestedPayment = $m_uni->addNestedModel(new Invoice(), ['amount'=>'-[amount]']); + $nestedInvoice = $m_uni->addNestedModel(new Payment(), ['description'=>'[note]']); + $unionPaymentInvoice->addField('description'); + +Should more flexibility be needed, more expressions (or fields) can be added directly to nested models:: + + $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice(), ['amount'=>'-[amount]']); + $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description'=>'[note]']); + + $nestedPayment->addExpression('type', '"payment"'); + $nestedInvoice->addExpression('type', '"invoice"'); + $unionPaymentInvoice->addField('type'); + +A new field "type" has been added that will be defined as a static constant. + +Referencing an Union Model +-------------------------- + +Like any other model, Union model can be assigned through a reference. In the case here one Client can have multiple transactions. +Initially a related union can be defined:: + + $client->hasMany('Transaction', new Transaction()); + +When condition is added on an Union model it will send it down to every nested model. This way the resulting SQL query remains optimized. + +The exception is when field is not mapped to nested model (if it's an Expression or associated with a Join). + +In most cases optimization on the query and Union model is not necessary as it will be done automatically. + +Grouping Results +---------------- + +Union model has also a built-in grouping support:: + + $unionPaymentInvoice->groupBy('client_id', ['amount'=>'sum']); + +When specifying a grouping field and it is associated with nested models then grouping will be enabled on every nested model. + From e43659957c226a213aab1d1e3f0b5dc7c1d6ab65 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 14:41:06 +0200 Subject: [PATCH 005/151] [update] php docs --- src/Model/Aggregate.php | 10 +++++++--- src/Model/Union.php | 18 +++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index d3bb68a93..062711980 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -52,12 +52,16 @@ class Aggregate extends Model */ public $read_only = true; + /** + * Aggregate does not have ID field. + * + * @var string + */ + public $id_field; + /** @var Model */ public $master_model; - /** @var string */ - public $id_field; - /** @var array */ public $group = []; diff --git a/src/Model/Union.php b/src/Model/Union.php index e10781d10..e6ab3bce2 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -35,15 +35,6 @@ class Union extends Model */ public $read_only = true; - /** - * Contain array of array containing model and mappings. - * - * $union = [ [ $m1, ['amount'=>'total_gross'] ] , [$m2, []] ]; - * - * @var array - */ - public $union = []; - /** * Union normally does not have ID field. Setting this to null will * disable various per-id operations, such as load(). @@ -55,6 +46,15 @@ class Union extends Model */ public $id_field; + /** + * Contain array of array containing model and mappings. + * + * $union = [ [ $model1, ['amount'=>'total_gross'] ] , [$model2, []] ]; + * + * @var array + */ + public $union = []; + /** * When aggregation happens, this field will contain list of fields * we use in groupBy. Multiple fields can be in the array. All From c728ce1cb391905f0412e695c202e75f43794690 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 14:41:35 +0200 Subject: [PATCH 006/151] [update] variable names --- src/Model/Union.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index e6ab3bce2..3133eb578 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -112,7 +112,7 @@ public function getSubQuery(array $fields): Expression foreach ($this->union as $n => [$nestedModel, $fieldMap]) { // map fields for related model - $queryFieldMap = []; + $queryFieldExpressions = []; foreach ($fields as $fieldName) { try { // Union can be joined with additional @@ -120,32 +120,34 @@ public function getSubQuery(array $fields): Expression // fields if (!$this->hasField($fieldName)) { - $queryFieldMap[$fieldName] = $nestedModel->expr('NULL'); + $queryFieldExpressions[$fieldName] = $nestedModel->expr('NULL'); continue; } - if ($this->getField($fieldName)->join || $this->getField($fieldName)->never_persist) { + $field = $this->getField($fieldName); + + if ($field->join || $field->never_persist) { continue; } // Union can have some fields defined as expressions. We don't touch those either. // Imants: I have no idea why this condition was set, but it's limiting our ability // to use expression fields in mapping - if ($this->getField($fieldName) instanceof FieldSqlExpression && !isset($this->aggregate[$fieldName])) { + if ($field instanceof FieldSqlExpression && !isset($this->aggregate[$fieldName])) { continue; } - $field = $this->getFieldExpr($nestedModel, $fieldName, $fieldMap[$fieldName] ?? null); + $fieldExpression = $this->getFieldExpr($nestedModel, $fieldName, $fieldMap[$fieldName] ?? null); if (isset($this->aggregate[$fieldName])) { $seed = (array) $this->aggregate[$fieldName]; // first element of seed should be expression itself - $field = $nestedModel->expr($seed[0], [$field]); + $fieldExpression = $nestedModel->expr($seed[0], [$fieldExpression]); } - $queryFieldMap[$fieldName] = $field; + $queryFieldExpressions[$fieldName] = $fieldExpression; } catch (\atk4\core\Exception $e) { throw $e->addMoreInfo('model', $n); } @@ -165,7 +167,7 @@ public function getSubQuery(array $fields): Expression } } - $query->field($queryFieldMap); + $query->field($queryFieldExpressions); // also for sub-queries if ($this->group) { From e3432cf3bef0fb1caa897a260b287c1176cf8e92 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 16:24:35 +0200 Subject: [PATCH 007/151] [update] use generic Model::export method --- src/Model/Union.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index 3133eb578..ce08f1941 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -299,27 +299,6 @@ public function action($mode, $args = []) return parent::action($mode, $args)->reset('table')->table($subquery); } - /** - * Export model. - * - * @param array|null $fields Names of fields to export - * @param string $key_field Optional name of field which value we will use as array key - * @param bool $typecast_data Should we typecast exported data - */ - public function export($fields = null, $key_field = null, $typecast_data = true): array - { - if ($fields) { - $this->onlyFields($fields); - } - - $data = []; - foreach ($this->getIterator() as $row) { - $data[] = $row->get(); - } - - return $data; - } - /** * Adds nested model in union. * From 9dcd45d75a7818e95e200f843a4cbc655763afd3 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 21:51:51 +0200 Subject: [PATCH 008/151] [fix] typos --- docs/unions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/unions.rst b/docs/unions.rst index eb4d69d87..ba37eea23 100644 --- a/docs/unions.rst +++ b/docs/unions.rst @@ -41,7 +41,7 @@ The Union model can be separated in a designated class and nested model added wi Next, assuming that both models have common fields "name" and "amount", `$unionPaymentInvoice` fields can be set:: $unionPaymentInvoice->addField('name'); - $unionPaymentInvoice->addFiled('amount', ['type'=>'money']); + $unionPaymentInvoice->addField('amount', ['type'=>'money']); Then data can be queried:: @@ -52,7 +52,7 @@ Union Model Fields Below is an example of 3 different ways to define fields for the Union model:: - // Will link the "name" field will all the nested models. + // Will link the "name" field with all the nested models. $unionPaymentInvoice->addField('client_id'); // Expression will not affect nested models in any way From e2364cbea219127e521e10d515d7ee5955cd3929 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 23:10:20 +0200 Subject: [PATCH 009/151] [update] improve naming consistency --- src/Model/Aggregate.php | 20 ++++++++++---------- src/Model/Union.php | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 062711980..9a7ab9344 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -38,12 +38,12 @@ class Aggregate extends Model { /** @const string */ - public const HOOK_AFTER_SELECT = self::class . '@afterSelect'; + public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; /** - * @deprecated use HOOK_AFTER_SELECT instead - will be removed dec-2020 + * @deprecated use HOOK_INIT_SELECT_QUERY instead - will be removed dec-2020 */ - public const HOOK_AFTER_GROUP_SELECT = self::HOOK_AFTER_SELECT; + public const HOOK_AFTER_GROUP_SELECT = self::HOOK_INIT_SELECT_QUERY; /** * Aggregate model should always be read-only. @@ -167,7 +167,7 @@ public function addField($name, $seed = []) /** * Given a query, will add safe fields in. */ - public function queryFields(Query $query, array $fields = []): Query + public function initQueryFields(Query $query, array $fields = []): Query { $this->persistence->initQueryFields($this, $query, $fields); @@ -177,7 +177,7 @@ public function queryFields(Query $query, array $fields = []): Query /** * Adds grouping in query. */ - public function addGrouping(Query $query) + public function initQueryGrouping(Query $query) { // use table alias of master model $this->table_alias = $this->master_model->table_alias; @@ -244,21 +244,21 @@ public function action($mode, $args = []) // select but no need your fields $query = $this->master_model->action($mode, [false]); - $query = $this->queryFields($query, array_unique($fields + $this->system_fields)); + $this->initQueryFields($query, array_unique($fields + $this->system_fields)); - $this->addGrouping($query); + $this->initQueryGrouping($query); $this->initQueryConditions($query); - $this->hook(self::HOOK_AFTER_SELECT, [$query]); + $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); return $query; case 'count': $query = $this->master_model->action($mode, $args); $query->reset('field')->field($this->expr('1')); - $this->addGrouping($query); + $this->initQueryGrouping($query); - $this->hook(self::HOOK_AFTER_SELECT, [$query]); + $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); return $query->dsql()->field('count(*)')->table($this->expr('([]) der', [$query])); case 'field': diff --git a/src/Model/Union.php b/src/Model/Union.php index ce08f1941..44a895c5a 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -23,10 +23,10 @@ class Union extends Model { /** @const string */ - public const HOOK_AFTER_SELECT = self::class . '@afterSelect'; + public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; - /** @deprecated use HOOK_AFTER_SELECT instead - will be removed dec-2020 */ - public const HOOK_AFTER_UNION_SELECT = self::HOOK_AFTER_SELECT; + /** @deprecated use HOOK_INIT_SELECT_QUERY instead - will be removed dec-2020 */ + public const HOOK_AFTER_UNION_SELECT = self::HOOK_INIT_SELECT_QUERY; /** * Union model should always be read-only. @@ -260,7 +260,7 @@ public function action($mode, $args = []) if (isset($this->group)) { $query->group($this->group); } - $this->hook(self::HOOK_AFTER_SELECT, [$query]); + $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); return $query; case 'count': From 19c6689d0cee7cbefd8bc9bd272a837f05af1c46 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 27 Jul 2020 23:54:07 +0200 Subject: [PATCH 010/151] [update] provide integration trait for generic support of aggregates on model --- src/Model.php | 1 + src/Model/Aggregate.php | 27 ++++++++++++++++++++++----- src/Model/AggregatesTrait.php | 31 +++++++++++++++++++++++++++++++ tests/ModelAggregateTest.php | 3 +-- 4 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 src/Model/AggregatesTrait.php diff --git a/src/Model.php b/src/Model.php index 1dbe607db..21763e180 100644 --- a/src/Model.php +++ b/src/Model.php @@ -34,6 +34,7 @@ class Model implements \IteratorAggregate use CollectionTrait; use ReadableCaptionTrait; use Model\HasUserActionsTrait; + use Model\AggregatesTrait; /** @const string */ public const HOOK_BEFORE_LOAD = self::class . '@beforeLoad'; diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 9a7ab9344..85bda9c82 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -89,18 +89,18 @@ public function __construct(Model $model, array $defaults = []) /** * Specify a single field or array of fields on which we will group model. * - * @param array $group Array of field names + * @param array $fields Array of field names * @param array $aggregate Array of aggregate mapping * * @return $this */ - public function groupBy(array $group, array $aggregate = []) + public function groupBy(array $fields, array $aggregate = []) { - $this->group = $group; + $this->group = $fields; $this->aggregate = $aggregate; - $this->system_fields = array_unique($this->system_fields + $group); - foreach ($group as $fieldName) { + $this->system_fields = array_unique($this->system_fields + $fields); + foreach ($fields as $fieldName) { $this->addField($fieldName); } @@ -132,6 +132,23 @@ public function getRef($link): Reference return $this->master_model->getRef($link); } + /** + * Method to enable commutative usage of methods enabling both of below + * Resulting in Aggregate on $model. + * + * $model->groupBy(['abc'])->withAggregateField('xyz'); + * + * and + * + * $model->withAggregateField('xyz')->groupBy(['abc']); + */ + public function withAggregateField($name, $seed = []) + { + static::addField(...func_get_args()); + + return $this; + } + /** * Adds new field into model. * diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php new file mode 100644 index 000000000..75a7cacf9 --- /dev/null +++ b/src/Model/AggregatesTrait.php @@ -0,0 +1,31 @@ +withAggregateField(...func_get_args()); + } + + /** + * @see Aggregate::groupBy. + * + * @return \atk4\data\Model + */ + public function groupBy(array $group, array $aggregate = []) + { + return (new Aggregate($this))->groupBy(...func_get_args()); + } +} diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 2ede199ac..b50458905 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -37,8 +37,7 @@ protected function setUp(): void $invoice = new Model\Invoice($this->db); $invoice->getRef('client_id')->addTitle(); - $this->aggregate = new Aggregate($invoice); - $this->aggregate->addField('client'); + $this->aggregate = $invoice->withAggregateField('client'); } public function testGroupSelect() From a03dbc5c58d05545811ee8b794e428f759e04e48 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 10:48:06 +0100 Subject: [PATCH 011/151] [update] add return types --- src/Model/AggregatesTrait.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php index 75a7cacf9..413083f8a 100644 --- a/src/Model/AggregatesTrait.php +++ b/src/Model/AggregatesTrait.php @@ -4,6 +4,8 @@ namespace atk4\data\Model; +use atk4\data\Model; + /** * Provides aggregation methods. */ @@ -11,20 +13,16 @@ trait AggregatesTrait { /** * @see Aggregate::withAggregateField. - * - * @return \atk4\data\Model */ - public function withAggregateField($name, $seed = []) + public function withAggregateField($name, $seed = []): Model { return (new Aggregate($this))->withAggregateField(...func_get_args()); } /** * @see Aggregate::groupBy. - * - * @return \atk4\data\Model */ - public function groupBy(array $group, array $aggregate = []) + public function groupBy(array $group, array $aggregate = []): Model { return (new Aggregate($this))->groupBy(...func_get_args()); } From 66483ed79538cd016b7079307940cddf7d7c883e Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 10:57:02 +0100 Subject: [PATCH 012/151] [fix] add method return types in Aggregate and Union --- src/Model/Aggregate.php | 4 ++-- src/Model/Union.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 705deecbd..5cf06a755 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -94,7 +94,7 @@ public function __construct(Model $model, array $defaults = []) * * @return $this */ - public function groupBy(array $fields, array $aggregate = []) + public function groupBy(array $fields, array $aggregate = []): Model { $this->group = $fields; $this->aggregate = $aggregate; @@ -142,7 +142,7 @@ public function getRef($link): Reference * * $model->withAggregateField('xyz')->groupBy(['abc']); */ - public function withAggregateField($name, $seed = []) + public function withAggregateField($name, $seed = []): Model { static::addField(...func_get_args()); diff --git a/src/Model/Union.php b/src/Model/Union.php index fca093205..e3c0808f4 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -321,7 +321,7 @@ public function addNestedModel($class, array $fieldMap = []): Model * * @return $this */ - public function groupBy($group, array $aggregate = []) + public function groupBy($group, array $aggregate = []): Model { $this->aggregate = $aggregate; $this->group = $group; From 4b4affab84a321b6869e6e50b9620d68e865df11 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 11:25:55 +0100 Subject: [PATCH 013/151] [fix] clone test models on use --- tests/ModelUnionTest.php | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 0d941c225..4992e8a4b 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -64,7 +64,7 @@ protected function createClient($persistence = null) public function testFieldExpr() { - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $this->assertSameSql('"amount"', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount')])->render()); $this->assertSameSql('-"amount"', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount', '-[]')])->render()); @@ -73,7 +73,7 @@ public function testFieldExpr() public function testNestedQuery1() { - $transaction = $this->transaction; + $transaction = clone $this->transaction; $this->assertSameSql( '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"', @@ -96,7 +96,7 @@ public function testNestedQuery1() */ public function testMissingField() { - $transaction = $this->transaction; + $transaction = clone $this->transaction; $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->addField('type'); @@ -108,7 +108,7 @@ public function testMissingField() public function testActions() { - $transaction = $this->transaction; + $transaction = clone $this->transaction; $this->assertSameSql( 'select "name","amount" from (select "name" "name","amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"', @@ -130,7 +130,7 @@ public function testActions() $transaction->action('fx', ['sum', 'amount'])->render() ); - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $this->assertSameSql( 'select sum("val") from (select sum(-"amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"', @@ -140,17 +140,17 @@ public function testActions() public function testActions2() { - $transaction = $this->transaction; + $transaction = clone $this->transaction; $this->assertSame(5, (int) $transaction->action('count')->getOne()); $this->assertSame(37.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $this->assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); } public function testSubAction1() { - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $this->assertSameSql( '(select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment") "derivedTable"', @@ -170,7 +170,7 @@ public function testBasics() $this->assertSame(19.0, (float) $client->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); - $transaction = $this->transaction; + $transaction = clone $this->transaction; $this->assertSame([ ['name' => 'chair purchase', 'amount' => 4.0], @@ -193,7 +193,7 @@ public function testBasics() $client->load(1); - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $this->assertSame([ ['name' => 'chair purchase', 'amount' => -4.0], @@ -215,7 +215,7 @@ public function testBasics() public function testGrouping1() { - $transaction = $this->transaction; + $transaction = clone $this->transaction; $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); @@ -224,7 +224,7 @@ public function testGrouping1() $transaction->getSubQuery(['name', 'amount'])->render() ); - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); @@ -236,7 +236,7 @@ public function testGrouping1() public function testGrouping2() { - $transaction = $this->transaction; + $transaction = clone $this->transaction; $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); @@ -245,7 +245,7 @@ public function testGrouping2() $transaction->action('select', [['name', 'amount']])->render() ); - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); @@ -261,7 +261,7 @@ public function testGrouping2() */ public function testGrouping3() { - $transaction = $this->transaction; + $transaction = clone $this->transaction; $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); $transaction->setOrder('name'); @@ -272,7 +272,7 @@ public function testGrouping3() ['name' => 'table purchase', 'amount' => 15.0], ], $transaction->export()); - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); $transaction->setOrder('name'); @@ -290,7 +290,7 @@ public function testGrouping3() */ public function testSubGroupingByExpressions() { - $transaction = $this->transaction; + $transaction = clone $this->transaction; $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->nestedPayment->addExpression('type', '\'payment\''); $transaction->addField('type'); @@ -302,7 +302,7 @@ public function testSubGroupingByExpressions() ['type' => 'payment', 'amount' => 14.0], ], $transaction->export(['type', 'amount'])); - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->nestedPayment->addExpression('type', '\'payment\''); $transaction->addField('type'); @@ -352,7 +352,7 @@ public function testReference() */ public function testFieldAggregate() { - $client = $this->client; + $client = clone $this->client; $client->hasMany('tr', $this->createTransaction()) ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); @@ -366,7 +366,7 @@ public function testFieldAggregate() */ public function testConditionOnMappedField() { - $transaction = $this->subtractInvoiceTransaction; + $transaction = clone $this->subtractInvoiceTransaction; $transaction->nestedInvoice->addCondition('amount', 4); $this->assertSame([ From c43694bf333571fb0d1d0caa36d27c1a75b44efc Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 12:45:42 +0100 Subject: [PATCH 014/151] [fix] skip non-aggregate fields on union select sub-query --- src/Model/Union.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Model/Union.php b/src/Model/Union.php index e3c0808f4..9626010bd 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -138,6 +138,11 @@ public function getSubQuery(array $fields): Expression continue; } + // if we group we do not select non-aggregate fields + if ($this->group && !in_array($fieldName, (array) $this->group, true) && !isset($this->aggregate[$fieldName])) { + continue; + } + $fieldExpression = $this->getFieldExpr($nestedModel, $fieldName, $fieldMap[$fieldName] ?? null); if (isset($this->aggregate[$fieldName])) { From 1325b54d102285067b299a147f5aef3dc23ed2d3 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 12:56:16 +0100 Subject: [PATCH 015/151] [fix] add test export sorting order --- tests/ModelAggregateTest.php | 34 +++++++++++++++++----------------- tests/ReportTest.php | 16 ++++++++-------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index b50458905..27f59f72d 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -42,7 +42,7 @@ protected function setUp(): void public function testGroupSelect() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); @@ -51,13 +51,13 @@ public function testGroupSelect() ['client' => 'Vinny', 'client_id' => '1', 'c' => 2], ['client' => 'Zoe', 'client_id' => '2', 'c' => 1], ], - $aggregate->export() + $aggregate->setOrder('client_id', 'asc')->export() ); } public function testGroupSelect2() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -68,13 +68,13 @@ public function testGroupSelect2() ['client' => 'Vinny', 'client_id' => '1', 'amount' => 19.0], ['client' => 'Zoe', 'client_id' => '2', 'amount' => 4.0], ], - $aggregate->export() + $aggregate->setOrder('client_id', 'asc')->export() ); } public function testGroupSelect3() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -88,13 +88,13 @@ public function testGroupSelect3() ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'min' => 4.0, 'max' => 4.0, 'amount' => 4.0], ], - $aggregate->export() + $aggregate->setOrder('client_id', 'asc')->export() ); } public function testGroupSelectExpr() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -108,13 +108,13 @@ public function testGroupSelectExpr() ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ], - $aggregate->export() + $aggregate->setOrder('client_id', 'asc')->export() ); } public function testGroupSelectCondition() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->master_model->addCondition('name', 'chair purchase'); $aggregate->groupBy(['client_id'], [ @@ -129,13 +129,13 @@ public function testGroupSelectCondition() ['client' => 'Vinny', 'client_id' => '1', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ], - $aggregate->export() + $aggregate->setOrder('client_id', 'asc')->export() ); } public function testGroupSelectCondition2() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -155,7 +155,7 @@ public function testGroupSelectCondition2() public function testGroupSelectCondition3() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -175,7 +175,7 @@ public function testGroupSelectCondition3() public function testGroupSelectCondition4() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -195,7 +195,7 @@ public function testGroupSelectCondition4() public function testGroupLimit() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -206,13 +206,13 @@ public function testGroupLimit() [ ['client' => 'Vinny', 'client_id' => '1', 'amount' => 19.0], ], - $aggregate->export() + $aggregate->setOrder('client_id', 'asc')->export() ); } public function testGroupLimit2() { - $aggregate = $this->aggregate; + $aggregate = clone $this->aggregate; $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -223,7 +223,7 @@ public function testGroupLimit2() [ ['client' => 'Zoe', 'client_id' => '2', 'amount' => 4.0], ], - $aggregate->export() + $aggregate->setOrder('client_id', 'asc')->export() ); } } diff --git a/tests/ReportTest.php b/tests/ReportTest.php index 395cd93c9..e39ff98bb 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -27,31 +27,31 @@ class ReportTest extends \atk4\schema\PhpunitTestCase ]; /** @var Aggregate */ - protected $g; + protected $invoiceAggregate; protected function setUp(): void { parent::setUp(); $this->setDB($this->init_db); - $m1 = new Model\Invoice($this->db); - $m1->getRef('client_id')->addTitle(); - $this->g = new Aggregate($m1); - $this->g->addField('client'); + $invoice = new Model\Invoice($this->db); + $invoice->getRef('client_id')->addTitle(); + $this->invoiceAggregate = new Aggregate($invoice); + $this->invoiceAggregate->addField('client'); } public function testAliasGroupSelect() { - $g = $this->g; + $invoiceAggregate = clone $this->invoiceAggregate; - $g->groupBy(['client_id'], ['c' => ['count(*)', 'type' => 'integer']]); + $invoiceAggregate->groupBy(['client_id'], ['c' => ['count(*)', 'type' => 'integer']]); $this->assertSame( [ ['client' => 'Vinny', 'client_id' => '1', 'c' => 2], ['client' => 'Zoe', 'client_id' => '2', 'c' => 1], ], - $g->export() + $invoiceAggregate->setOrder('client_id', 'asc')->export() ); } } From 650e44936cadc31757afab866666bb35204b4606 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 17:45:27 +0100 Subject: [PATCH 016/151] [fix] teardown class properties --- tests/ModelAggregateTest.php | 7 +++++++ tests/ModelUnionTest.php | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 27f59f72d..156652fbd 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -40,6 +40,13 @@ protected function setUp(): void $this->aggregate = $invoice->withAggregateField('client'); } + protected function tearDown(): void + { + $this->aggregate = null; + + parent::tearDown(); + } + public function testGroupSelect() { $aggregate = clone $this->aggregate; diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 4992e8a4b..b711e9ddc 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -42,6 +42,15 @@ protected function setUp(): void $this->subtractInvoiceTransaction = $this->createSubtractInvoiceTransaction($this->db); } + protected function tearDown(): void + { + $this->client = null; + $this->transaction = null; + $this->subtractInvoiceTransaction = null; + + parent::tearDown(); + } + protected function createTransaction($persistence = null) { return new Model\Transaction($persistence); From bb349c1adb8c46ec5379a3a1ddf8572abd628fac Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 22:11:23 +0100 Subject: [PATCH 017/151] [fix] mark test incomplete --- tests/ModelUnionTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index b711e9ddc..5ff11e3d1 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -4,6 +4,8 @@ namespace atk4\data\tests; +use Doctrine\DBAL\Platforms\OraclePlatform; + class ModelUnionTest extends \atk4\schema\PhpunitTestCase { /** @var Model\Client */ @@ -299,6 +301,10 @@ public function testGrouping3() */ public function testSubGroupingByExpressions() { + if ($this->getDatabasePlatform() instanceof OraclePlatform) { // TODO + $this->markTestIncomplete('TODO - for some reasons Oracle does not accept the query'); + } + $transaction = clone $this->transaction; $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->nestedPayment->addExpression('type', '\'payment\''); From ee3be7927738b002b08c0a3289da01b83a5cdb2f Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 23:13:48 +0100 Subject: [PATCH 018/151] [update] rename properties --- src/Model/Aggregate.php | 80 +++++++++++++----------------------- tests/ModelAggregateTest.php | 2 +- 2 files changed, 30 insertions(+), 52 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 5cf06a755..4bcf4f344 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -45,6 +45,12 @@ class Aggregate extends Model */ public const HOOK_AFTER_GROUP_SELECT = self::HOOK_INIT_SELECT_QUERY; + /** @var array */ + protected $systemFields = []; + + /** @var Model */ + public $baseModel; + /** * Aggregate model should always be read-only. * @@ -59,28 +65,22 @@ class Aggregate extends Model */ public $id_field; - /** @var Model */ - public $master_model; - /** @var array */ public $group = []; /** @var array */ public $aggregate = []; - /** @var array */ - public $system_fields = []; - /** * Constructor. */ - public function __construct(Model $model, array $defaults = []) + public function __construct(Model $baseModel, array $defaults = []) { - $this->master_model = $model; - $this->table = $model->table; + $this->baseModel = $baseModel; + $this->table = $baseModel->table; //$this->_default_class_addExpression = $model->_default_class_addExpression; - parent::__construct($model->persistence, $defaults); + parent::__construct($baseModel->persistence, $defaults); // always use table prefixes for this model $this->persistence_data['use_table_prefixes'] = true; @@ -99,7 +99,7 @@ public function groupBy(array $fields, array $aggregate = []): Model $this->group = $fields; $this->aggregate = $aggregate; - $this->system_fields = array_unique($this->system_fields + $fields); + $this->systemFields = array_unique($this->systemFields + $fields); foreach ($fields as $fieldName) { $this->addField($fieldName); } @@ -109,11 +109,11 @@ public function groupBy(array $fields, array $aggregate = []): Model $args = []; // if field originally defined in the parent model, then it can be used as part of expression - if ($this->master_model->hasField($fieldName)) { - $args = [$this->master_model->getField($fieldName)]; // @TODO Probably need cloning here + if ($this->baseModel->hasField($fieldName)) { + $args = [$this->baseModel->getField($fieldName)]; // @TODO Probably need cloning here } - $seed['expr'] = $this->master_model->expr($seed[0] ?? $seed['expr'], $args); + $seed['expr'] = $this->baseModel->expr($seed[0] ?? $seed['expr'], $args); // now add the expressions here $this->addExpression($fieldName, $seed); @@ -129,7 +129,7 @@ public function groupBy(array $fields, array $aggregate = []): Model */ public function getRef($link): Reference { - return $this->master_model->getRef($link); + return $this->baseModel->getRef($link); } /** @@ -166,8 +166,8 @@ public function addField(string $name, $seed = []): Field return parent::addField($name, $seed); } - if ($this->master_model->hasField($name)) { - $field = clone $this->master_model->getField($name); + if ($this->baseModel->hasField($name)) { + $field = clone $this->baseModel->getField($name); $field->unsetOwner(); // will be new owner } else { $field = null; @@ -193,12 +193,12 @@ public function initQueryFields(Query $query, array $fields = []): Query */ public function initQueryGrouping(Query $query) { - // use table alias of master model - $this->table_alias = $this->master_model->table_alias; + // use table alias of base model + $this->table_alias = $this->baseModel->table_alias; foreach ($this->group as $field) { - if ($this->master_model->hasField($field)) { - $expression = $this->master_model->getField($field); + if ($this->baseModel->hasField($field)) { + $expression = $this->baseModel->getField($field); } else { $expression = $this->expr($field); } @@ -219,7 +219,7 @@ public function initQueryGrouping(Query $query) */ public function setLimit(int $count = null, int $offset = 0) { - $this->master_model->setLimit($count, $offset); + $this->baseModel->setLimit($count, $offset); return $this; } @@ -236,7 +236,7 @@ public function setLimit(int $count = null, int $offset = 0) */ public function setOrder($field, string $desc = null) { - $this->master_model->setOrder($field, $desc); + $this->baseModel->setOrder($field, $desc); return $this; } @@ -251,14 +251,13 @@ public function setOrder($field, string $desc = null) */ public function action($mode, $args = []) { - $subquery = null; switch ($mode) { case 'select': $fields = $this->only_fields ?: array_keys($this->getFields()); // select but no need your fields - $query = $this->master_model->action($mode, [false]); - $this->initQueryFields($query, array_unique($fields + $this->system_fields)); + $query = $this->baseModel->action($mode, [false]); + $this->initQueryFields($query, array_unique($fields + $this->systemFields)); $this->initQueryGrouping($query); $this->initQueryConditions($query); @@ -267,7 +266,7 @@ public function action($mode, $args = []) return $query; case 'count': - $query = $this->master_model->action($mode, $args); + $query = $this->baseModel->action($mode, $args); $query->reset('field')->field($this->expr('1')); $this->initQueryGrouping($query); @@ -276,37 +275,16 @@ public function action($mode, $args = []) return $query->dsql()->field('count(*)')->table($this->expr('([]) der', [$query])); case 'field': - if (!isset($args[0])) { - throw (new Exception('This action requires one argument with field name')) - ->addMoreInfo('mode', $mode); - } - - if (!is_string($args[0])) { - throw (new Exception('action "field" only support string fields')) - ->addMoreInfo('field', $args[0]); - } - - $subquery = $this->getSubQuery([$args[0]]); - - break; case 'fx': - $subquery = $this->getSubAction('fx', [$args[0], $args[1], 'alias' => 'val']); - - $args = [$args[0], $this->expr('val')]; - - break; + return parent::action($mode, $args); default: throw (new Exception('Aggregate model does not support this action')) ->addMoreInfo('mode', $mode); } - - // Substitute FROM table with our subquery expression - return parent::action($mode, $args)->reset('table')->table($subquery); } /** - * Our own way applying conditions, where we use "having" for - * fields. + * Our own way applying conditions, where we use "having" for fields. */ public function initQueryConditions(Query $query, Model\Scope\AbstractScope $condition = null): void { @@ -344,7 +322,7 @@ public function __debugInfo(): array return array_merge(parent::__debugInfo(), [ 'group' => $this->group, 'aggregate' => $this->aggregate, - 'master_model' => $this->master_model->__debugInfo(), + 'baseModel' => $this->baseModel->__debugInfo(), ]); } diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 156652fbd..1fd7ad225 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -122,7 +122,7 @@ public function testGroupSelectExpr() public function testGroupSelectCondition() { $aggregate = clone $this->aggregate; - $aggregate->master_model->addCondition('name', 'chair purchase'); + $aggregate->baseModel->addCondition('name', 'chair purchase'); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], From 1274012b323198b5f979ae22cfa355517a215f5d Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 2 Dec 2020 23:38:17 +0100 Subject: [PATCH 019/151] [update] add annotations for $persistence and expr --- src/Model/Aggregate.php | 32 ++++++++++---------------------- src/Model/Union.php | 4 ++++ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 4bcf4f344..dc5fc9031 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -8,6 +8,7 @@ use atk4\data\Field; use atk4\data\FieldSqlExpression; use atk4\data\Model; +use atk4\data\Persistence; use atk4\data\Reference; use atk4\dsql\Query; @@ -34,6 +35,10 @@ * * You can also pass seed (for example field type) when aggregating: * $aggregate->groupBy(['first','last'], ['salary' => ['sum([])', 'type'=>'money']]; + * + * @property \atk4\data\Persistence\Sql $persistence + * + * @method Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model */ class Aggregate extends Model { @@ -47,7 +52,7 @@ class Aggregate extends Model /** @var array */ protected $systemFields = []; - + /** @var Model */ public $baseModel; @@ -76,10 +81,13 @@ class Aggregate extends Model */ public function __construct(Model $baseModel, array $defaults = []) { + if (!$baseModel->persistence instanceof Persistence\Sql) { + throw new Exception('Base model must have Sql persistence to use grouping'); + } + $this->baseModel = $baseModel; $this->table = $baseModel->table; - //$this->_default_class_addExpression = $model->_default_class_addExpression; parent::__construct($baseModel->persistence, $defaults); // always use table prefixes for this model @@ -207,16 +215,6 @@ public function initQueryGrouping(Query $query) } } - /** - * Sets limit. - * - * @param int $count - * @param int|null $offset - * - * @return $this - * - * @todo Incorrect implementation - */ public function setLimit(int $count = null, int $offset = 0) { $this->baseModel->setLimit($count, $offset); @@ -224,16 +222,6 @@ public function setLimit(int $count = null, int $offset = 0) return $this; } - /** - * Sets order. - * - * @param mixed $field - * @param bool|null $desc - * - * @return $this - * - * @todo Incorrect implementation - */ public function setOrder($field, string $desc = null) { $this->baseModel->setOrder($field, $desc); diff --git a/src/Model/Union.php b/src/Model/Union.php index 9626010bd..cee1fe07a 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -19,6 +19,10 @@ * * For example if you are asking sum(amount), there is no need to fetch any extra * fields from sub-models. + * + * @property \atk4\data\Persistence\Sql $persistence + * + * @method Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model */ class Union extends Model { From 6e5f66a4f385c733b394764708c4ef3bdd062bf8 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Thu, 3 Dec 2020 11:49:48 +0100 Subject: [PATCH 020/151] [update] use clone of base model --- src/Model/Aggregate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index dc5fc9031..9510b12b9 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -85,7 +85,7 @@ public function __construct(Model $baseModel, array $defaults = []) throw new Exception('Base model must have Sql persistence to use grouping'); } - $this->baseModel = $baseModel; + $this->baseModel = clone $baseModel; $this->table = $baseModel->table; parent::__construct($baseModel->persistence, $defaults); From 9059644c060a9719a34249cee4816d1f2c51854b Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Thu, 3 Dec 2020 11:50:37 +0100 Subject: [PATCH 021/151] [update] method signature and phpdoc --- src/Model/Union.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index cee1fe07a..018f486bc 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -310,13 +310,10 @@ public function action($mode, $args = []) /** * Adds nested model in union. - * - * @param string|Model $class model - * @param array $fieldMap Array of field mapping */ - public function addNestedModel($class, array $fieldMap = []): Model + public function addNestedModel(Model $model, array $fieldMap = []): Model { - $nestedModel = $this->persistence->add($class); + $nestedModel = $this->persistence->add($model); $this->union[] = [$nestedModel, $fieldMap]; From 8c8bb418445e63847549abaab7cde3fbea35ab34 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Thu, 3 Dec 2020 11:51:16 +0100 Subject: [PATCH 022/151] [update] aggregate order --- src/Model/Aggregate.php | 28 +++++++++++++++++++++------- tests/ModelAggregateTest.php | 16 ++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 9510b12b9..cfb7e36a8 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -10,6 +10,7 @@ use atk4\data\Model; use atk4\data\Persistence; use atk4\data\Reference; +use atk4\dsql\Expression; use atk4\dsql\Query; /** @@ -222,13 +223,6 @@ public function setLimit(int $count = null, int $offset = 0) return $this; } - public function setOrder($field, string $desc = null) - { - $this->baseModel->setOrder($field, $desc); - - return $this; - } - /** * Execute action. * @@ -247,6 +241,7 @@ public function action($mode, $args = []) $query = $this->baseModel->action($mode, [false]); $this->initQueryFields($query, array_unique($fields + $this->systemFields)); + $this->initQueryOrder($query); $this->initQueryGrouping($query); $this->initQueryConditions($query); @@ -271,6 +266,25 @@ public function action($mode, $args = []) } } + protected function initQueryOrder(Query $query) + { + if ($this->order) { + foreach ($this->order as $order) { + $isDesc = strtolower($order[1]) === 'desc'; + + if ($order[0] instanceof Expression) { + $query->order($order[0], $isDesc); + } elseif (is_string($order[0])) { + $query->order($this->getField($order[0]), $isDesc); + } else { + throw (new Exception('Unsupported order parameter')) + ->addMoreInfo('model', $this) + ->addMoreInfo('field', $order[0]); + } + } + } + } + /** * Our own way applying conditions, where we use "having" for fields. */ diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 1fd7ad225..66784c77f 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -200,6 +200,22 @@ public function testGroupSelectCondition4() ); } + public function testGroupOrder() + { + $aggregate = clone $this->aggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->setOrder('client_id', 'asc'); + + $this->assertSameSql( + 'select (select "name" from "client" "c" where "id" = "invoice"."client_id") "client","invoice"."client_id",sum("invoice"."amount") "amount" from "invoice" group by "invoice"."client_id" order by "invoice"."client_id"', + $aggregate->action('select')->render() + ); + } + public function testGroupLimit() { $aggregate = clone $this->aggregate; From 4922475a36713544bcd9ea3b6f2f3e6a4e04c329 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Thu, 3 Dec 2020 12:16:42 +0100 Subject: [PATCH 023/151] [refactor] Union::getSubAction --- src/Model/Union.php | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index 018f486bc..571a5fe4f 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -207,40 +207,38 @@ public function getSubQuery(array $fields): Expression return $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ') {' . $cnt . '}', $args); } - /** - * No description. - */ - public function getSubAction(string $action, array $act_arg = []): Expression + public function getSubAction(string $action, array $actionArgs = []): Expression { $cnt = 0; $expr = []; - $args = []; + $exprArgs = []; foreach ($this->union as [$model, $mapping]) { + $modelActionArgs = $actionArgs; + // now prepare query $expr[] = '[' . $cnt . ']'; - if ($act_arg && isset($act_arg[1])) { - $a = $act_arg; - $a[1] = $this->getFieldExpr( + if ($fieldName = $actionArgs[1] ?? null) { + $modelActionArgs[1] = $this->getFieldExpr( $model, - $a[1], - $mapping[$a[1]] ?? null + $fieldName, + $mapping[$fieldName] ?? null ); - $query = $model->action($action, $a); - } else { - $query = $model->action($action, $act_arg); } + $query = $model->action($action, $modelActionArgs); + // subquery should not be wrapped in parenthesis, SQLite is especially picky about that $query->wrapInParentheses = false; - $args[$cnt++] = $query; + $exprArgs[$cnt++] = $query; } + $expr = '(' . implode(' UNION ALL ', $expr) . ') {' . $cnt . '}'; // last element is table name itself - $args[$cnt] = $this->table; + $exprArgs[$cnt] = $this->table; - return $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ') {' . $cnt . '}', $args); + return $this->persistence->dsql()->expr($expr, $exprArgs); } /** @@ -294,7 +292,9 @@ public function action($mode, $args = []) break; case 'fx': - $subquery = $this->getSubAction('fx', [$args[0], $args[1], 'alias' => 'val']); + $args['alias'] = 'val'; + + $subquery = $this->getSubAction('fx', $args); $args = [$args[0], $this->expr('{}', ['val'])]; From 7af4243bf9b823cbf008f5ec30314b778b9224b4 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Thu, 3 Dec 2020 13:37:42 +0100 Subject: [PATCH 024/151] [update] code comments --- src/Model/Aggregate.php | 2 +- src/Model/Union.php | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index cfb7e36a8..98859be86 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -119,7 +119,7 @@ public function groupBy(array $fields, array $aggregate = []): Model $args = []; // if field originally defined in the parent model, then it can be used as part of expression if ($this->baseModel->hasField($fieldName)) { - $args = [$this->baseModel->getField($fieldName)]; // @TODO Probably need cloning here + $args = [$this->baseModel->getField($fieldName)]; } $seed['expr'] = $this->baseModel->expr($seed[0] ?? $seed['expr'], $args); diff --git a/src/Model/Union.php b/src/Model/Union.php index 571a5fe4f..fc83ec287 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -213,7 +213,7 @@ public function getSubAction(string $action, array $actionArgs = []): Expression $expr = []; $exprArgs = []; - foreach ($this->union as [$model, $mapping]) { + foreach ($this->union as [$model, $fieldMap]) { $modelActionArgs = $actionArgs; // now prepare query @@ -222,7 +222,7 @@ public function getSubAction(string $action, array $actionArgs = []): Expression $modelActionArgs[1] = $this->getFieldExpr( $model, $fieldName, - $mapping[$fieldName] ?? null + $fieldMap[$fieldName] ?? null ); } @@ -324,8 +324,6 @@ public function addNestedModel(Model $model, array $fieldMap = []): Model * Specify a single field or array of fields. * * @param string|array $group - * - * @return $this */ public function groupBy($group, array $aggregate = []): Model { From 6a089063efebdc5ba7378a64cc7252b43965542f Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Thu, 3 Dec 2020 16:29:55 +0100 Subject: [PATCH 025/151] [update] Union::addCondition --- src/Model/Union.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index fc83ec287..08405520e 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -119,9 +119,8 @@ public function getSubQuery(array $fields): Expression $queryFieldExpressions = []; foreach ($fields as $fieldName) { try { - // Union can be joined with additional - // table/query and we don't touch those - // fields + // Union can be joined with additional table/query + // We don't touch those fields if (!$this->hasField($fieldName)) { $queryFieldExpressions[$fieldName] = $nestedModel->expr('NULL'); @@ -381,8 +380,8 @@ public function addCondition($key, $operator = null, $value = null, $forceNested return parent::addCondition(...func_get_args()); } - // otherwise add condition in all sub-models - foreach ($this->union as $n => [$nestedModel, $fieldMap]) { + // otherwise add condition in all nested models + foreach ($this->union as [$nestedModel, $fieldMap]) { try { $field = $key; @@ -404,14 +403,13 @@ public function addCondition($key, $operator = null, $value = null, $forceNested $nestedModel->addCondition($field, $operator); break; - case 3: - case 4: + default: $nestedModel->addCondition($field, $operator, $value); break; } } catch (\atk4\core\Exception $e) { - throw $e->addMoreInfo('sub_model', $n); + throw $e->addMoreInfo('nestedModel', get_class($nestedModel)); } } From 993631587442feab3ed92c0ba2842688ed75c268 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Mon, 7 Dec 2020 10:04:33 +0100 Subject: [PATCH 026/151] [update] fix complex conditions handling using field alias --- src/Field.php | 6 ++- src/Model/Aggregate.php | 4 +- src/Model/Scope/Condition.php | 4 +- src/Model/Union.php | 6 +-- tests/ModelAggregateTest.php | 98 +++++++++++++++++++++++++++++------ tests/ModelUnionTest.php | 58 +++++++++++++++++++++ 6 files changed, 151 insertions(+), 25 deletions(-) diff --git a/src/Field.php b/src/Field.php index 1e82f474b..854b67921 100644 --- a/src/Field.php +++ b/src/Field.php @@ -559,7 +559,7 @@ public function useAlias(): bool * @param string|null $operator one of Scope\Condition operators * @param mixed $value the condition value to be handled */ - public function getQueryArguments($operator, $value): array + public function getQueryArguments($operator, $value, $useFieldAlias = false): array { $skipValueTypecast = [ Scope\Condition::OPERATOR_LIKE, @@ -578,7 +578,9 @@ public function getQueryArguments($operator, $value): array } } - return [$this, $operator, $value]; + $field = $useFieldAlias && $this->useAlias() ? $this->short_name : $this; + + return [$field, $operator, $value]; } // }}} diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 98859be86..3b2a550b9 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -298,7 +298,7 @@ public function initQueryConditions(Query $query, Model\Scope\AbstractScope $con // simple condition if ($condition instanceof Model\Scope\Condition) { - $query = $query->having(...$condition->toQueryArguments()); + $query->having(...$condition->toQueryArguments(true)); } // nested conditions @@ -309,7 +309,7 @@ public function initQueryConditions(Query $query, Model\Scope\AbstractScope $con $this->initQueryConditions($expression, $nestedCondition); } - $query = $query->having($expression); + $query->having($expression); } } } diff --git a/src/Model/Scope/Condition.php b/src/Model/Scope/Condition.php index ac41fc64e..9c999e8ef 100644 --- a/src/Model/Scope/Condition.php +++ b/src/Model/Scope/Condition.php @@ -178,7 +178,7 @@ protected function onChangeModel(): void } } - public function toQueryArguments(): array + public function toQueryArguments($useFieldAlias = false): array { if ($this->isEmpty()) { return []; @@ -220,7 +220,7 @@ public function toQueryArguments(): array // handle the query arguments using field if ($field instanceof Field) { - [$field, $operator, $value] = $field->getQueryArguments($operator, $value); + [$field, $operator, $value] = $field->getQueryArguments($operator, $value, $useFieldAlias); } // only expression contained in $field diff --git a/src/Model/Union.php b/src/Model/Union.php index 08405520e..fe987697e 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -114,7 +114,7 @@ public function getSubQuery(array $fields): Expression $expr = []; $args = []; - foreach ($this->union as $n => [$nestedModel, $fieldMap]) { + foreach ($this->union as [$nestedModel, $fieldMap]) { // map fields for related model $queryFieldExpressions = []; foreach ($fields as $fieldName) { @@ -157,7 +157,7 @@ public function getSubQuery(array $fields): Expression $queryFieldExpressions[$fieldName] = $fieldExpression; } catch (\atk4\core\Exception $e) { - throw $e->addMoreInfo('model', $n); + throw $e->addMoreInfo('nestedModel', get_class($nestedModel)); } } @@ -389,7 +389,7 @@ public function addCondition($key, $operator = null, $value = null, $forceNested // field is included in mapping - use mapping expression $field = $fieldMap[$key] instanceof Expression ? $fieldMap[$key] - : $this->expr($fieldMap[$key], $nestedModel->getFields()); + : $this->getFieldExpr($nestedModel, $key, $fieldMap[$key]); } elseif (is_string($key) && $nestedModel->hasField($key)) { // model has such field - use that field directly $field = $nestedModel->getField($key); diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 66784c77f..9294d1af7 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -5,6 +5,8 @@ namespace atk4\data\tests; use atk4\data\Model\Aggregate; +use atk4\data\Model\Scope; +use atk4\data\Model\Scope\Condition; class ModelAggregateTest extends \atk4\schema\PhpunitTestCase { @@ -26,30 +28,46 @@ class ModelAggregateTest extends \atk4\schema\PhpunitTestCase ], ]; + /** @var Model\Invoice */ + protected $invoice; /** @var Aggregate */ - protected $aggregate; + protected $invoiceAggregate; protected function setUp(): void { parent::setUp(); $this->setDB($this->init_db); - $invoice = new Model\Invoice($this->db); - $invoice->getRef('client_id')->addTitle(); + $this->invoice = new Model\Invoice($this->db); + $this->invoice->getRef('client_id')->addTitle(); - $this->aggregate = $invoice->withAggregateField('client'); + $this->invoiceAggregate = $this->invoice->withAggregateField('client'); } protected function tearDown(): void { - $this->aggregate = null; + $this->invoice = null; + $this->invoiceAggregate = null; parent::tearDown(); } + public function testGroupBy() + { + $invoiceAggregate = $this->invoice->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); + + $this->assertSame( + [ + ['client_id' => '1', 'c' => 2], + ['client_id' => '2', 'c' => 1], + ], + $invoiceAggregate->setOrder('client_id', 'asc')->export() + ); + } + public function testGroupSelect() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); @@ -64,7 +82,7 @@ public function testGroupSelect() public function testGroupSelect2() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -81,7 +99,7 @@ public function testGroupSelect2() public function testGroupSelect3() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -101,7 +119,7 @@ public function testGroupSelect3() public function testGroupSelectExpr() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -121,7 +139,7 @@ public function testGroupSelectExpr() public function testGroupSelectCondition() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->baseModel->addCondition('name', 'chair purchase'); $aggregate->groupBy(['client_id'], [ @@ -142,7 +160,7 @@ public function testGroupSelectCondition() public function testGroupSelectCondition2() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -162,7 +180,7 @@ public function testGroupSelectCondition2() public function testGroupSelectCondition3() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -182,7 +200,7 @@ public function testGroupSelectCondition3() public function testGroupSelectCondition4() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -200,9 +218,29 @@ public function testGroupSelectCondition4() ); } + public function testGroupSelectScope() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', 4.0)); + + $aggregate->addCondition($scope); + var_dump($aggregate->action('select')->getDebugQuery()); + $this->assertSame( + [ + ['client' => 'Zoe', 'client_id' => '2', 'amount' => 4.0], + ], + $aggregate->export() + ); + } + public function testGroupOrder() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -218,7 +256,7 @@ public function testGroupOrder() public function testGroupLimit() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -235,7 +273,7 @@ public function testGroupLimit() public function testGroupLimit2() { - $aggregate = clone $this->aggregate; + $aggregate = clone $this->invoiceAggregate; $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -249,4 +287,32 @@ public function testGroupLimit2() $aggregate->setOrder('client_id', 'asc')->export() ); } + + public function testGroupCount() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $this->assertSameSql( + 'select count(*) from ((select 1 from "invoice" group by "client_id")) der', + $aggregate->action('count')->render() + ); + } + + public function testAggregateFieldExpression() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['abc'], [ + 'xyz' => ['expr' => 'sum([amount])'], + ]); + + $this->assertSameSql( + 'select (select "name" from "client" "c" where "id" = "invoice"."client_id") "client","invoice"."abc",sum("invoice"."amount") "xyz" from "invoice" group by abc', + $aggregate->action('select')->render() + ); + } } diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 5ff11e3d1..979baf1eb 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -4,6 +4,7 @@ namespace atk4\data\tests; +use atk4\dsql\Expression; use Doctrine\DBAL\Platforms\OraclePlatform; class ModelUnionTest extends \atk4\schema\PhpunitTestCase @@ -376,6 +377,59 @@ public function testFieldAggregate() //$c->load(1); } + public function testConditionOnUnionField() + { + $transaction = clone $this->subtractInvoiceTransaction; + $transaction->addCondition('amount', '<', 0); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'table purchase', 'amount' => -15.0], + ['name' => 'chair purchase', 'amount' => -4.0], + ], $transaction->export()); + } + + public function testConditionOnNestedModelField() + { + $transaction = clone $this->subtractInvoiceTransaction; + $transaction->addCondition('client_id', '>', 1); + + $this->assertSame([ + ['name' => 'chair purchase', 'amount' => -4.0], + ['name' => 'full pay', 'amount' => 4.0], + ], $transaction->export()); + } + + public function testConditionForcedOnNestedModels1() + { + $transaction = clone $this->subtractInvoiceTransaction; + $transaction->addCondition('amount', '>', 5, true); + + $this->assertSame([ + ['name' => 'prepay', 'amount' => 10.0], + ], $transaction->export()); + } + + public function testConditionForcedOnNestedModels2() + { + $transaction = clone $this->subtractInvoiceTransaction; + $transaction->addCondition('amount', '<', -10, true); + + $this->assertSame([ + ['name' => 'table purchase', 'amount' => -15.0], + ], $transaction->export()); + } + + public function testConditionExpression() + { + $transaction = clone $this->subtractInvoiceTransaction; + $transaction->addCondition(new Expression('{} > 5', ['amount'])); + + $this->assertSame([ + ['name' => 'prepay', 'amount' => 10.0], + ], $transaction->export()); + } + /** * Model's conditions can still be placed on the original field values. */ @@ -391,4 +445,8 @@ public function testConditionOnMappedField() ['name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); } + + public function testNestedUnion() + { + } } From a8839b1d3f9c8bada4554f21bb9d1b736543cd9d Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 8 Dec 2020 15:02:50 +0100 Subject: [PATCH 027/151] [fix] revert field alias usage --- src/Field.php | 6 ++---- src/Model/Aggregate.php | 2 +- src/Model/Scope/Condition.php | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Field.php b/src/Field.php index 5be92a703..7d6b1b168 100644 --- a/src/Field.php +++ b/src/Field.php @@ -559,7 +559,7 @@ public function useAlias(): bool * @param string|null $operator one of Scope\Condition operators * @param mixed $value the condition value to be handled */ - public function getQueryArguments($operator, $value, $useFieldAlias = false): array + public function getQueryArguments($operator, $value): array { $skipValueTypecast = [ Scope\Condition::OPERATOR_LIKE, @@ -578,9 +578,7 @@ public function getQueryArguments($operator, $value, $useFieldAlias = false): ar } } - $field = $useFieldAlias && $this->useAlias() ? $this->short_name : $this; - - return [$field, $operator, $value]; + return [$this, $operator, $value]; } // }}} diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index e27d88e92..59b3ea8b1 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -298,7 +298,7 @@ public function initQueryConditions(Query $query, Model\Scope\AbstractScope $con // simple condition if ($condition instanceof Model\Scope\Condition) { - $query->having(...$condition->toQueryArguments(true)); + $query->having(...$condition->toQueryArguments()); } // nested conditions diff --git a/src/Model/Scope/Condition.php b/src/Model/Scope/Condition.php index b48a6936a..4e053e25f 100644 --- a/src/Model/Scope/Condition.php +++ b/src/Model/Scope/Condition.php @@ -178,7 +178,7 @@ protected function onChangeModel(): void } } - public function toQueryArguments($useFieldAlias = false): array + public function toQueryArguments(): array { if ($this->isEmpty()) { return []; @@ -220,7 +220,7 @@ public function toQueryArguments($useFieldAlias = false): array // handle the query arguments using field if ($field instanceof Field) { - [$field, $operator, $value] = $field->getQueryArguments($operator, $value, $useFieldAlias); + [$field, $operator, $value] = $field->getQueryArguments($operator, $value); } // only expression contained in $field From b75a23dbc24512a7b62f2a390fc4a673d6f59c12 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Sat, 26 Dec 2020 23:55:56 +0100 Subject: [PATCH 028/151] [update] to latest develop --- tests/Model/Invoice.php | 2 +- tests/Model/Payment.php | 2 +- tests/ModelAggregateTest.php | 36 ++++++++++++++++++------------------ tests/ModelUnionTest.php | 4 ++-- tests/ReportTest.php | 4 ++-- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/Model/Invoice.php b/tests/Model/Invoice.php index 3dd7133ab..f7ee71345 100644 --- a/tests/Model/Invoice.php +++ b/tests/Model/Invoice.php @@ -15,7 +15,7 @@ protected function init(): void parent::init(); $this->addField('name'); - $this->hasOne('client_id', [Client::class]); + $this->hasOne('client_id', ['model' => [Client::class]]); $this->addField('amount', ['type' => 'money']); } } diff --git a/tests/Model/Payment.php b/tests/Model/Payment.php index 18e32e224..be44a5dae 100644 --- a/tests/Model/Payment.php +++ b/tests/Model/Payment.php @@ -15,7 +15,7 @@ protected function init(): void parent::init(); $this->addField('name'); - $this->hasOne('client_id', [Client::class]); + $this->hasOne('client_id', ['model' => [Client::class]]); $this->addField('amount', ['type' => 'money']); } } diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 89e7837c9..6236437f5 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -58,8 +58,8 @@ public function testGroupBy() $this->assertSame( [ - ['client_id' => '1', 'c' => 2], - ['client_id' => '2', 'c' => 1], + ['client_id' => 1, 'c' => 2], + ['client_id' => 2, 'c' => 1], ], $invoiceAggregate->setOrder('client_id', 'asc')->export() ); @@ -73,8 +73,8 @@ public function testGroupSelect() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 'c' => 2], - ['client' => 'Zoe', 'client_id' => '2', 'c' => 1], + ['client' => 'Vinny', 'client_id' => 1, 'c' => 2], + ['client' => 'Zoe', 'client_id' => 2, 'c' => 1], ], $aggregate->setOrder('client_id', 'asc')->export() ); @@ -90,8 +90,8 @@ public function testGroupSelect2() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 'amount' => 19.0], - ['client' => 'Zoe', 'client_id' => '2', 'amount' => 4.0], + ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], ], $aggregate->setOrder('client_id', 'asc')->export() ); @@ -110,8 +110,8 @@ public function testGroupSelect3() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], - ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'min' => 4.0, 'max' => 4.0, 'amount' => 4.0], + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'min' => 4.0, 'max' => 4.0, 'amount' => 4.0], ], $aggregate->setOrder('client_id', 'asc')->export() ); @@ -130,8 +130,8 @@ public function testGroupSelectExpr() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], - ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ], $aggregate->setOrder('client_id', 'asc')->export() ); @@ -151,8 +151,8 @@ public function testGroupSelectCondition() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], - ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ['client' => 'Vinny', 'client_id' => 1, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ], $aggregate->setOrder('client_id', 'asc')->export() ); @@ -172,7 +172,7 @@ public function testGroupSelectCondition2() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], ], $aggregate->export() ); @@ -192,7 +192,7 @@ public function testGroupSelectCondition3() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], ], $aggregate->export() ); @@ -212,7 +212,7 @@ public function testGroupSelectCondition4() $this->assertSame( [ - ['client' => 'Zoe', 'client_id' => '2', 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ], $aggregate->export() ); @@ -232,7 +232,7 @@ public function testGroupSelectScope() var_dump($aggregate->action('select')->getDebugQuery()); $this->assertSame( [ - ['client' => 'Zoe', 'client_id' => '2', 'amount' => 4.0], + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], ], $aggregate->export() ); @@ -265,7 +265,7 @@ public function testGroupLimit() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 'amount' => 19.0], + ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], ], $aggregate->setOrder('client_id', 'asc')->export() ); @@ -282,7 +282,7 @@ public function testGroupLimit2() $this->assertSame( [ - ['client' => 'Zoe', 'client_id' => '2', 'amount' => 4.0], + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], ], $aggregate->setOrder('client_id', 'asc')->export() ); diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index cc2dc2007..36d3fc458 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -68,8 +68,8 @@ protected function createClient($persistence = null) { $client = new Model\Client($this->db); - $client->hasMany('Payment', [Model\Payment::class]); - $client->hasMany('Invoice', [Model\Invoice::class]); + $client->hasMany('Payment', ['model' => [Model\Payment::class]]); + $client->hasMany('Invoice', ['model' => [Model\Invoice::class]]); return $client; } diff --git a/tests/ReportTest.php b/tests/ReportTest.php index 68e85651d..a125a7de9 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -48,8 +48,8 @@ public function testAliasGroupSelect() $this->assertSame( [ - ['client' => 'Vinny', 'client_id' => '1', 'c' => 2], - ['client' => 'Zoe', 'client_id' => '2', 'c' => 1], + ['client' => 'Vinny', 'client_id' => 1, 'c' => 2], + ['client' => 'Zoe', 'client_id' => 2, 'c' => 1], ], $invoiceAggregate->setOrder('client_id', 'asc')->export() ); From 30ece17597e7063d8c38211d6366eaae31d6ba9c Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Sun, 27 Dec 2020 14:17:47 +0100 Subject: [PATCH 029/151] [fix] test --- tests/ModelAggregateTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 6236437f5..fe7228ed4 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -226,10 +226,10 @@ public function testGroupSelectScope() 'amount' => ['expr' => 'sum([])', 'type' => 'money'], ]); - $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', 4.0)); + $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', 4)); $aggregate->addCondition($scope); - var_dump($aggregate->action('select')->getDebugQuery()); + $this->assertSame( [ ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], From 8c0080ea03f20564291c65031397eb52c86b219a Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Sun, 27 Dec 2020 14:23:16 +0100 Subject: [PATCH 030/151] [fix] static analysis errors --- tests/ModelAggregateTest.php | 6 +++--- tests/ModelUnionTest.php | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index fe7228ed4..84e636363 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -4,7 +4,6 @@ namespace Atk4\Data\Tests; -use Atk4\Data\Model\Aggregate; use Atk4\Data\Model\Scope; use Atk4\Data\Model\Scope\Condition; @@ -28,9 +27,9 @@ class ModelAggregateTest extends \Atk4\Schema\PhpunitTestCase ], ]; - /** @var Model\Invoice */ + /** @var Model\Invoice|null */ protected $invoice; - /** @var Aggregate */ + /** @var \Atk4\Data\Model|null */ protected $invoiceAggregate; protected function setUp(): void @@ -139,6 +138,7 @@ public function testGroupSelectExpr() public function testGroupSelectCondition() { + /** @var \Atk4\Data\Model\Aggregate $aggregate */ $aggregate = clone $this->invoiceAggregate; $aggregate->baseModel->addCondition('name', 'chair purchase'); diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 36d3fc458..79c176b19 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -4,16 +4,15 @@ namespace Atk4\Data\Tests; -use Atk4\Dsql\Expression; use Doctrine\DBAL\Platforms\OraclePlatform; class ModelUnionTest extends \Atk4\Schema\PhpunitTestCase { - /** @var Model\Client */ + /** @var Model\Client|null */ protected $client; - /** @var Model\Transaction */ + /** @var Model\Transaction|null */ protected $transaction; - /** @var Model\Transaction */ + /** @var Model\Transaction|null */ protected $subtractInvoiceTransaction; /** @var array */ From 33c067431d73d13e82aea71bbaabb486af63b47a Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Sun, 27 Dec 2020 14:54:07 +0100 Subject: [PATCH 031/151] [fix] docs --- docs/aggregates.rst | 10 +++++----- docs/unions.rst | 13 +++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/aggregates.rst b/docs/aggregates.rst index 09a0fc4e4..705c24861 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -5,7 +5,7 @@ Model Aggregates ================ -.. php:namespace:: atk4\data\Model +.. php:namespace:: Atk4\Data\Model .. php:class:: Aggregate @@ -16,7 +16,7 @@ Grouping Aggregate model can be used for grouping:: - $orders->add(new \atk4\data\Model\Aggregate()); + $orders->add(new \Atk4\Data\Model\Aggregate()); $aggregate = $orders->action('group'); @@ -33,7 +33,7 @@ in various ways to fine-tune aggregation. Below is one sample use:: ], ); - foreach($aggregate as $row) { + foreach ($aggregate as $row) { var_dump(json_encode($row)); // ['country'=>'UK', 'count'=>20, 'total_amount'=>123.20]; // .. @@ -45,7 +45,7 @@ Below is how opening balance can be build:: $ledger->addCondition('date', '<', $from); // we actually need grouping by nominal - $ledger->add(new \atk4\data\Model\Aggregate()); + $ledger->add(new \Atk4\Data\Model\Aggregate()); $byNominal = $ledger->action('group', 'nominal_id'); $byNominal->addField('opening_balance', ['sum', 'amount']); - $byNominal->join() \ No newline at end of file + $byNominal->join(); \ No newline at end of file diff --git a/docs/unions.rst b/docs/unions.rst index ba37eea23..531d13d23 100644 --- a/docs/unions.rst +++ b/docs/unions.rst @@ -5,18 +5,19 @@ Model Unions ============ -.. php:namespace:: atk4\data\Model +.. php:namespace:: Atk4\Data\Model .. php:class:: Union In some cases data from multiple models need to be combined. In this case the Union model comes very handy. In the case used below Client model schema may have multiple invoices and multiple payments. Payment is not related to the invoice.:: - class Client extends \atk4\data\Model { + class Client extends \Atk4\Data\Model { public $table = 'client'; - function init() { - parent::init(); + protected function init(): void + { + parent::init(); $this->addField('name'); $this->hasMany('Payment'); @@ -24,7 +25,7 @@ In the case used below Client model schema may have multiple invoices and multip } } -(see tests/ModelUnionTest.php, tests/Client.php, tests/Payment.php and tests/Invoice.php files). +(see tests/ModelUnionTest.php, tests/Model/Client.php, tests/Model/Payment.php and tests/Model/Invoice.php files). Union Model Definition ---------------------- @@ -33,7 +34,7 @@ Normally a model is associated with a single table. Union model can have multipl results from that. As a result, Union model will have no "id" field. Below is an example of inline definition of Union model. The Union model can be separated in a designated class and nested model added within the init() method body of the new class:: - $unionPaymentInvoice = new \atk4\data\Model\Union(); + $unionPaymentInvoice = new \Atk4\Data\Model\Union(); $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment()); From 707c070382763821e50c8633cfeea581fa4dce5a Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 09:53:24 +0100 Subject: [PATCH 032/151] [update] reorder methods and adjust visibility --- src/Model/Aggregate.php | 57 +++---- src/Model/Union.php | 355 ++++++++++++++++++++-------------------- 2 files changed, 200 insertions(+), 212 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 59b3ea8b1..62a1f8003 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -187,35 +187,6 @@ public function addField(string $name, $seed = []): Field : parent::addField($name, $seed); } - /** - * Given a query, will add safe fields in. - */ - public function initQueryFields(Query $query, array $fields = []): Query - { - $this->persistence->initQueryFields($this, $query, $fields); - - return $query; - } - - /** - * Adds grouping in query. - */ - public function initQueryGrouping(Query $query) - { - // use table alias of base model - $this->table_alias = $this->baseModel->table_alias; - - foreach ($this->group as $field) { - if ($this->baseModel->hasField($field)) { - $expression = $this->baseModel->getField($field); - } else { - $expression = $this->expr($field); - } - - $query->group($expression); - } - } - public function setLimit(int $count = null, int $offset = 0) { $this->baseModel->setLimit($count, $offset); @@ -266,6 +237,13 @@ public function action($mode, $args = []) } } + protected function initQueryFields(Query $query, array $fields = []): Query + { + $this->persistence->initQueryFields($this, $query, $fields); + + return $query; + } + protected function initQueryOrder(Query $query) { if ($this->order) { @@ -285,10 +263,23 @@ protected function initQueryOrder(Query $query) } } - /** - * Our own way applying conditions, where we use "having" for fields. - */ - public function initQueryConditions(Query $query, Model\Scope\AbstractScope $condition = null): void + protected function initQueryGrouping(Query $query) + { + // use table alias of base model + $this->table_alias = $this->baseModel->table_alias; + + foreach ($this->group as $field) { + if ($this->baseModel->hasField($field)) { + $expression = $this->baseModel->getField($field); + } else { + $expression = $this->expr($field); + } + + $query->group($expression); + } + } + + protected function initQueryConditions(Query $query, Model\Scope\AbstractScope $condition = null): void { $condition = $condition ?? $this->scope(); diff --git a/src/Model/Union.php b/src/Model/Union.php index bef20a199..97a139d38 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -105,8 +105,181 @@ public function getFieldExpr(Model $model, string $fieldName, string $expr = nul } /** - * Configures nested models to have a specified set of fields - * available. + * Adds nested model in union. + */ + public function addNestedModel(Model $model, array $fieldMap = []): Model + { + $nestedModel = $this->persistence->add($model); + + $this->union[] = [$nestedModel, $fieldMap]; + + return $nestedModel; + } + + /** + * Specify a single field or array of fields. + * + * @param string|array $group + */ + public function groupBy($group, array $aggregate = []): Model + { + $this->aggregate = $aggregate; + $this->group = $group; + + foreach ($aggregate as $fieldName => $seed) { + $seed = (array) $seed; + + $field = $this->hasField($fieldName) ? $this->getField($fieldName) : null; + + // first element of seed should be expression itself + if (isset($seed[0]) && is_string($seed[0])) { + $seed[0] = $this->expr($seed[0], $field ? [$field] : null); + } + + if ($field) { + $this->removeField($fieldName); + } + + $this->addExpression($fieldName, $seed); + } + + foreach ($this->union as [$nestedModel, $fieldMap]) { + if ($nestedModel instanceof self) { + $nestedModel->aggregate = $aggregate; + $nestedModel->group = $group; + } + } + + return $this; + } + + /** + * If Union model has such field, then add condition to it. + * Otherwise adds condition to all nested models. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @param bool $forceNested Should we add condition to all nested models? + * + * @return $this + */ + public function addCondition($key, $operator = null, $value = null, $forceNested = false) + { + if (func_num_args() === 1) { + return parent::addCondition($key); + } + + // if Union model has such field, then add condition to it + if ($this->hasField($key) && !$forceNested) { + return parent::addCondition(...func_get_args()); + } + + // otherwise add condition in all nested models + foreach ($this->union as [$nestedModel, $fieldMap]) { + try { + $field = $key; + + if (isset($fieldMap[$key])) { + // field is included in mapping - use mapping expression + $field = $fieldMap[$key] instanceof Expression + ? $fieldMap[$key] + : $this->getFieldExpr($nestedModel, $key, $fieldMap[$key]); + } elseif (is_string($key) && $nestedModel->hasField($key)) { + // model has such field - use that field directly + $field = $nestedModel->getField($key); + } else { + // we don't know what to do, so let's do nothing + continue; + } + + switch (func_num_args()) { + case 2: + $nestedModel->addCondition($field, $operator); + + break; + default: + $nestedModel->addCondition($field, $operator, $value); + + break; + } + } catch (\Atk4\Core\Exception $e) { + throw $e->addMoreInfo('nestedModel', get_class($nestedModel)); + } + } + + return $this; + } + + /** + * Execute action. + * + * @param string $mode + * @param array $args + * + * @return Query + */ + public function action($mode, $args = []) + { + $subquery = null; + switch ($mode) { + case 'select': + // get list of available fields + $fields = $this->only_fields ?: array_keys($this->getFields()); + foreach ($fields as $k => $field) { + if ($this->getField($field)->never_persist) { + unset($fields[$k]); + } + } + $subquery = $this->getSubQuery($fields); + $query = parent::action($mode, $args)->reset('table')->table($subquery); + + if (isset($this->group)) { + $query->group($this->group); + } + $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); + + return $query; + case 'count': + $subquery = $this->getSubAction('count', ['alias' => 'cnt']); + + $mode = 'fx'; + $args = ['sum', $this->expr('{}', ['cnt'])]; + + break; + case 'field': + if (!isset($args[0])) { + throw (new Exception('This action requires one argument with field name')) + ->addMoreInfo('mode', $mode); + } + + if (!is_string($args[0])) { + throw (new Exception('action "field" only support string fields')) + ->addMoreInfo('field', $args[0]); + } + + $subquery = $this->getSubQuery([$args[0]]); + + break; + case 'fx': + $args['alias'] = 'val'; + + $subquery = $this->getSubAction('fx', $args); + + $args = [$args[0], $this->expr('{}', ['val'])]; + + break; + default: + throw (new Exception('Union model does not support this action')) + ->addMoreInfo('mode', $mode); + } + + // Substitute FROM table with our subquery expression + return parent::action($mode, $args)->reset('table')->table($subquery); + } + + /** + * Configures nested models to have a specified set of fields available. */ public function getSubQuery(array $fields): Expression { @@ -222,7 +395,7 @@ public function getSubAction(string $action, array $actionArgs = []): Expression $model, $fieldName, $fieldMap[$fieldName] ?? null - ); + ); } $query = $model->action($action, $modelActionArgs); @@ -240,182 +413,6 @@ public function getSubAction(string $action, array $actionArgs = []): Expression return $this->persistence->dsql()->expr($expr, $exprArgs); } - /** - * Execute action. - * - * @param string $mode - * @param array $args - * - * @return Query - */ - public function action($mode, $args = []) - { - $subquery = null; - switch ($mode) { - case 'select': - // get list of available fields - $fields = $this->only_fields ?: array_keys($this->getFields()); - foreach ($fields as $k => $field) { - if ($this->getField($field)->never_persist) { - unset($fields[$k]); - } - } - $subquery = $this->getSubQuery($fields); - $query = parent::action($mode, $args)->reset('table')->table($subquery); - - if (isset($this->group)) { - $query->group($this->group); - } - $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); - - return $query; - case 'count': - $subquery = $this->getSubAction('count', ['alias' => 'cnt']); - - $mode = 'fx'; - $args = ['sum', $this->expr('{}', ['cnt'])]; - - break; - case 'field': - if (!isset($args[0])) { - throw (new Exception('This action requires one argument with field name')) - ->addMoreInfo('mode', $mode); - } - - if (!is_string($args[0])) { - throw (new Exception('action "field" only support string fields')) - ->addMoreInfo('field', $args[0]); - } - - $subquery = $this->getSubQuery([$args[0]]); - - break; - case 'fx': - $args['alias'] = 'val'; - - $subquery = $this->getSubAction('fx', $args); - - $args = [$args[0], $this->expr('{}', ['val'])]; - - break; - default: - throw (new Exception('Union model does not support this action')) - ->addMoreInfo('mode', $mode); - } - - // Substitute FROM table with our subquery expression - return parent::action($mode, $args)->reset('table')->table($subquery); - } - - /** - * Adds nested model in union. - */ - public function addNestedModel(Model $model, array $fieldMap = []): Model - { - $nestedModel = $this->persistence->add($model); - - $this->union[] = [$nestedModel, $fieldMap]; - - return $nestedModel; - } - - /** - * Specify a single field or array of fields. - * - * @param string|array $group - */ - public function groupBy($group, array $aggregate = []): Model - { - $this->aggregate = $aggregate; - $this->group = $group; - - foreach ($aggregate as $fieldName => $seed) { - $seed = (array) $seed; - - $field = $this->hasField($fieldName) ? $this->getField($fieldName) : null; - - // first element of seed should be expression itself - if (isset($seed[0]) && is_string($seed[0])) { - $seed[0] = $this->expr($seed[0], $field ? [$field] : null); - } - - if ($field) { - $this->removeField($fieldName); - } - - $this->addExpression($fieldName, $seed); - } - - foreach ($this->union as [$nestedModel, $fieldMap]) { - if ($nestedModel instanceof self) { - $nestedModel->aggregate = $aggregate; - $nestedModel->group = $group; - } - } - - return $this; - } - - /** - * Adds condition. - * - * If Union model has such field, then add condition to it. - * Otherwise adds condition to all nested models. - * - * @param mixed $key - * @param mixed $operator - * @param mixed $value - * @param bool $forceNested Should we add condition to all nested models? - * - * @return $this - */ - public function addCondition($key, $operator = null, $value = null, $forceNested = false) - { - if (func_num_args() === 1) { - return parent::addCondition($key); - } - - // if Union model has such field, then add condition to it - if ($this->hasField($key) && !$forceNested) { - return parent::addCondition(...func_get_args()); - } - - // otherwise add condition in all nested models - foreach ($this->union as [$nestedModel, $fieldMap]) { - try { - $field = $key; - - if (isset($fieldMap[$key])) { - // field is included in mapping - use mapping expression - $field = $fieldMap[$key] instanceof Expression - ? $fieldMap[$key] - : $this->getFieldExpr($nestedModel, $key, $fieldMap[$key]); - } elseif (is_string($key) && $nestedModel->hasField($key)) { - // model has such field - use that field directly - $field = $nestedModel->getField($key); - } else { - // we don't know what to do, so let's do nothing - continue; - } - - switch (func_num_args()) { - case 2: - $nestedModel->addCondition($field, $operator); - - break; - default: - $nestedModel->addCondition($field, $operator, $value); - - break; - } - } catch (\Atk4\Core\Exception $e) { - throw $e->addMoreInfo('nestedModel', get_class($nestedModel)); - } - } - - return $this; - } - // {{{ Debug Methods /** From 690e1ca6c936cc82495ac958b11355e0994214d9 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 12:30:22 +0100 Subject: [PATCH 033/151] [feature] introduce Model\Aggregate --- docs/index.rst | 1 + src/Model.php | 1 + src/Model/Aggregate.php | 316 +++++++++++++++++++++++++++++++++ src/Model/AggregatesTrait.php | 29 ++++ tests/Model/Client.php | 2 + tests/Model/Invoice.php | 21 +++ tests/ModelAggregateTest.php | 318 ++++++++++++++++++++++++++++++++++ 7 files changed, 688 insertions(+) create mode 100644 src/Model/Aggregate.php create mode 100644 src/Model/AggregatesTrait.php create mode 100644 tests/Model/Invoice.php create mode 100644 tests/ModelAggregateTest.php diff --git a/docs/index.rst b/docs/index.rst index e17fcc5f5..7562c9871 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,7 @@ Contents: references expressions joins + aggregates hooks deriving advanced diff --git a/src/Model.php b/src/Model.php index 28bebbf05..ffe241f12 100644 --- a/src/Model.php +++ b/src/Model.php @@ -37,6 +37,7 @@ class Model implements \IteratorAggregate use InitializerTrait { init as _init; } + use Model\AggregatesTrait; use Model\JoinsTrait; use Model\ReferencesTrait; use Model\UserActionsTrait; diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php new file mode 100644 index 000000000..fe20c5145 --- /dev/null +++ b/src/Model/Aggregate.php @@ -0,0 +1,316 @@ +groupBy(['first','last'], ['salary'=>'sum([])']; + * + * your resulting model will have 3 fields: + * first, last, salary + * + * but when querying it will use the original model to calculate the query, then add grouping and aggregates. + * + * If you wish you can add more fields, which will be passed through: + * $aggregate->addField('middle'); + * + * If this field exist in the original model it will be added and you'll get exception otherwise. Finally you are + * permitted to add expressions. + * + * The base model must not be Union model or another Aggregate model, however it's possible to use Aggregate model as nestedModel inside Union model. + * Union model implements identical grouping rule on its own. + * + * You can also pass seed (for example field type) when aggregating: + * $aggregate->groupBy(['first','last'], ['salary' => ['sum([])', 'type'=>'money']]; + * + * @property \Atk4\Data\Persistence\Sql $persistence + * + * @method Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model + */ +class Aggregate extends Model +{ + /** @const string */ + public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; + + /** @var array */ + protected $systemFields = []; + + /** @var Model */ + public $baseModel; + + /** + * Aggregate model should always be read-only. + * + * @var bool + */ + public $read_only = true; + + /** + * Aggregate does not have ID field. + * + * @var string + */ + public $id_field; + + /** @var array */ + public $group = []; + + /** @var array */ + public $aggregate = []; + + /** + * Constructor. + */ + public function __construct(Model $baseModel, array $defaults = []) + { + if (!$baseModel->persistence instanceof Persistence\Sql) { + throw new Exception('Base model must have Sql persistence to use grouping'); + } + + $this->baseModel = clone $baseModel; + $this->table = $baseModel->table; + + parent::__construct($baseModel->persistence, $defaults); + + // always use table prefixes for this model + $this->persistence_data['use_table_prefixes'] = true; + } + + /** + * Specify a single field or array of fields on which we will group model. + * + * @param array $fields Array of field names + * @param array $aggregate Array of aggregate mapping + * + * @return $this + */ + public function groupBy(array $fields, array $aggregate = []): Model + { + $this->group = $fields; + $this->aggregate = $aggregate; + + $this->systemFields = array_unique($this->systemFields + $fields); + foreach ($fields as $fieldName) { + $this->addField($fieldName); + } + + foreach ($aggregate as $fieldName => $expr) { + $seed = is_array($expr) ? $expr : [$expr]; + + $args = []; + // if field originally defined in the parent model, then it can be used as part of expression + if ($this->baseModel->hasField($fieldName)) { + $args = [$this->baseModel->getField($fieldName)]; + } + + $seed['expr'] = $this->baseModel->expr($seed[0] ?? $seed['expr'], $args); + + // now add the expressions here + $this->addExpression($fieldName, $seed); + } + + return $this; + } + + /** + * Return reference field. + * + * @param string $link + */ + public function getRef($link): Reference + { + return $this->baseModel->getRef($link); + } + + /** + * Method to enable commutative usage of methods enabling both of below + * Resulting in Aggregate on $model. + * + * $model->groupBy(['abc'])->withAggregateField('xyz'); + * + * and + * + * $model->withAggregateField('xyz')->groupBy(['abc']); + */ + public function withAggregateField($name, $seed = []): Model + { + static::addField(...func_get_args()); + + return $this; + } + + /** + * Adds new field into model. + * + * @param array|object $seed + */ + public function addField(string $name, $seed = []): Field + { + $seed = is_array($seed) ? $seed : [$seed]; + + if (isset($seed[0]) && $seed[0] instanceof FieldSqlExpression) { + return parent::addField($name, $seed[0]); + } + + if ($seed['never_persist'] ?? false) { + return parent::addField($name, $seed); + } + + if ($this->baseModel->hasField($name)) { + $field = clone $this->baseModel->getField($name); + $field->unsetOwner(); // will be new owner + } else { + $field = null; + } + + return $field + ? parent::addField($name, $field)->setDefaults($seed) + : parent::addField($name, $seed); + } + + public function setLimit(int $count = null, int $offset = 0) + { + $this->baseModel->setLimit($count, $offset); + + return $this; + } + + /** + * Execute action. + * + * @param string $mode + * @param array $args + * + * @return Query + */ + public function action($mode, $args = []) + { + switch ($mode) { + case 'select': + $fields = $this->only_fields ?: array_keys($this->getFields()); + + // select but no need your fields + $query = $this->baseModel->action($mode, [false]); + + $this->initQueryFields($query, array_unique($fields + $this->systemFields)); + $this->initQueryOrder($query); + $this->initQueryGrouping($query); + $this->initQueryConditions($query); + + $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); + + return $query; + case 'count': + $query = $this->baseModel->action($mode, $args); + + $query->reset('field')->field($this->expr('1')); + $this->initQueryGrouping($query); + + $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); + + return $query->dsql()->field('count(*)')->table($this->expr('([]) der', [$query])); + case 'field': + case 'fx': + return parent::action($mode, $args); + default: + throw (new Exception('Aggregate model does not support this action')) + ->addMoreInfo('mode', $mode); + } + } + + protected function initQueryFields(Query $query, array $fields = []): void + { + $this->persistence->initQueryFields($this, $query, $fields); + } + + protected function initQueryOrder(Query $query): void + { + if ($this->order) { + foreach ($this->order as $order) { + $isDesc = strtolower($order[1]) === 'desc'; + + if ($order[0] instanceof Expression) { + $query->order($order[0], $isDesc); + } elseif (is_string($order[0])) { + $query->order($this->getField($order[0]), $isDesc); + } else { + throw (new Exception('Unsupported order parameter')) + ->addMoreInfo('model', $this) + ->addMoreInfo('field', $order[0]); + } + } + } + } + + protected function initQueryGrouping(Query $query): void + { + // use table alias of base model + $this->table_alias = $this->baseModel->table_alias; + + foreach ($this->group as $field) { + if ($this->baseModel->hasField($field)) { + $expression = $this->baseModel->getField($field); + } else { + $expression = $this->expr($field); + } + + $query->group($expression); + } + } + + protected function initQueryConditions(Query $query, Model\Scope\AbstractScope $condition = null): void + { + $condition = $condition ?? $this->scope(); + + if (!$condition->isEmpty()) { + // peel off the single nested scopes to convert (((field = value))) to field = value + $condition = $condition->simplify(); + + // simple condition + if ($condition instanceof Model\Scope\Condition) { + $query->having(...$condition->toQueryArguments()); + } + + // nested conditions + if ($condition instanceof Model\Scope) { + $expression = $condition->isOr() ? $query->orExpr() : $query->andExpr(); + + foreach ($condition->getNestedConditions() as $nestedCondition) { + $this->initQueryConditions($expression, $nestedCondition); + } + + $query->having($expression); + } + } + } + + // {{{ Debug Methods + + /** + * Returns array with useful debug info for var_dump. + */ + public function __debugInfo(): array + { + return array_merge(parent::__debugInfo(), [ + 'group' => $this->group, + 'aggregate' => $this->aggregate, + 'baseModel' => $this->baseModel->__debugInfo(), + ]); + } + + // }}} +} diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php new file mode 100644 index 000000000..46e2b9cf4 --- /dev/null +++ b/src/Model/AggregatesTrait.php @@ -0,0 +1,29 @@ +withAggregateField(...func_get_args()); + } + + /** + * @see Aggregate::groupBy. + */ + public function groupBy(array $group, array $aggregate = []): Model + { + return (new Aggregate($this))->groupBy(...func_get_args()); + } +} diff --git a/tests/Model/Client.php b/tests/Model/Client.php index d5dd6d1a5..02ec7d380 100644 --- a/tests/Model/Client.php +++ b/tests/Model/Client.php @@ -6,6 +6,8 @@ class Client extends User { + public $table = 'client'; + protected function init(): void { parent::init(); diff --git a/tests/Model/Invoice.php b/tests/Model/Invoice.php new file mode 100644 index 000000000..f7ee71345 --- /dev/null +++ b/tests/Model/Invoice.php @@ -0,0 +1,21 @@ +addField('name'); + + $this->hasOne('client_id', ['model' => [Client::class]]); + $this->addField('amount', ['type' => 'money']); + } +} diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php new file mode 100644 index 000000000..84e636363 --- /dev/null +++ b/tests/ModelAggregateTest.php @@ -0,0 +1,318 @@ + [ + ['name' => 'Vinny'], + ['name' => 'Zoe'], + ], + 'invoice' => [ + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], + ], + 'payment' => [ + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], + ], + ]; + + /** @var Model\Invoice|null */ + protected $invoice; + /** @var \Atk4\Data\Model|null */ + protected $invoiceAggregate; + + protected function setUp(): void + { + parent::setUp(); + $this->setDB($this->init_db); + + $this->invoice = new Model\Invoice($this->db); + $this->invoice->getRef('client_id')->addTitle(); + + $this->invoiceAggregate = $this->invoice->withAggregateField('client'); + } + + protected function tearDown(): void + { + $this->invoice = null; + $this->invoiceAggregate = null; + + parent::tearDown(); + } + + public function testGroupBy() + { + $invoiceAggregate = $this->invoice->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); + + $this->assertSame( + [ + ['client_id' => 1, 'c' => 2], + ['client_id' => 2, 'c' => 1], + ], + $invoiceAggregate->setOrder('client_id', 'asc')->export() + ); + } + + public function testGroupSelect() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => 1, 'c' => 2], + ['client' => 'Zoe', 'client_id' => 2, 'c' => 1], + ], + $aggregate->setOrder('client_id', 'asc')->export() + ); + } + + public function testGroupSelect2() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], + ], + $aggregate->setOrder('client_id', 'asc')->export() + ); + } + + public function testGroupSelect3() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'min' => ['expr' => 'min([amount])', 'type' => 'money'], + 'max' => ['expr' => 'max([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], // same as `s`, but reuse name `amount` + ]); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'min' => 4.0, 'max' => 4.0, 'amount' => 4.0], + ], + $aggregate->setOrder('client_id', 'asc')->export() + ); + } + + public function testGroupSelectExpr() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], + $aggregate->setOrder('client_id', 'asc')->export() + ); + } + + public function testGroupSelectCondition() + { + /** @var \Atk4\Data\Model\Aggregate $aggregate */ + $aggregate = clone $this->invoiceAggregate; + $aggregate->baseModel->addCondition('name', 'chair purchase'); + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => 1, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], + $aggregate->setOrder('client_id', 'asc')->export() + ); + } + + public function testGroupSelectCondition2() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addCondition('double', '>', 10); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelectCondition3() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addCondition('double', 38); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelectCondition4() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 's' => ['expr' => 'sum([amount])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addCondition('client_id', 2); + + $this->assertSame( + [ + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], + $aggregate->export() + ); + } + + public function testGroupSelectScope() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', 4)); + + $aggregate->addCondition($scope); + + $this->assertSame( + [ + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], + ], + $aggregate->export() + ); + } + + public function testGroupOrder() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $aggregate->setOrder('client_id', 'asc'); + + $this->assertSameSql( + 'select (select "name" from "client" "c" where "id" = "invoice"."client_id") "client","invoice"."client_id",sum("invoice"."amount") "amount" from "invoice" group by "invoice"."client_id" order by "invoice"."client_id"', + $aggregate->action('select')->render() + ); + } + + public function testGroupLimit() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + $aggregate->setLimit(1); + + $this->assertSame( + [ + ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], + ], + $aggregate->setOrder('client_id', 'asc')->export() + ); + } + + public function testGroupLimit2() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + $aggregate->setLimit(1, 1); + + $this->assertSame( + [ + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], + ], + $aggregate->setOrder('client_id', 'asc')->export() + ); + } + + public function testGroupCount() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['client_id'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + ]); + + $this->assertSameSql( + 'select count(*) from ((select 1 from "invoice" group by "client_id")) der', + $aggregate->action('count')->render() + ); + } + + public function testAggregateFieldExpression() + { + $aggregate = clone $this->invoiceAggregate; + + $aggregate->groupBy(['abc'], [ + 'xyz' => ['expr' => 'sum([amount])'], + ]); + + $this->assertSameSql( + 'select (select "name" from "client" "c" where "id" = "invoice"."client_id") "client","invoice"."abc",sum("invoice"."amount") "xyz" from "invoice" group by abc', + $aggregate->action('select')->render() + ); + } +} From 2ee790bca48997e08459325a00106d5cf3cbcb02 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 12:56:12 +0100 Subject: [PATCH 034/151] [update] avoid modifying existing model Client --- tests/Model/Client.php | 2 -- tests/Model/Invoice.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Model/Client.php b/tests/Model/Client.php index 02ec7d380..d5dd6d1a5 100644 --- a/tests/Model/Client.php +++ b/tests/Model/Client.php @@ -6,8 +6,6 @@ class Client extends User { - public $table = 'client'; - protected function init(): void { parent::init(); diff --git a/tests/Model/Invoice.php b/tests/Model/Invoice.php index f7ee71345..cd1e85000 100644 --- a/tests/Model/Invoice.php +++ b/tests/Model/Invoice.php @@ -15,7 +15,7 @@ protected function init(): void parent::init(); $this->addField('name'); - $this->hasOne('client_id', ['model' => [Client::class]]); + $this->hasOne('client_id', ['model' => [Client::class, 'table' => 'client']]); $this->addField('amount', ['type' => 'money']); } } From 4a9f37cc54c4b67d1742f4453f1e0e4576ae08b7 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 13:58:15 +0100 Subject: [PATCH 035/151] [update] move setup of properties to constructor --- src/Model/Aggregate.php | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index fe20c5145..a460bf98f 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -52,29 +52,12 @@ class Aggregate extends Model /** @var Model */ public $baseModel; - /** - * Aggregate model should always be read-only. - * - * @var bool - */ - public $read_only = true; - - /** - * Aggregate does not have ID field. - * - * @var string - */ - public $id_field; - /** @var array */ public $group = []; /** @var array */ public $aggregate = []; - /** - * Constructor. - */ public function __construct(Model $baseModel, array $defaults = []) { if (!$baseModel->persistence instanceof Persistence\Sql) { @@ -84,6 +67,12 @@ public function __construct(Model $baseModel, array $defaults = []) $this->baseModel = clone $baseModel; $this->table = $baseModel->table; + // this model does not have ID field + $this->id_field = null; + + // this model should always be read-only + $this->read_only = true; + parent::__construct($baseModel->persistence, $defaults); // always use table prefixes for this model From d566c62391585d928dab1afeaf95182925154979 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 13:58:34 +0100 Subject: [PATCH 036/151] [update] add docs --- docs/aggregates.rst | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/aggregates.rst diff --git a/docs/aggregates.rst b/docs/aggregates.rst new file mode 100644 index 000000000..705c24861 --- /dev/null +++ b/docs/aggregates.rst @@ -0,0 +1,51 @@ + +.. _Aggregates: + +================ +Model Aggregates +================ + +.. php:namespace:: Atk4\Data\Model + +.. php:class:: Aggregate + +In order to create model aggregates the Aggregate model needs to be used: + +Grouping +-------- + +Aggregate model can be used for grouping:: + + $orders->add(new \Atk4\Data\Model\Aggregate()); + + $aggregate = $orders->action('group'); + +`$aggregate` above will return a new object that is most appropriate for the model persistence and which can be manipulated +in various ways to fine-tune aggregation. Below is one sample use:: + + $aggregate = $orders->action( + 'group', + 'country_id', + [ + 'country', + 'count'=>'count', + 'total_amount'=>['sum', 'amount'] + ], + ); + + foreach ($aggregate as $row) { + var_dump(json_encode($row)); + // ['country'=>'UK', 'count'=>20, 'total_amount'=>123.20]; + // .. + } + +Below is how opening balance can be build:: + + $ledger = new GeneralLedger($db); + $ledger->addCondition('date', '<', $from); + + // we actually need grouping by nominal + $ledger->add(new \Atk4\Data\Model\Aggregate()); + $byNominal = $ledger->action('group', 'nominal_id'); + $byNominal->addField('opening_balance', ['sum', 'amount']); + $byNominal->join(); \ No newline at end of file From de06f0343b01f3a1cf6e1711dc053b02dd0b1473 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 14:00:30 +0100 Subject: [PATCH 037/151] [update] use strong argument typing --- src/Model/Aggregate.php | 4 ++-- src/Model/AggregatesTrait.php | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index a460bf98f..b3dee15ac 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -135,9 +135,9 @@ public function getRef($link): Reference * * $model->withAggregateField('xyz')->groupBy(['abc']); */ - public function withAggregateField($name, $seed = []): Model + public function withAggregateField(string $name, $seed = []): Model { - static::addField(...func_get_args()); + static::addField($name, $seed); return $this; } diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php index 46e2b9cf4..cf891ac6c 100644 --- a/src/Model/AggregatesTrait.php +++ b/src/Model/AggregatesTrait.php @@ -12,11 +12,13 @@ trait AggregatesTrait { /** + * @param array|object $seed + * * @see Aggregate::withAggregateField. */ - public function withAggregateField($name, $seed = []): Model + public function withAggregateField(string $name, $seed = []): Model { - return (new Aggregate($this))->withAggregateField(...func_get_args()); + return (new Aggregate($this))->withAggregateField($name, $seed); } /** From ae45471633b0fa355ebc42037c0cddd0f057aead Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 14:01:47 +0100 Subject: [PATCH 038/151] [update] rename properties to intuitive names --- src/Model/Aggregate.php | 37 +++++++++++++++-------------------- src/Model/AggregatesTrait.php | 4 ++-- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index b3dee15ac..a6e63922c 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -46,17 +46,14 @@ class Aggregate extends Model /** @const string */ public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; - /** @var array */ - protected $systemFields = []; - /** @var Model */ public $baseModel; /** @var array */ - public $group = []; + public $groupByFields = []; /** @var array */ - public $aggregate = []; + public $aggregateExpressions = []; public function __construct(Model $baseModel, array $defaults = []) { @@ -82,34 +79,34 @@ public function __construct(Model $baseModel, array $defaults = []) /** * Specify a single field or array of fields on which we will group model. * - * @param array $fields Array of field names - * @param array $aggregate Array of aggregate mapping + * @param array $fields Array of field names + * @param array $aggregateExpressions Array of aggregate expressions with alias as key * * @return $this */ - public function groupBy(array $fields, array $aggregate = []): Model + public function groupBy(array $fields, array $aggregateExpressions = []): Model { - $this->group = $fields; - $this->aggregate = $aggregate; + $this->groupByFields = array_unique($this->groupByFields + $fields); - $this->systemFields = array_unique($this->systemFields + $fields); foreach ($fields as $fieldName) { $this->addField($fieldName); } - foreach ($aggregate as $fieldName => $expr) { + foreach ($aggregateExpressions as $name => $expr) { + $this->aggregateExpressions[$name] = $expr; + $seed = is_array($expr) ? $expr : [$expr]; $args = []; // if field originally defined in the parent model, then it can be used as part of expression - if ($this->baseModel->hasField($fieldName)) { - $args = [$this->baseModel->getField($fieldName)]; + if ($this->baseModel->hasField($name)) { + $args = [$this->baseModel->getField($name)]; } $seed['expr'] = $this->baseModel->expr($seed[0] ?? $seed['expr'], $args); // now add the expressions here - $this->addExpression($fieldName, $seed); + $this->addExpression($name, $seed); } return $this; @@ -179,8 +176,6 @@ public function setLimit(int $count = null, int $offset = 0) } /** - * Execute action. - * * @param string $mode * @param array $args * @@ -195,7 +190,7 @@ public function action($mode, $args = []) // select but no need your fields $query = $this->baseModel->action($mode, [false]); - $this->initQueryFields($query, array_unique($fields + $this->systemFields)); + $this->initQueryFields($query, array_unique($fields + $this->groupByFields)); $this->initQueryOrder($query); $this->initQueryGrouping($query); $this->initQueryConditions($query); @@ -250,7 +245,7 @@ protected function initQueryGrouping(Query $query): void // use table alias of base model $this->table_alias = $this->baseModel->table_alias; - foreach ($this->group as $field) { + foreach ($this->groupByFields as $field) { if ($this->baseModel->hasField($field)) { $expression = $this->baseModel->getField($field); } else { @@ -295,8 +290,8 @@ protected function initQueryConditions(Query $query, Model\Scope\AbstractScope $ public function __debugInfo(): array { return array_merge(parent::__debugInfo(), [ - 'group' => $this->group, - 'aggregate' => $this->aggregate, + 'groupByFields' => $this->groupByFields, + 'aggregateExpressions' => $this->aggregateExpressions, 'baseModel' => $this->baseModel->__debugInfo(), ]); } diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php index cf891ac6c..b816f1d8e 100644 --- a/src/Model/AggregatesTrait.php +++ b/src/Model/AggregatesTrait.php @@ -24,8 +24,8 @@ public function withAggregateField(string $name, $seed = []): Model /** * @see Aggregate::groupBy. */ - public function groupBy(array $group, array $aggregate = []): Model + public function groupBy(array $fields, array $aggregateExpressions = []): Model { - return (new Aggregate($this))->groupBy(...func_get_args()); + return (new Aggregate($this))->groupBy($fields, $aggregateExpressions); } } From 3dd1ab08da25106b92349be2b0b93fe98a76df6f Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 14:05:08 +0100 Subject: [PATCH 039/151] [update] phpdoc types --- src/Model/Aggregate.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index a6e63922c..b9203b606 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -49,10 +49,10 @@ class Aggregate extends Model /** @var Model */ public $baseModel; - /** @var array */ + /** @var string[] */ public $groupByFields = []; - /** @var array */ + /** @var mixed[] */ public $aggregateExpressions = []; public function __construct(Model $baseModel, array $defaults = []) @@ -79,8 +79,7 @@ public function __construct(Model $baseModel, array $defaults = []) /** * Specify a single field or array of fields on which we will group model. * - * @param array $fields Array of field names - * @param array $aggregateExpressions Array of aggregate expressions with alias as key + * @param mixed[] $aggregateExpressions Array of aggregate expressions with alias as key * * @return $this */ From 5cb2bf826a3702e488fa7b2c6023fd95b46bc5c2 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 14:11:27 +0100 Subject: [PATCH 040/151] [update] allow to unset Model::$id_field --- src/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model.php b/src/Model.php index ffe241f12..0ef82302b 100644 --- a/src/Model.php +++ b/src/Model.php @@ -224,7 +224,7 @@ class Model implements \IteratorAggregate * you would want to use a different one or maybe don't create field * at all. * - * @var string + * @var string|null */ public $id_field = 'id'; From 68d32d36f45cadd89cc5406e23e2c296481d46cb Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 14:47:51 +0100 Subject: [PATCH 041/151] [update] use array_merge --- src/Model/Aggregate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index b9203b606..620fb5c1e 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -85,7 +85,7 @@ public function __construct(Model $baseModel, array $defaults = []) */ public function groupBy(array $fields, array $aggregateExpressions = []): Model { - $this->groupByFields = array_unique($this->groupByFields + $fields); + $this->groupByFields = array_unique(array_merge($this->groupByFields, $fields)); foreach ($fields as $fieldName) { $this->addField($fieldName); From 8c69320b5a1e8f199b3c492202033364c870f033 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 14:48:27 +0100 Subject: [PATCH 042/151] [update] arguments and return types --- src/Model/Aggregate.php | 9 ++------- src/Model/AggregatesTrait.php | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 620fb5c1e..3262980b0 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -83,7 +83,7 @@ public function __construct(Model $baseModel, array $defaults = []) * * @return $this */ - public function groupBy(array $fields, array $aggregateExpressions = []): Model + public function groupBy(array $fields, array $aggregateExpressions = []) { $this->groupByFields = array_unique(array_merge($this->groupByFields, $fields)); @@ -111,12 +111,7 @@ public function groupBy(array $fields, array $aggregateExpressions = []): Model return $this; } - /** - * Return reference field. - * - * @param string $link - */ - public function getRef($link): Reference + public function getRef(string $link): Reference { return $this->baseModel->getRef($link); } diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php index b816f1d8e..d5612ea7b 100644 --- a/src/Model/AggregatesTrait.php +++ b/src/Model/AggregatesTrait.php @@ -24,7 +24,7 @@ public function withAggregateField(string $name, $seed = []): Model /** * @see Aggregate::groupBy. */ - public function groupBy(array $fields, array $aggregateExpressions = []): Model + public function groupBy(array $fields, array $aggregateExpressions = []) { return (new Aggregate($this))->groupBy($fields, $aggregateExpressions); } From 47fc81dea35a559cc8a288410567ebf5411b2217 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 14:48:51 +0100 Subject: [PATCH 043/151] [update] tests --- tests/ModelAggregateTest.php | 53 ++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 84e636363..ecccba87d 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -4,6 +4,7 @@ namespace Atk4\Data\Tests; +use Atk4\Data\Model\Aggregate; use Atk4\Data\Model\Scope; use Atk4\Data\Model\Scope\Condition; @@ -27,33 +28,28 @@ class ModelAggregateTest extends \Atk4\Schema\PhpunitTestCase ], ]; - /** @var Model\Invoice|null */ - protected $invoice; - /** @var \Atk4\Data\Model|null */ - protected $invoiceAggregate; - protected function setUp(): void { parent::setUp(); $this->setDB($this->init_db); + } - $this->invoice = new Model\Invoice($this->db); - $this->invoice->getRef('client_id')->addTitle(); + protected function createInvoice() + { + $invoice = new Model\Invoice($this->db); + $invoice->getRef('client_id')->addTitle(); - $this->invoiceAggregate = $this->invoice->withAggregateField('client'); + return $invoice; } - protected function tearDown(): void + protected function createInvoiceAggregate(): Aggregate { - $this->invoice = null; - $this->invoiceAggregate = null; - - parent::tearDown(); + return $this->createInvoice()->withAggregateField('client'); } public function testGroupBy() { - $invoiceAggregate = $this->invoice->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); + $invoiceAggregate = $this->createInvoice()->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); $this->assertSame( [ @@ -66,7 +62,7 @@ public function testGroupBy() public function testGroupSelect() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); @@ -81,7 +77,7 @@ public function testGroupSelect() public function testGroupSelect2() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -98,7 +94,7 @@ public function testGroupSelect2() public function testGroupSelect3() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -118,7 +114,7 @@ public function testGroupSelect3() public function testGroupSelectExpr() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -138,8 +134,7 @@ public function testGroupSelectExpr() public function testGroupSelectCondition() { - /** @var \Atk4\Data\Model\Aggregate $aggregate */ - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->baseModel->addCondition('name', 'chair purchase'); $aggregate->groupBy(['client_id'], [ @@ -160,7 +155,7 @@ public function testGroupSelectCondition() public function testGroupSelectCondition2() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -180,7 +175,7 @@ public function testGroupSelectCondition2() public function testGroupSelectCondition3() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -200,7 +195,7 @@ public function testGroupSelectCondition3() public function testGroupSelectCondition4() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'money'], @@ -220,7 +215,7 @@ public function testGroupSelectCondition4() public function testGroupSelectScope() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -240,7 +235,7 @@ public function testGroupSelectScope() public function testGroupOrder() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -256,7 +251,7 @@ public function testGroupOrder() public function testGroupLimit() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -273,7 +268,7 @@ public function testGroupLimit() public function testGroupLimit2() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -290,7 +285,7 @@ public function testGroupLimit2() public function testGroupCount() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'money'], @@ -304,7 +299,7 @@ public function testGroupCount() public function testAggregateFieldExpression() { - $aggregate = clone $this->invoiceAggregate; + $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['abc'], [ 'xyz' => ['expr' => 'sum([amount])'], From adb57dc88b86b163e753e28f8993af7062f44938 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 15:05:23 +0100 Subject: [PATCH 044/151] [update] docs --- docs/aggregates.rst | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/docs/aggregates.rst b/docs/aggregates.rst index 705c24861..d959fdd25 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -16,28 +16,22 @@ Grouping Aggregate model can be used for grouping:: - $orders->add(new \Atk4\Data\Model\Aggregate()); + $aggregate = $orders->groupBy(['country_id']); - $aggregate = $orders->action('group'); - -`$aggregate` above will return a new object that is most appropriate for the model persistence and which can be manipulated +`$aggregate` above is a new object that is most appropriate for the model's persistence and which can be manipulated in various ways to fine-tune aggregation. Below is one sample use:: - $aggregate = $orders->action( - 'group', - 'country_id', - [ - 'country', - 'count'=>'count', - 'total_amount'=>['sum', 'amount'] - ], + $aggregate = $orders->groupBy(['country_id'], [ + 'count' => ['expr' => 'count(*)', 'type' => 'integer'], + 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'money'] + ], ); - foreach ($aggregate as $row) { - var_dump(json_encode($row)); - // ['country'=>'UK', 'count'=>20, 'total_amount'=>123.20]; - // .. - } + $aggregate->addField('country'); + + // $aggregate will have following rows: + // ['country'=>'UK', 'count'=>20, 'total_amount'=>123.20]; + // .. Below is how opening balance can be build:: @@ -45,7 +39,7 @@ Below is how opening balance can be build:: $ledger->addCondition('date', '<', $from); // we actually need grouping by nominal - $ledger->add(new \Atk4\Data\Model\Aggregate()); - $byNominal = $ledger->action('group', 'nominal_id'); - $byNominal->addField('opening_balance', ['sum', 'amount']); - $byNominal->join(); \ No newline at end of file + $ledger->groupBy(['nominal_id'], [ + 'opening_balance' => ['expr' => 'sum([amount])', 'type' => 'money'] + ]); + From b6b085277d1093c5bad65d3630b6c10a5132ce69 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 15:08:59 +0100 Subject: [PATCH 045/151] [update] add return type for Invoice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Voříšek --- tests/ModelAggregateTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index ecccba87d..d650cebfa 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -34,7 +34,7 @@ protected function setUp(): void $this->setDB($this->init_db); } - protected function createInvoice() + protected function createInvoice(): Model\Invoice { $invoice = new Model\Invoice($this->db); $invoice->getRef('client_id')->addTitle(); From a97465881bea6be404d320c5de719295cf3061d6 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 15:28:43 +0100 Subject: [PATCH 046/151] [fix] phpstan issues --- tests/ModelAggregateTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index d650cebfa..396ca7aa9 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -42,7 +42,7 @@ protected function createInvoice(): Model\Invoice return $invoice; } - protected function createInvoiceAggregate(): Aggregate + protected function createInvoiceAggregate() { return $this->createInvoice()->withAggregateField('client'); } @@ -134,6 +134,7 @@ public function testGroupSelectExpr() public function testGroupSelectCondition() { + /** @var Aggregate $aggregate */ $aggregate = $this->createInvoiceAggregate(); $aggregate->baseModel->addCondition('name', 'chair purchase'); From a176a338de0fb1115999a74029a1708edf0f4ef1 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Tue, 29 Dec 2020 16:12:54 +0100 Subject: [PATCH 047/151] [update] optimize demo --- docs/aggregates.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/aggregates.rst b/docs/aggregates.rst index d959fdd25..64f4eaa45 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -21,19 +21,17 @@ Aggregate model can be used for grouping:: `$aggregate` above is a new object that is most appropriate for the model's persistence and which can be manipulated in various ways to fine-tune aggregation. Below is one sample use:: - $aggregate = $orders->groupBy(['country_id'], [ + $aggregate = $orders->withAggregateField('country')->groupBy(['country_id'], [ 'count' => ['expr' => 'count(*)', 'type' => 'integer'], 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'money'] ], ); - - $aggregate->addField('country'); - + // $aggregate will have following rows: // ['country'=>'UK', 'count'=>20, 'total_amount'=>123.20]; // .. -Below is how opening balance can be build:: +Below is how opening balance can be built:: $ledger = new GeneralLedger($db); $ledger->addCondition('date', '<', $from); From 665eb8638e0107e82866139e0b7a9c29560263d8 Mon Sep 17 00:00:00 2001 From: Georgi Hristov Date: Wed, 30 Dec 2020 10:04:42 +0100 Subject: [PATCH 048/151] [update] LIMIT functionality --- src/Model/Aggregate.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 3262980b0..99c2a520f 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -162,13 +162,6 @@ public function addField(string $name, $seed = []): Field : parent::addField($name, $seed); } - public function setLimit(int $count = null, int $offset = 0) - { - $this->baseModel->setLimit($count, $offset); - - return $this; - } - /** * @param string $mode * @param array $args @@ -188,6 +181,7 @@ public function action($mode, $args = []) $this->initQueryOrder($query); $this->initQueryGrouping($query); $this->initQueryConditions($query); + $this->initQueryLimit($query); $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); @@ -276,6 +270,17 @@ protected function initQueryConditions(Query $query, Model\Scope\AbstractScope $ } } + protected function initQueryLimit(Query $query) + { + if ($this->limit && ($this->limit[0] || $this->limit[1])) { + if ($this->limit[0] === null) { + $this->limit[0] = PHP_INT_MAX; + } + + $query->limit($this->limit[0], $this->limit[1]); + } + } + // {{{ Debug Methods /** From ae0a56a54daa01264e64b3e61cc644dcd5d548a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 6 Nov 2021 11:48:49 +0100 Subject: [PATCH 049/151] fix renamed TestCase class --- tests/ModelAggregateTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 396ca7aa9..85af098a8 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -7,8 +7,9 @@ use Atk4\Data\Model\Aggregate; use Atk4\Data\Model\Scope; use Atk4\Data\Model\Scope\Condition; +use Atk4\Data\Schema\TestCase; -class ModelAggregateTest extends \Atk4\Schema\PhpunitTestCase +class ModelAggregateTest extends TestCase { /** @var array */ private $init_db = From 722cbdfe72aac1a77ad6af0fe9ca736f19ccce0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 6 Nov 2021 11:50:57 +0100 Subject: [PATCH 050/151] fix renamed atk4_money type --- docs/aggregates.rst | 4 +-- src/Model/Aggregate.php | 6 ++--- tests/Model/Invoice.php | 2 +- tests/ModelAggregateTest.php | 50 ++++++++++++++++++------------------ 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/aggregates.rst b/docs/aggregates.rst index 64f4eaa45..f3f418850 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -23,7 +23,7 @@ in various ways to fine-tune aggregation. Below is one sample use:: $aggregate = $orders->withAggregateField('country')->groupBy(['country_id'], [ 'count' => ['expr' => 'count(*)', 'type' => 'integer'], - 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'money'] + 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] ], ); @@ -38,6 +38,6 @@ Below is how opening balance can be built:: // we actually need grouping by nominal $ledger->groupBy(['nominal_id'], [ - 'opening_balance' => ['expr' => 'sum([amount])', 'type' => 'money'] + 'opening_balance' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] ]); diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 99c2a520f..d3078ea85 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -35,7 +35,7 @@ * Union model implements identical grouping rule on its own. * * You can also pass seed (for example field type) when aggregating: - * $aggregate->groupBy(['first','last'], ['salary' => ['sum([])', 'type'=>'money']]; + * $aggregate->groupBy(['first', 'last'], ['salary' => ['sum([])', 'type' => 'atk4_']]; * * @property \Atk4\Data\Persistence\Sql $persistence * @@ -246,7 +246,7 @@ protected function initQueryGrouping(Query $query): void protected function initQueryConditions(Query $query, Model\Scope\AbstractScope $condition = null): void { - $condition = $condition ?? $this->scope(); + $condition ??= $this->scope(); if (!$condition->isEmpty()) { // peel off the single nested scopes to convert (((field = value))) to field = value @@ -274,7 +274,7 @@ protected function initQueryLimit(Query $query) { if ($this->limit && ($this->limit[0] || $this->limit[1])) { if ($this->limit[0] === null) { - $this->limit[0] = PHP_INT_MAX; + $this->limit[0] = \PHP_INT_MAX; } $query->limit($this->limit[0], $this->limit[1]); diff --git a/tests/Model/Invoice.php b/tests/Model/Invoice.php index cd1e85000..3ab331584 100644 --- a/tests/Model/Invoice.php +++ b/tests/Model/Invoice.php @@ -16,6 +16,6 @@ protected function init(): void $this->addField('name'); $this->hasOne('client_id', ['model' => [Client::class, 'table' => 'client']]); - $this->addField('amount', ['type' => 'money']); + $this->addField('amount', ['type' => 'atk4_money']); } } diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 85af098a8..d129d3f8e 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -81,7 +81,7 @@ public function testGroupSelect2() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); $this->assertSame( @@ -98,10 +98,10 @@ public function testGroupSelect3() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 's' => ['expr' => 'sum([amount])', 'type' => 'money'], - 'min' => ['expr' => 'min([amount])', 'type' => 'money'], - 'max' => ['expr' => 'max([amount])', 'type' => 'money'], - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], // same as `s`, but reuse name `amount` + 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + 'min' => ['expr' => 'min([amount])', 'type' => 'atk4_money'], + 'max' => ['expr' => 'max([amount])', 'type' => 'atk4_money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], // same as `s`, but reuse name `amount` ]); $this->assertSame( @@ -118,11 +118,11 @@ public function testGroupSelectExpr() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 's' => ['expr' => 'sum([amount])', 'type' => 'money'], - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); $this->assertSame( [ @@ -140,11 +140,11 @@ public function testGroupSelectCondition() $aggregate->baseModel->addCondition('name', 'chair purchase'); $aggregate->groupBy(['client_id'], [ - 's' => ['expr' => 'sum([amount])', 'type' => 'money'], - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); $this->assertSame( [ @@ -160,11 +160,11 @@ public function testGroupSelectCondition2() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 's' => ['expr' => 'sum([amount])', 'type' => 'money'], - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); $aggregate->addCondition('double', '>', 10); $this->assertSame( @@ -180,11 +180,11 @@ public function testGroupSelectCondition3() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 's' => ['expr' => 'sum([amount])', 'type' => 'money'], - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); $aggregate->addCondition('double', 38); $this->assertSame( @@ -200,11 +200,11 @@ public function testGroupSelectCondition4() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 's' => ['expr' => 'sum([amount])', 'type' => 'money'], - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'money']); + $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); $aggregate->addCondition('client_id', 2); $this->assertSame( @@ -220,7 +220,7 @@ public function testGroupSelectScope() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', 4)); @@ -240,7 +240,7 @@ public function testGroupOrder() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); $aggregate->setOrder('client_id', 'asc'); @@ -256,7 +256,7 @@ public function testGroupLimit() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); $aggregate->setLimit(1); @@ -273,7 +273,7 @@ public function testGroupLimit2() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); $aggregate->setLimit(1, 1); @@ -290,7 +290,7 @@ public function testGroupCount() $aggregate = $this->createInvoiceAggregate(); $aggregate->groupBy(['client_id'], [ - 'amount' => ['expr' => 'sum([])', 'type' => 'money'], + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); $this->assertSameSql( From 1f257409f1bf47594f3c478191b8134e602e0e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 6 Nov 2021 12:00:49 +0100 Subject: [PATCH 051/151] add missing void return type for test cases --- tests/ModelAggregateTest.php | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index d129d3f8e..d3667d31e 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -48,7 +48,7 @@ protected function createInvoiceAggregate() return $this->createInvoice()->withAggregateField('client'); } - public function testGroupBy() + public function testGroupBy(): void { $invoiceAggregate = $this->createInvoice()->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); @@ -61,7 +61,7 @@ public function testGroupBy() ); } - public function testGroupSelect() + public function testGroupSelect(): void { $aggregate = $this->createInvoiceAggregate(); @@ -76,7 +76,7 @@ public function testGroupSelect() ); } - public function testGroupSelect2() + public function testGroupSelect2(): void { $aggregate = $this->createInvoiceAggregate(); @@ -93,7 +93,7 @@ public function testGroupSelect2() ); } - public function testGroupSelect3() + public function testGroupSelect3(): void { $aggregate = $this->createInvoiceAggregate(); @@ -113,7 +113,7 @@ public function testGroupSelect3() ); } - public function testGroupSelectExpr() + public function testGroupSelectExpr(): void { $aggregate = $this->createInvoiceAggregate(); @@ -133,7 +133,7 @@ public function testGroupSelectExpr() ); } - public function testGroupSelectCondition() + public function testGroupSelectCondition(): void { /** @var Aggregate $aggregate */ $aggregate = $this->createInvoiceAggregate(); @@ -155,7 +155,7 @@ public function testGroupSelectCondition() ); } - public function testGroupSelectCondition2() + public function testGroupSelectCondition2(): void { $aggregate = $this->createInvoiceAggregate(); @@ -175,7 +175,7 @@ public function testGroupSelectCondition2() ); } - public function testGroupSelectCondition3() + public function testGroupSelectCondition3(): void { $aggregate = $this->createInvoiceAggregate(); @@ -195,7 +195,7 @@ public function testGroupSelectCondition3() ); } - public function testGroupSelectCondition4() + public function testGroupSelectCondition4(): void { $aggregate = $this->createInvoiceAggregate(); @@ -215,7 +215,7 @@ public function testGroupSelectCondition4() ); } - public function testGroupSelectScope() + public function testGroupSelectScope(): void { $aggregate = $this->createInvoiceAggregate(); @@ -235,7 +235,7 @@ public function testGroupSelectScope() ); } - public function testGroupOrder() + public function testGroupOrder(): void { $aggregate = $this->createInvoiceAggregate(); @@ -251,7 +251,7 @@ public function testGroupOrder() ); } - public function testGroupLimit() + public function testGroupLimit(): void { $aggregate = $this->createInvoiceAggregate(); @@ -268,7 +268,7 @@ public function testGroupLimit() ); } - public function testGroupLimit2() + public function testGroupLimit2(): void { $aggregate = $this->createInvoiceAggregate(); @@ -285,7 +285,7 @@ public function testGroupLimit2() ); } - public function testGroupCount() + public function testGroupCount(): void { $aggregate = $this->createInvoiceAggregate(); @@ -299,7 +299,7 @@ public function testGroupCount() ); } - public function testAggregateFieldExpression() + public function testAggregateFieldExpression(): void { $aggregate = $this->createInvoiceAggregate(); From 5c40110a01385774940366cf654ea7165310d473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 6 Nov 2021 12:02:15 +0100 Subject: [PATCH 052/151] fix renamespaced Dsql names --- src/Model/Aggregate.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index d3078ea85..93a5a39eb 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -9,9 +9,9 @@ use Atk4\Data\FieldSqlExpression; use Atk4\Data\Model; use Atk4\Data\Persistence; +use Atk4\Data\Persistence\Sql\Expression; +use Atk4\Data\Persistence\Sql\Query; use Atk4\Data\Reference; -use Atk4\Dsql\Expression; -use Atk4\Dsql\Query; /** * Aggregate model allows you to query using "group by" clause on your existing model. From d047cd55683a6d995c57dcd821a87f848a73bca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 6 Nov 2021 12:08:12 +0100 Subject: [PATCH 053/151] fix stan --- src/Model/Aggregate.php | 6 ++++-- src/Model/AggregatesTrait.php | 6 +++++- tests/ModelAggregateTest.php | 5 ++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 93a5a39eb..b1df2369f 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -83,7 +83,7 @@ public function __construct(Model $baseModel, array $defaults = []) * * @return $this */ - public function groupBy(array $fields, array $aggregateExpressions = []) + public function groupBy(array $fields, array $aggregateExpressions = []): Model { $this->groupByFields = array_unique(array_merge($this->groupByFields, $fields)); @@ -125,6 +125,8 @@ public function getRef(string $link): Reference * and * * $model->withAggregateField('xyz')->groupBy(['abc']); + * + * @return $this */ public function withAggregateField(string $name, $seed = []): Model { @@ -270,7 +272,7 @@ protected function initQueryConditions(Query $query, Model\Scope\AbstractScope $ } } - protected function initQueryLimit(Query $query) + protected function initQueryLimit(Query $query): void { if ($this->limit && ($this->limit[0] || $this->limit[1])) { if ($this->limit[0] === null) { diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php index d5612ea7b..1c260d577 100644 --- a/src/Model/AggregatesTrait.php +++ b/src/Model/AggregatesTrait.php @@ -14,6 +14,8 @@ trait AggregatesTrait /** * @param array|object $seed * + * @return Aggregate + * * @see Aggregate::withAggregateField. */ public function withAggregateField(string $name, $seed = []): Model @@ -22,9 +24,11 @@ public function withAggregateField(string $name, $seed = []): Model } /** + * @return Aggregate + * * @see Aggregate::groupBy. */ - public function groupBy(array $fields, array $aggregateExpressions = []) + public function groupBy(array $fields, array $aggregateExpressions = []): Model { return (new Aggregate($this))->groupBy($fields, $aggregateExpressions); } diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index d3667d31e..0cdff3cec 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -32,7 +32,7 @@ class ModelAggregateTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->setDB($this->init_db); + $this->setDb($this->init_db); } protected function createInvoice(): Model\Invoice @@ -43,7 +43,7 @@ protected function createInvoice(): Model\Invoice return $invoice; } - protected function createInvoiceAggregate() + protected function createInvoiceAggregate(): Aggregate { return $this->createInvoice()->withAggregateField('client'); } @@ -135,7 +135,6 @@ public function testGroupSelectExpr(): void public function testGroupSelectCondition(): void { - /** @var Aggregate $aggregate */ $aggregate = $this->createInvoiceAggregate(); $aggregate->baseModel->addCondition('name', 'chair purchase'); From ad9c90a268183affaa93005388664f5b59cd2f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 5 Jan 2022 02:27:08 +0100 Subject: [PATCH 054/151] fix typo in comment --- src/Model/Aggregate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index b1df2369f..85bd51075 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -35,7 +35,7 @@ * Union model implements identical grouping rule on its own. * * You can also pass seed (for example field type) when aggregating: - * $aggregate->groupBy(['first', 'last'], ['salary' => ['sum([])', 'type' => 'atk4_']]; + * $aggregate->groupBy(['first', 'last'], ['salary' => ['sum([])', 'type' => 'atk4_money']]; * * @property \Atk4\Data\Persistence\Sql $persistence * From 41637d05eac93723c354b63242bff53ec9777554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 5 Jan 2022 02:40:58 +0100 Subject: [PATCH 055/151] fix SQL render assertions --- tests/ModelAggregateTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 0cdff3cec..c55cefbd8 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -245,8 +245,8 @@ public function testGroupOrder(): void $aggregate->setOrder('client_id', 'asc'); $this->assertSameSql( - 'select (select "name" from "client" "c" where "id" = "invoice"."client_id") "client","invoice"."client_id",sum("invoice"."amount") "amount" from "invoice" group by "invoice"."client_id" order by "invoice"."client_id"', - $aggregate->action('select')->render() + 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", "invoice"."client_id", sum("amount") "amount" from "invoice" group by "client_id" order by "invoice"."client_id"', + $aggregate->action('select')->render()[0] ); } @@ -294,7 +294,7 @@ public function testGroupCount(): void $this->assertSameSql( 'select count(*) from ((select 1 from "invoice" group by "client_id")) der', - $aggregate->action('count')->render() + $aggregate->action('count')->render()[0] ); } @@ -307,8 +307,8 @@ public function testAggregateFieldExpression(): void ]); $this->assertSameSql( - 'select (select "name" from "client" "c" where "id" = "invoice"."client_id") "client","invoice"."abc",sum("invoice"."amount") "xyz" from "invoice" group by abc', - $aggregate->action('select')->render() + 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", "invoice"."abc", sum("amount") "xyz" from "invoice" group by abc', + $aggregate->action('select')->render()[0] ); } } From a21ef63dbdd6798f8c5a8dce189f329ddf8c9a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 5 Jan 2022 02:41:43 +0100 Subject: [PATCH 056/151] fix renamed API --- src/Model/Aggregate.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 85bd51075..0d48e8b5f 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -6,7 +6,7 @@ use Atk4\Data\Exception; use Atk4\Data\Field; -use Atk4\Data\FieldSqlExpression; +use Atk4\Data\Field\SqlExpressionField; use Atk4\Data\Model; use Atk4\Data\Persistence; use Atk4\Data\Persistence\Sql\Expression; @@ -144,7 +144,7 @@ public function addField(string $name, $seed = []): Field { $seed = is_array($seed) ? $seed : [$seed]; - if (isset($seed[0]) && $seed[0] instanceof FieldSqlExpression) { + if (isset($seed[0]) && $seed[0] instanceof SqlExpressionField) { return parent::addField($name, $seed[0]); } @@ -174,7 +174,7 @@ public function action($mode, $args = []) { switch ($mode) { case 'select': - $fields = $this->only_fields ?: array_keys($this->getFields()); + $fields = $this->onlyFields ?: array_keys($this->getFields()); // select but no need your fields $query = $this->baseModel->action($mode, [false]); From 25dc215f13dcfa7c560bb4e682e6de0d0597ef1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 5 Jan 2022 11:29:02 +0100 Subject: [PATCH 057/151] fix comparison/bind for atk4_money typed values for Sqlite --- tests/ModelAggregateTest.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index c55cefbd8..2a6785d2f 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -8,6 +8,7 @@ use Atk4\Data\Model\Scope; use Atk4\Data\Model\Scope\Condition; use Atk4\Data\Schema\TestCase; +use Doctrine\DBAL\Platforms\SqlitePlatform; class ModelAggregateTest extends TestCase { @@ -164,7 +165,12 @@ public function testGroupSelectCondition2(): void ]); $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); - $aggregate->addCondition('double', '>', 10); + $aggregate->addCondition( + 'double', + '>', + // TODO Sqlite bind param does not work, expr needed, even if casted to float with DBAL type (comparison works only if casted to/bind as int) + $this->getDatabasePlatform() instanceof SqlitePlatform ? $aggregate->expr('10') : 10 + ); $this->assertSame( [ @@ -184,7 +190,11 @@ public function testGroupSelectCondition3(): void ]); $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); - $aggregate->addCondition('double', 38); + $aggregate->addCondition( + 'double', + // TODO Sqlite bind param does not work, expr needed, even if casted to float with DBAL type (comparison works only if casted to/bind as int) + $this->getDatabasePlatform() instanceof SqlitePlatform ? $aggregate->expr('38') : 38 + ); $this->assertSame( [ @@ -222,7 +232,9 @@ public function testGroupSelectScope(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', 4)); + // TODO Sqlite bind param does not work, expr needed, even if casted to float with DBAL type (comparison works only if casted to/bind as int) + $numExpr = $this->getDatabasePlatform() instanceof SqlitePlatform ? $aggregate->expr('4') : 4; + $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', $numExpr)); $aggregate->addCondition($scope); From 0f9bbe88aa08f8403ea25cffe37f03f0f084554d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 5 Jan 2022 11:57:01 +0100 Subject: [PATCH 058/151] fix tests by not enforcing use_table_prefixes --- src/Model/Aggregate.php | 3 --- tests/ModelAggregateTest.php | 7 ++++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 0d48e8b5f..0feac147e 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -71,9 +71,6 @@ public function __construct(Model $baseModel, array $defaults = []) $this->read_only = true; parent::__construct($baseModel->persistence, $defaults); - - // always use table prefixes for this model - $this->persistence_data['use_table_prefixes'] = true; } /** diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 2a6785d2f..5a96334ed 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -16,7 +16,8 @@ class ModelAggregateTest extends TestCase private $init_db = [ 'client' => [ - ['name' => 'Vinny'], + // allow of migrator to create all columns + ['name' => 'Vinny', 'surname' => null, 'order' => null], ['name' => 'Zoe'], ], 'invoice' => [ @@ -257,7 +258,7 @@ public function testGroupOrder(): void $aggregate->setOrder('client_id', 'asc'); $this->assertSameSql( - 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", "invoice"."client_id", sum("amount") "amount" from "invoice" group by "client_id" order by "invoice"."client_id"', + 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", "client_id", sum("amount") "amount" from "invoice" group by "client_id" order by "client_id"', $aggregate->action('select')->render()[0] ); } @@ -319,7 +320,7 @@ public function testAggregateFieldExpression(): void ]); $this->assertSameSql( - 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", "invoice"."abc", sum("amount") "xyz" from "invoice" group by abc', + 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", "abc", sum("amount") "xyz" from "invoice" group by abc', $aggregate->action('select')->render()[0] ); } From e7639e804f1df4ba7990379add52ad168837dd5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 6 Jan 2022 20:12:58 +0100 Subject: [PATCH 059/151] workaround mysql server 8.0.27 bug in test --- phpstan.neon.dist | 2 +- src/Persistence/GenericPlatform.php | 2 ++ tests/ModelAggregateTest.php | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 726da5b7c..e4f995e32 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -36,7 +36,7 @@ parameters: message: '~^(Call to an undefined method Doctrine\\DBAL\\Driver\\Connection::getWrappedConnection\(\)\.|Call to an undefined method Doctrine\\DBAL\\Connection::createSchemaManager\(\)\.|Call to an undefined static method Doctrine\\DBAL\\Exception::invalidPdoInstance\(\)\.|Call to method (getCreateTableSQL|getClobTypeDeclarationSQL|initializeCommentedDoctrineTypes)\(\) of deprecated class Doctrine\\DBAL\\Platforms\\\w+Platform:\n.+|Anonymous class extends deprecated class Doctrine\\DBAL\\Platforms\\(PostgreSQL94Platform|SQLServer2012Platform):\n.+|Call to deprecated method fetch(|All)\(\) of class Doctrine\\DBAL\\Result:\n.+|Call to deprecated method getSchemaManager\(\) of class Doctrine\\DBAL\\Connection:\n.+|Access to an undefined property Doctrine\\DBAL\\Driver\\PDO\\Connection::\$connection\.|Parameter #1 \$dsn of class Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection constructor expects string, Doctrine\\DBAL\\Driver\\PDO\\Connection given\.|Method Atk4\\Data\\Persistence\\Sql\\Expression::execute\(\) should return Doctrine\\DBAL\\Result\|PDOStatement but returns bool\.|PHPDoc tag @return contains generic type Doctrine\\DBAL\\Schema\\AbstractSchemaManager but class Doctrine\\DBAL\\Schema\\AbstractSchemaManager is not generic\.|Class Doctrine\\DBAL\\Platforms\\(MySqlPlatform|PostgreSqlPlatform) referenced with incorrect case: Doctrine\\DBAL\\Platforms\\(MySQLPlatform|PostgreSQLPlatform)\.)$~' path: '*' # count for DBAL 3.x matched in "src/Persistence/GenericPlatform.php" file - count: 39 + count: 41 # TODO these rules are generated, this ignores should be fixed in the code # for src/Schema/TestCase.php diff --git a/src/Persistence/GenericPlatform.php b/src/Persistence/GenericPlatform.php index 8b6a59dd6..94f415824 100644 --- a/src/Persistence/GenericPlatform.php +++ b/src/Persistence/GenericPlatform.php @@ -47,6 +47,8 @@ private function createNotSupportedException(): \Exception $connection->getSchemaManager(); $connection->getSchemaManager(); $connection->getSchemaManager(); + $connection->getSchemaManager(); + $connection->getSchemaManager(); } } diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 5a96334ed..22d9a6e31 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -8,6 +8,7 @@ use Atk4\Data\Model\Scope; use Atk4\Data\Model\Scope\Condition; use Atk4\Data\Schema\TestCase; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\SqlitePlatform; class ModelAggregateTest extends TestCase @@ -237,6 +238,13 @@ public function testGroupSelectScope(): void $numExpr = $this->getDatabasePlatform() instanceof SqlitePlatform ? $aggregate->expr('4') : 4; $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', $numExpr)); + // MySQL Server v8.0.27 (and possibly some lower versions) returns a wrong result + // see https://bugs.mysql.com/bug.php?id=106063 + // remove this fix once v8.0.28 is released and Docker image is available + if ($this->getDatabasePlatform() instanceof MySQLPlatform) { + array_pop($scope->elements); + } + $aggregate->addCondition($scope); $this->assertSame( From 787c5d9988c772bb211ab8e8a82be3e530c75c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 6 Jan 2022 20:18:10 +0100 Subject: [PATCH 060/151] fix doc cs --- docs/aggregates.rst | 2 +- docs/index.rst | 2 -- tests/Model/Client.php | 2 ++ tests/Model/Invoice.php | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/aggregates.rst b/docs/aggregates.rst index f3f418850..ac982e07c 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -28,7 +28,7 @@ in various ways to fine-tune aggregation. Below is one sample use:: ); // $aggregate will have following rows: - // ['country'=>'UK', 'count'=>20, 'total_amount'=>123.20]; + // ['country' => 'UK', 'count' => 20, 'total_amount' => 123.20]; // .. Below is how opening balance can be built:: diff --git a/docs/index.rst b/docs/index.rst index 7562c9871..c033b46c3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,8 +28,6 @@ Contents: extensions persistence/csv - - Indices and tables ================== diff --git a/tests/Model/Client.php b/tests/Model/Client.php index d5dd6d1a5..02ec7d380 100644 --- a/tests/Model/Client.php +++ b/tests/Model/Client.php @@ -6,6 +6,8 @@ class Client extends User { + public $table = 'client'; + protected function init(): void { parent::init(); diff --git a/tests/Model/Invoice.php b/tests/Model/Invoice.php index 3ab331584..414807cc9 100644 --- a/tests/Model/Invoice.php +++ b/tests/Model/Invoice.php @@ -13,9 +13,9 @@ class Invoice extends Model protected function init(): void { parent::init(); - $this->addField('name'); - $this->hasOne('client_id', ['model' => [Client::class, 'table' => 'client']]); + $this->hasOne('client_id', ['model' => [Client::class]]); + $this->addField('name'); $this->addField('amount', ['type' => 'atk4_money']); } } From 1fa050bde968fd8929d3de445de8ef7de1f6428b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 5 Jan 2022 15:37:08 +0100 Subject: [PATCH 061/151] adjust to the latest atk4/data (by phpstan) --- docs/unions.rst | 57 +++++------ src/Model/Union.php | 63 ++++++------ tests/Model/Payment.php | 2 +- tests/Model/Transaction.php | 11 +- tests/ModelUnionTest.php | 195 ++++++++++++++++-------------------- tests/ReportTest.php | 21 ++-- 6 files changed, 168 insertions(+), 181 deletions(-) diff --git a/docs/unions.rst b/docs/unions.rst index 531d13d23..2265f3a47 100644 --- a/docs/unions.rst +++ b/docs/unions.rst @@ -13,16 +13,17 @@ In some cases data from multiple models need to be combined. In this case the Un In the case used below Client model schema may have multiple invoices and multiple payments. Payment is not related to the invoice.:: class Client extends \Atk4\Data\Model { - public $table = 'client'; - - protected function init(): void - { - parent::init(); - $this->addField('name'); - - $this->hasMany('Payment'); - $this->hasMany('Invoice'); - } + public $table = 'client'; + + protected function init(): void + { + parent::init(); + + $this->addField('name'); + + $this->hasMany('Payment'); + $this->hasMany('Invoice'); + } } (see tests/ModelUnionTest.php, tests/Model/Client.php, tests/Model/Payment.php and tests/Model/Invoice.php files). @@ -30,19 +31,19 @@ In the case used below Client model schema may have multiple invoices and multip Union Model Definition ---------------------- -Normally a model is associated with a single table. Union model can have multiple nested models defined and it fetches +Normally a model is associated with a single table. Union model can have multiple nested models defined and it fetches results from that. As a result, Union model will have no "id" field. Below is an example of inline definition of Union model. The Union model can be separated in a designated class and nested model added within the init() method body of the new class:: $unionPaymentInvoice = new \Atk4\Data\Model\Union(); - + $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment()); Next, assuming that both models have common fields "name" and "amount", `$unionPaymentInvoice` fields can be set:: $unionPaymentInvoice->addField('name'); - $unionPaymentInvoice->addField('amount', ['type'=>'money']); + $unionPaymentInvoice->addField('amount', ['type' => 'atk4_money']); Then data can be queried:: @@ -54,14 +55,14 @@ Union Model Fields Below is an example of 3 different ways to define fields for the Union model:: // Will link the "name" field with all the nested models. - $unionPaymentInvoice->addField('client_id'); - + $unionPaymentInvoice->addField('client_id'); + // Expression will not affect nested models in any way - $unionPaymentInvoice->addExpression('name_capital','upper([name])'); - + $unionPaymentInvoice->addExpression('name_capital', 'upper([name])'); + // Union model can be joined with extra tables and define some fields from those joins $unionPaymentInvoice - ->join('client','client_id') + ->join('client', 'client_id') ->addField('client_name', 'name'); :ref:`Expressions` and :ref:`Joins` are working just as they would on any other model. @@ -69,26 +70,26 @@ Below is an example of 3 different ways to define fields for the Union model:: Field Mapping ------------- -Sometimes the field that is defined in the Union model may be named differently inside nested models. -E.g. Invoice has field "description" and payment has field "note". +Sometimes the field that is defined in the Union model may be named differently inside nested models. +E.g. Invoice has field "description" and payment has field "note". When defining a nested model a field map array needs to be specified:: $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); - $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description'=>'[note]']); + $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); $unionPaymentInvoice->addField('description'); The key of the field map array must match the Union field. The value is an expression. (See :ref:`Model`). -This format can also be used to reverse sign on amounts. When we are creating "Transactions", then invoices would be subtracted from the amount, -while payments will be added:: +This format can also be used to reverse sign on amounts. When we are creating "Transactions", then invoices would be +subtracted from the amount, while payments will be added:: - $nestedPayment = $m_uni->addNestedModel(new Invoice(), ['amount'=>'-[amount]']); - $nestedInvoice = $m_uni->addNestedModel(new Payment(), ['description'=>'[note]']); + $nestedPayment = $m_uni->addNestedModel(new Invoice(), ['amount' => '-[amount]']); + $nestedInvoice = $m_uni->addNestedModel(new Payment(), ['description' => '[note]']); $unionPaymentInvoice->addField('description'); Should more flexibility be needed, more expressions (or fields) can be added directly to nested models:: - $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice(), ['amount'=>'-[amount]']); - $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description'=>'[note]']); + $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice(), ['amount' => '-[amount]']); + $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); $nestedPayment->addExpression('type', '"payment"'); $nestedInvoice->addExpression('type', '"invoice"'); @@ -115,7 +116,7 @@ Grouping Results Union model has also a built-in grouping support:: - $unionPaymentInvoice->groupBy('client_id', ['amount'=>'sum']); + $unionPaymentInvoice->groupBy('client_id', ['amount' => 'sum']); When specifying a grouping field and it is associated with nested models then grouping will be enabled on every nested model. diff --git a/src/Model/Union.php b/src/Model/Union.php index 97a139d38..a5697b92a 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -6,10 +6,10 @@ use Atk4\Data\Exception; use Atk4\Data\Field; -use Atk4\Data\FieldSqlExpression; +use Atk4\Data\Field\SqlExpressionField; use Atk4\Data\Model; -use Atk4\Dsql\Expression; -use Atk4\Dsql\Query; +use Atk4\Data\Persistence\Sql\Expression; +use Atk4\Data\Persistence\Sql\Query; /** * Union model combines multiple nested models through a UNION in order to retrieve @@ -65,9 +65,9 @@ class Union extends Model * the remaining fields will be hidden (marked as system()) and * have their "aggregates" added into the selectQuery (if possible). * - * @var array|string + * @var array */ - public $group; + public $group = []; /** * When grouping, the functions will be applied as per aggregate @@ -109,22 +109,24 @@ public function getFieldExpr(Model $model, string $fieldName, string $expr = nul */ public function addNestedModel(Model $model, array $fieldMap = []): Model { - $nestedModel = $this->persistence->add($model); + $this->persistence->add($model); - $this->union[] = [$nestedModel, $fieldMap]; + $this->union[] = [$model, $fieldMap]; - return $nestedModel; + return $model; // TODO nothing/void should be returned } /** * Specify a single field or array of fields. * * @param string|array $group + * + * @phpstan-return Model */ - public function groupBy($group, array $aggregate = []): Model + public function groupBy($group, array $aggregate = []): Model // @phpstan-ignore-line { $this->aggregate = $aggregate; - $this->group = $group; + $this->group = is_string($group) ? [$group] : $group; foreach ($aggregate as $fieldName => $seed) { $seed = (array) $seed; @@ -146,7 +148,7 @@ public function groupBy($group, array $aggregate = []): Model foreach ($this->union as [$nestedModel, $fieldMap]) { if ($nestedModel instanceof self) { $nestedModel->aggregate = $aggregate; - $nestedModel->group = $group; + $nestedModel->group = is_string($group) ? [$group] : $group; } } @@ -225,7 +227,7 @@ public function action($mode, $args = []) switch ($mode) { case 'select': // get list of available fields - $fields = $this->only_fields ?: array_keys($this->getFields()); + $fields = $this->onlyFields ?: array_keys($this->getFields()); foreach ($fields as $k => $field) { if ($this->getField($field)->never_persist) { unset($fields[$k]); @@ -234,9 +236,10 @@ public function action($mode, $args = []) $subquery = $this->getSubQuery($fields); $query = parent::action($mode, $args)->reset('table')->table($subquery); - if (isset($this->group)) { - $query->group($this->group); + foreach ($this->group as $group) { + $query->group($group); } + $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); return $query; @@ -310,12 +313,12 @@ public function getSubQuery(array $fields): Expression // Union can have some fields defined as expressions. We don't touch those either. // Imants: I have no idea why this condition was set, but it's limiting our ability // to use expression fields in mapping - if ($field instanceof FieldSqlExpression && !isset($this->aggregate[$fieldName])) { + if ($field instanceof SqlExpressionField && !isset($this->aggregate[$fieldName])) { continue; } // if we group we do not select non-aggregate fields - if ($this->group && !in_array($fieldName, (array) $this->group, true) && !isset($this->aggregate[$fieldName])) { + if (count($this->group) > 0 && !in_array($fieldName, $this->group, true) && !isset($this->aggregate[$fieldName])) { continue; } @@ -343,27 +346,21 @@ public function getSubQuery(array $fields): Expression //$query = parent::action($mode, $args); $query->reset('table')->table($subquery); - if (isset($nestedModel->group)) { - $query->group($nestedModel->group); + foreach ($nestedModel->group as $group) { + $query->group($group); } } - $query->field($queryFieldExpressions); + foreach ($queryFieldExpressions as $fAlias => $fExpr) { + $query->field($fExpr, $fAlias); + } // also for sub-queries - if ($this->group) { - if (is_array($this->group)) { - foreach ($this->group as $group) { - if (isset($fieldMap[$group])) { - $query->group($nestedModel->expr($fieldMap[$group])); - } elseif ($nestedModel->hasField($group)) { - $query->group($nestedModel->getField($group)); - } - } - } elseif (isset($fieldMap[$this->group])) { - $query->group($nestedModel->expr($fieldMap[$this->group])); - } else { - $query->group($this->group); + foreach ($this->group as $group) { + if (isset($fieldMap[$group])) { + $query->group($nestedModel->expr($fieldMap[$group])); + } elseif ($nestedModel->hasField($group)) { + $query->group($nestedModel->getField($group)); } } @@ -395,7 +392,7 @@ public function getSubAction(string $action, array $actionArgs = []): Expression $model, $fieldName, $fieldMap[$fieldName] ?? null - ); + ); } $query = $model->action($action, $modelActionArgs); diff --git a/tests/Model/Payment.php b/tests/Model/Payment.php index be44a5dae..ca69d6668 100644 --- a/tests/Model/Payment.php +++ b/tests/Model/Payment.php @@ -16,6 +16,6 @@ protected function init(): void $this->addField('name'); $this->hasOne('client_id', ['model' => [Client::class]]); - $this->addField('amount', ['type' => 'money']); + $this->addField('amount', ['type' => 'atk4_money']); } } diff --git a/tests/Model/Transaction.php b/tests/Model/Transaction.php index 84ef4fa8c..3255f792a 100644 --- a/tests/Model/Transaction.php +++ b/tests/Model/Transaction.php @@ -8,9 +8,12 @@ class Transaction extends Union { + /** @var Invoice */ public $nestedInvoice; + /** @var Payment */ public $nestedPayment; + /** @var bool */ public $subtractInvoice; protected function init(): void @@ -18,11 +21,13 @@ protected function init(): void parent::init(); // first lets define nested models - $this->nestedInvoice = $this->addNestedModel(new Invoice(), $this->subtractInvoice ? ['amount' => '-[]'] : []); - $this->nestedPayment = $this->addNestedModel(new Payment()); + $this->nestedInvoice = new Invoice(); + $this->addNestedModel($this->nestedInvoice, $this->subtractInvoice ? ['amount' => '-[]'] : []); + $this->nestedPayment = new Payment(); + $this->addNestedModel($this->nestedPayment); // next, define common fields $this->addField('name'); - $this->addField('amount', ['type' => 'money']); + $this->addField('amount', ['type' => 'atk4_money']); } } diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 79c176b19..ff2014bfe 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -4,17 +4,11 @@ namespace Atk4\Data\Tests; +use Atk4\Data\Schema\TestCase; use Doctrine\DBAL\Platforms\OraclePlatform; -class ModelUnionTest extends \Atk4\Schema\PhpunitTestCase +class ModelUnionTest extends TestCase { - /** @var Model\Client|null */ - protected $client; - /** @var Model\Transaction|null */ - protected $transaction; - /** @var Model\Transaction|null */ - protected $subtractInvoiceTransaction; - /** @var array */ private $init_db = [ @@ -37,33 +31,20 @@ class ModelUnionTest extends \Atk4\Schema\PhpunitTestCase protected function setUp(): void { parent::setUp(); - $this->setDB($this->init_db); - - $this->client = $this->createClient($this->db); - $this->transaction = $this->createTransaction($this->db); - $this->subtractInvoiceTransaction = $this->createSubtractInvoiceTransaction($this->db); - } - - protected function tearDown(): void - { - $this->client = null; - $this->transaction = null; - $this->subtractInvoiceTransaction = null; - - parent::tearDown(); + $this->setDb($this->init_db); } - protected function createTransaction($persistence = null) + protected function createTransaction(): Model\Transaction { - return new Model\Transaction($persistence); + return new Model\Transaction($this->db); } - protected function createSubtractInvoiceTransaction($persistence = null) + protected function createSubtractInvoiceTransaction(): Model\Transaction { - return new Model\Transaction($persistence, ['subtractInvoice' => true]); + return new Model\Transaction($this->db, ['subtractInvoice' => true]); } - protected function createClient($persistence = null) + protected function createClient(): Model\Client { $client = new Model\Client($this->db); @@ -73,105 +54,105 @@ protected function createClient($persistence = null) return $client; } - public function testFieldExpr() + public function testFieldExpr(): void { - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); - $this->assertSameSql('"amount"', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount')])->render()); - $this->assertSameSql('-"amount"', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount', '-[]')])->render()); - $this->assertSameSql('-NULL', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'blah', '-[]')])->render()); + $this->assertSameSql('"amount"', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount')])->render()[0]); + $this->assertSameSql('-"amount"', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount', '-[]')])->render()[0]); + $this->assertSameSql('-NULL', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'blah', '-[]')])->render()[0]); } - public function testNestedQuery1() + public function testNestedQuery1(): void { - $transaction = clone $this->transaction; + $transaction = $this->createTransaction(); $this->assertSameSql( '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"', - $transaction->getSubQuery(['name'])->render() + $transaction->getSubQuery(['name'])->render()[0] ); $this->assertSameSql( '(select "name" "name","amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"', - $transaction->getSubQuery(['name', 'amount'])->render() + $transaction->getSubQuery(['name', 'amount'])->render()[0] ); $this->assertSameSql( '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"', - $transaction->getSubQuery(['name'])->render() + $transaction->getSubQuery(['name'])->render()[0] ); } /** * If field is not set for one of the nested model, instead of generating exception, NULL will be filled in. */ - public function testMissingField() + public function testMissingField(): void { - $transaction = clone $this->transaction; + $transaction = $this->createTransaction(); $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->addField('type'); $this->assertSameSql( '(select (\'invoice\') "type","amount" "amount" from "invoice" UNION ALL select NULL "type","amount" "amount" from "payment") "derivedTable"', - $transaction->getSubQuery(['type', 'amount'])->render() + $transaction->getSubQuery(['type', 'amount'])->render()[0] ); } - public function testActions() + public function testActions(): void { - $transaction = clone $this->transaction; + $transaction = $this->createTransaction(); $this->assertSameSql( 'select "name","amount" from (select "name" "name","amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"', - $transaction->action('select')->render() + $transaction->action('select')->render()[0] ); $this->assertSameSql( 'select "name" from (select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"', - $transaction->action('field', ['name'])->render() + $transaction->action('field', ['name'])->render()[0] ); $this->assertSameSql( 'select sum("cnt") from (select count(*) "cnt" from "invoice" UNION ALL select count(*) "cnt" from "payment") "derivedTable"', - $transaction->action('count')->render() + $transaction->action('count')->render()[0] ); $this->assertSameSql( 'select sum("val") from (select sum("amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"', - $transaction->action('fx', ['sum', 'amount'])->render() + $transaction->action('fx', ['sum', 'amount'])->render()[0] ); - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( 'select sum("val") from (select sum(-"amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"', - $transaction->action('fx', ['sum', 'amount'])->render() + $transaction->action('fx', ['sum', 'amount'])->render()[0] ); } - public function testActions2() + public function testActions2(): void { - $transaction = clone $this->transaction; + $transaction = $this->createTransaction(); $this->assertSame(5, (int) $transaction->action('count')->getOne()); $this->assertSame(37.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); } - public function testSubAction1() + public function testSubAction1(): void { - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( '(select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment") "derivedTable"', - $transaction->getSubAction('fx', ['sum', 'amount'])->render() + $transaction->getSubAction('fx', ['sum', 'amount'])->render()[0] ); } - public function testBasics() + public function testBasics(): void { - $client = clone $this->client; + $client = $this->createClient(); // There are total of 2 clients $this->assertSame(2, (int) $client->action('count')->getOne()); @@ -181,7 +162,7 @@ public function testBasics() $this->assertSame(19.0, (float) $client->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); - $transaction = clone $this->transaction; + $transaction = $this->createTransaction(); $this->assertSame([ ['name' => 'chair purchase', 'amount' => 4.0], @@ -192,7 +173,7 @@ public function testBasics() ], $transaction->export()); // Transaction is Union Model - $client->hasMany('Transaction', $transaction); + $client->hasMany('Transaction', ['model' => $transaction]); $this->assertSame([ ['name' => 'chair purchase', 'amount' => 4.0], @@ -200,11 +181,11 @@ public function testBasics() ['name' => 'prepay', 'amount' => 10.0], ], $client->ref('Transaction')->export()); - $client = clone $this->client; + $client = $this->createClient(); $client->load(1); - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSame([ ['name' => 'chair purchase', 'amount' => -4.0], @@ -215,7 +196,7 @@ public function testBasics() ], $transaction->export()); // Transaction is Union Model - $client->hasMany('Transaction', $transaction); + $client->hasMany('Transaction', ['model' => $transaction]); $this->assertSame([ ['name' => 'chair purchase', 'amount' => -4.0], @@ -224,45 +205,45 @@ public function testBasics() ], $client->ref('Transaction')->export()); } - public function testGrouping1() + public function testGrouping1(): void { - $transaction = clone $this->transaction; + $transaction = $this->createTransaction(); - $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); + $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( '(select "name" "name",sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"', - $transaction->getSubQuery(['name', 'amount'])->render() + $transaction->getSubQuery(['name', 'amount'])->render()[0] ); - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( '(select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"', - $transaction->getSubQuery(['name', 'amount'])->render() + $transaction->getSubQuery(['name', 'amount'])->render()[0] ); } - public function testGrouping2() + public function testGrouping2(): void { - $transaction = clone $this->transaction; + $transaction = $this->createTransaction(); - $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); + $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( 'select "name",sum("amount") "amount" from (select "name" "name",sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', - $transaction->action('select', [['name', 'amount']])->render() + $transaction->action('select', [['name', 'amount']])->render()[0] ); - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( 'select "name",sum("amount") "amount" from (select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', - $transaction->action('select', [['name', 'amount']])->render() + $transaction->action('select', [['name', 'amount']])->render()[0] ); } @@ -270,10 +251,10 @@ public function testGrouping2() * If all nested models have a physical field to which a grouped column can be mapped into, then we should group all our * sub-queries. */ - public function testGrouping3() + public function testGrouping3(): void { - $transaction = clone $this->transaction; - $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'money']]); + $transaction = $this->createTransaction(); + $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $transaction->setOrder('name'); $this->assertSame([ @@ -283,8 +264,8 @@ public function testGrouping3() ['name' => 'table purchase', 'amount' => 15.0], ], $transaction->export()); - $transaction = clone $this->subtractInvoiceTransaction; - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'money']]); + $transaction = $this->createSubtractInvoiceTransaction(); + $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $transaction->setOrder('name'); $this->assertSame([ @@ -299,30 +280,30 @@ public function testGrouping3() * If a nested model has a field defined through expression, it should be still used in grouping. We should test this * with both expressions based off the fields and static expressions (such as "blah"). */ - public function testSubGroupingByExpressions() + public function testSubGroupingByExpressions(): void { if ($this->getDatabasePlatform() instanceof OraclePlatform) { // TODO $this->markTestIncomplete('TODO - for some reasons Oracle does not accept the query'); } - $transaction = clone $this->transaction; + $transaction = $this->createTransaction(); $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->nestedPayment->addExpression('type', '\'payment\''); $transaction->addField('type'); - $transaction->groupBy('type', ['amount' => ['sum([amount])', 'type' => 'money']]); + $transaction->groupBy('type', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSame([ ['type' => 'invoice', 'amount' => 23.0], ['type' => 'payment', 'amount' => 14.0], ], $transaction->export(['type', 'amount'])); - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->nestedPayment->addExpression('type', '\'payment\''); $transaction->addField('type'); - $transaction->groupBy('type', ['amount' => ['sum([])', 'type' => 'money']]); + $transaction->groupBy('type', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSame([ ['type' => 'invoice', 'amount' => -23.0], @@ -330,10 +311,10 @@ public function testSubGroupingByExpressions() ], $transaction->export(['type', 'amount'])); } - public function testReference() + public function testReference(): void { - $client = clone $this->client; - $client->hasMany('tr', $this->createTransaction()); + $client = $this->createClient(); + $client->hasMany('tr', ['model' => $this->createTransaction()]); $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); @@ -342,11 +323,11 @@ public function testReference() $this->assertSameSql( 'select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = :a ' . 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"', - $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render() + $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); - $client = clone $this->client; - $client->hasMany('tr', $this->createSubtractInvoiceTransaction()); + $client = $this->createClient(); + $client->hasMany('tr', ['model' => $this->createSubtractInvoiceTransaction()]); $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); @@ -355,7 +336,7 @@ public function testReference() $this->assertSameSql( 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a ' . 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"', - $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render() + $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); } @@ -365,10 +346,10 @@ public function testReference() * * See also: http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 */ - public function testFieldAggregate() + public function testFieldAggregate(): void { - $client = clone $this->client; - $client->hasMany('tr', $this->createTransaction()) + $client = $this->createClient(); + $client->hasMany('tr', ['model' => $this->createTransaction()]) ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); $this->assertTrue(true); // fake assert @@ -376,9 +357,9 @@ public function testFieldAggregate() //$c->load(1); } - public function testConditionOnUnionField() + public function testConditionOnUnionField(): void { - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '<', 0); $this->assertSame([ @@ -388,9 +369,9 @@ public function testConditionOnUnionField() ], $transaction->export()); } - public function testConditionOnNestedModelField() + public function testConditionOnNestedModelField(): void { - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('client_id', '>', 1); $this->assertSame([ @@ -399,9 +380,9 @@ public function testConditionOnNestedModelField() ], $transaction->export()); } - public function testConditionForcedOnNestedModels1() + public function testConditionForcedOnNestedModels1(): void { - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '>', 5, true); $this->assertSame([ @@ -409,9 +390,9 @@ public function testConditionForcedOnNestedModels1() ], $transaction->export()); } - public function testConditionForcedOnNestedModels2() + public function testConditionForcedOnNestedModels2(): void { - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '<', -10, true); $this->assertSame([ @@ -419,9 +400,9 @@ public function testConditionForcedOnNestedModels2() ], $transaction->export()); } - public function testConditionExpression() + public function testConditionExpression(): void { - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition($transaction->expr('{} > 5', ['amount'])); $this->assertSame([ @@ -432,9 +413,9 @@ public function testConditionExpression() /** * Model's conditions can still be placed on the original field values. */ - public function testConditionOnMappedField() + public function testConditionOnMappedField(): void { - $transaction = clone $this->subtractInvoiceTransaction; + $transaction = $this->createSubtractInvoiceTransaction(); $transaction->nestedInvoice->addCondition('amount', 4); $this->assertSame([ diff --git a/tests/ReportTest.php b/tests/ReportTest.php index a125a7de9..23c3268a7 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -5,8 +5,9 @@ namespace Atk4\Data\Tests; use Atk4\Data\Model\Aggregate; +use Atk4\Data\Schema\TestCase; -class ReportTest extends \Atk4\Schema\PhpunitTestCase +class ReportTest extends TestCase { /** @var array */ private $init_db = @@ -26,23 +27,25 @@ class ReportTest extends \Atk4\Schema\PhpunitTestCase ], ]; - /** @var Aggregate */ - protected $invoiceAggregate; - protected function setUp(): void { parent::setUp(); - $this->setDB($this->init_db); + $this->setDb($this->init_db); + } + protected function createInvoiceAggregate(): Aggregate + { $invoice = new Model\Invoice($this->db); $invoice->getRef('client_id')->addTitle(); - $this->invoiceAggregate = new Aggregate($invoice); - $this->invoiceAggregate->addField('client'); + $invoiceAggregate = new Aggregate($invoice); + $invoiceAggregate->addField('client'); + + return $invoiceAggregate; } - public function testAliasGroupSelect() + public function testAliasGroupSelect(): void { - $invoiceAggregate = clone $this->invoiceAggregate; + $invoiceAggregate = $this->createInvoiceAggregate(); $invoiceAggregate->groupBy(['client_id'], ['c' => ['count(*)', 'type' => 'integer']]); From 5593643e6cb48a2a86562bbf334a587ef13f5777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 6 Jan 2022 01:04:36 +0100 Subject: [PATCH 062/151] fix assertions --- tests/ModelUnionTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index ff2014bfe..0cb7c04d3 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -73,7 +73,7 @@ public function testNestedQuery1(): void ); $this->assertSameSql( - '(select "name" "name","amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"', + '(select "name" "name", "amount" "amount" from "invoice" UNION ALL select "name" "name", "amount" "amount" from "payment") "derivedTable"', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); @@ -93,7 +93,7 @@ public function testMissingField(): void $transaction->addField('type'); $this->assertSameSql( - '(select (\'invoice\') "type","amount" "amount" from "invoice" UNION ALL select NULL "type","amount" "amount" from "payment") "derivedTable"', + '(select (\'invoice\') "type", "amount" "amount" from "invoice" UNION ALL select NULL "type", "amount" "amount" from "payment") "derivedTable"', $transaction->getSubQuery(['type', 'amount'])->render()[0] ); } @@ -103,7 +103,7 @@ public function testActions(): void $transaction = $this->createTransaction(); $this->assertSameSql( - 'select "name","amount" from (select "name" "name","amount" "amount" from "invoice" UNION ALL select "name" "name","amount" "amount" from "payment") "derivedTable"', + 'select "name", "amount" from (select "name" "name", "amount" "amount" from "invoice" UNION ALL select "name" "name", "amount" "amount" from "payment") "derivedTable"', $transaction->action('select')->render()[0] ); @@ -212,7 +212,7 @@ public function testGrouping1(): void $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( - '(select "name" "name",sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"', + '(select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable"', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); @@ -221,7 +221,7 @@ public function testGrouping1(): void $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( - '(select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable"', + '(select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable"', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); } @@ -233,7 +233,7 @@ public function testGrouping2(): void $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( - 'select "name",sum("amount") "amount" from (select "name" "name",sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', + 'select "name", sum("amount") "amount" from (select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', $transaction->action('select', [['name', 'amount']])->render()[0] ); @@ -242,7 +242,7 @@ public function testGrouping2(): void $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( - 'select "name",sum("amount") "amount" from (select "name" "name",sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name",sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', + 'select "name", sum("amount") "amount" from (select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', $transaction->action('select', [['name', 'amount']])->render()[0] ); } @@ -353,8 +353,8 @@ public function testFieldAggregate(): void ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); $this->assertTrue(true); // fake assert - //select "client"."id","client"."name",(select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1 - //$c->load(1); + //select "client"."id", "client"."name", (select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1 + //$client->load(1); } public function testConditionOnUnionField(): void From 55df20d83bba69c5cbb24e4eb7824e50aa4c6db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 6 Jan 2022 20:36:00 +0100 Subject: [PATCH 063/151] client_id in transaction model --- src/Model/Union.php | 1 + src/Reference.php | 2 +- tests/Model/Payment.php | 2 +- tests/Model/Transaction.php | 1 + tests/ModelUnionTest.php | 70 ++++++++++++++++++------------------- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index a5697b92a..3618d5171 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -318,6 +318,7 @@ public function getSubQuery(array $fields): Expression } // if we group we do not select non-aggregate fields + // TODO this breaks composide design - remove this if statement, fields must be manually removed or added to grouping! if (count($this->group) > 0 && !in_array($fieldName, $this->group, true) && !isset($this->aggregate[$fieldName])) { continue; } diff --git a/src/Reference.php b/src/Reference.php index fb4699319..4c5c8bf65 100644 --- a/src/Reference.php +++ b/src/Reference.php @@ -221,7 +221,7 @@ protected function initTableAlias(): void $ourModel = $this->getOurModel(null); $aliasFull = $this->link; - $alias = preg_replace('~_(' . preg_quote($ourModel->id_field, '~') . '|id)$~', '', $aliasFull); + $alias = preg_replace('~_(' . preg_quote($ourModel->id_field ?? '', '~') . '|id)$~', '', $aliasFull); $alias = preg_replace('~([0-9a-z]?)[0-9a-z]*[^0-9a-z]*~i', '$1', $alias); if ($ourModel->table_alias !== null) { $aliasFull = $ourModel->table_alias . '_' . $aliasFull; diff --git a/tests/Model/Payment.php b/tests/Model/Payment.php index ca69d6668..375510230 100644 --- a/tests/Model/Payment.php +++ b/tests/Model/Payment.php @@ -13,9 +13,9 @@ class Payment extends Model protected function init(): void { parent::init(); - $this->addField('name'); $this->hasOne('client_id', ['model' => [Client::class]]); + $this->addField('name'); $this->addField('amount', ['type' => 'atk4_money']); } } diff --git a/tests/Model/Transaction.php b/tests/Model/Transaction.php index 3255f792a..108693d36 100644 --- a/tests/Model/Transaction.php +++ b/tests/Model/Transaction.php @@ -27,6 +27,7 @@ protected function init(): void $this->addNestedModel($this->nestedPayment); // next, define common fields + $this->hasOne('client_id', ['model' => [Client::class]]); $this->addField('name'); $this->addField('amount', ['type' => 'atk4_money']); } diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 0cb7c04d3..0b1fae1ca 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -103,7 +103,7 @@ public function testActions(): void $transaction = $this->createTransaction(); $this->assertSameSql( - 'select "name", "amount" from (select "name" "name", "amount" "amount" from "invoice" UNION ALL select "name" "name", "amount" "amount" from "payment") "derivedTable"', + 'select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", "amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "derivedTable"', $transaction->action('select')->render()[0] ); @@ -158,51 +158,47 @@ public function testBasics(): void $this->assertSame(2, (int) $client->action('count')->getOne()); // Client with ID=1 has invoices for 19 - $client->load(1); - - $this->assertSame(19.0, (float) $client->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); $transaction = $this->createTransaction(); $this->assertSame([ - ['name' => 'chair purchase', 'amount' => 4.0], - ['name' => 'table purchase', 'amount' => 15.0], - ['name' => 'chair purchase', 'amount' => 4.0], - ['name' => 'prepay', 'amount' => 10.0], - ['name' => 'full pay', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); // Transaction is Union Model $client->hasMany('Transaction', ['model' => $transaction]); $this->assertSame([ - ['name' => 'chair purchase', 'amount' => 4.0], - ['name' => 'table purchase', 'amount' => 15.0], - ['name' => 'prepay', 'amount' => 10.0], - ], $client->ref('Transaction')->export()); + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ], $client->load(1)->ref('Transaction')->export()); $client = $this->createClient(); - $client->load(1); - $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'table purchase', 'amount' => -15.0], - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'prepay', 'amount' => 10.0], - ['name' => 'full pay', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); // Transaction is Union Model $client->hasMany('Transaction', ['model' => $transaction]); $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'table purchase', 'amount' => -15.0], - ['name' => 'prepay', 'amount' => 10.0], - ], $client->ref('Transaction')->export()); + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ], $client->load(1)->ref('Transaction')->export()); } public function testGrouping1(): void @@ -254,6 +250,7 @@ public function testGrouping2(): void public function testGrouping3(): void { $transaction = $this->createTransaction(); + $transaction->removeField('client_id'); $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $transaction->setOrder('name'); @@ -265,6 +262,7 @@ public function testGrouping3(): void ], $transaction->export()); $transaction = $this->createSubtractInvoiceTransaction(); + $transaction->removeField('client_id'); $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $transaction->setOrder('name'); @@ -363,9 +361,9 @@ public function testConditionOnUnionField(): void $transaction->addCondition('amount', '<', 0); $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'table purchase', 'amount' => -15.0], - ['name' => 'chair purchase', 'amount' => -4.0], + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], ], $transaction->export()); } @@ -375,8 +373,8 @@ public function testConditionOnNestedModelField(): void $transaction->addCondition('client_id', '>', 1); $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'full pay', 'amount' => 4.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); } @@ -386,7 +384,7 @@ public function testConditionForcedOnNestedModels1(): void $transaction->addCondition('amount', '>', 5, true); $this->assertSame([ - ['name' => 'prepay', 'amount' => 10.0], + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ], $transaction->export()); } @@ -396,7 +394,7 @@ public function testConditionForcedOnNestedModels2(): void $transaction->addCondition('amount', '<', -10, true); $this->assertSame([ - ['name' => 'table purchase', 'amount' => -15.0], + ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ], $transaction->export()); } @@ -406,7 +404,7 @@ public function testConditionExpression(): void $transaction->addCondition($transaction->expr('{} > 5', ['amount'])); $this->assertSame([ - ['name' => 'prepay', 'amount' => 10.0], + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ], $transaction->export()); } @@ -419,10 +417,10 @@ public function testConditionOnMappedField(): void $transaction->nestedInvoice->addCondition('amount', 4); $this->assertSame([ - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'chair purchase', 'amount' => -4.0], - ['name' => 'prepay', 'amount' => 10.0], - ['name' => 'full pay', 'amount' => 4.0], + ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], + ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], + ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], + ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); } } From 92654fc7b80f5321bd0eb57f8d7e404fc67e3546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 6 Jan 2022 15:38:27 +0100 Subject: [PATCH 064/151] skip tests with wrong fields pushdown --- src/Model/Union.php | 3 ++- tests/ModelUnionTest.php | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index 3618d5171..f3a18934d 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -265,9 +265,10 @@ public function action($mode, $args = []) break; case 'fx': + case 'fx0': $args['alias'] = 'val'; - $subquery = $this->getSubAction('fx', $args); + $subquery = $this->getSubAction($mode, $args); $args = [$args[0], $this->expr('{}', ['val'])]; diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 0b1fae1ca..ae52d163b 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -316,6 +316,14 @@ public function testReference(): void $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); + + // TODO aggregated fields are pushdown, but where condition is not + // I belive the fields pushdown is even wrong as not every aggregated result produces same result when aggregated again + // then fix also self::testFieldAggregate() + $this->assertTrue(true); + + return; + // @phpstan-ignore-next-line $this->assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( @@ -350,9 +358,15 @@ public function testFieldAggregate(): void $client->hasMany('tr', ['model' => $this->createTransaction()]) ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); - $this->assertTrue(true); // fake assert - //select "client"."id", "client"."name", (select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1 - //$client->load(1); + // TODO some fields are pushdown, but some not, same issue as in self::testReference() + $this->assertTrue(true); + + return; + // @phpstan-ignore-next-line + $this->assertSameSql( + 'select "client"."id", "client"."name", (select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1', + $client->load(1)->action('select')->render()[0] + ); } public function testConditionOnUnionField(): void From f403b821cb1125c42cbce20be1a9f5a1ae993cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 6 Jan 2022 17:54:38 +0100 Subject: [PATCH 065/151] fix grouping for PostgreSQL --- src/Model/Aggregate.php | 2 +- src/Model/Union.php | 2 +- tests/ModelUnionTest.php | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 0feac147e..78c6b0110 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -234,7 +234,7 @@ protected function initQueryGrouping(Query $query): void foreach ($this->groupByFields as $field) { if ($this->baseModel->hasField($field)) { - $expression = $this->baseModel->getField($field); + $expression = $this->baseModel->getField($field)->short_name /* TODO short_name should be used by DSQL automatically when in GROUP BY, HAVING, ... */; } else { $expression = $this->expr($field); } diff --git a/src/Model/Union.php b/src/Model/Union.php index f3a18934d..487c3e3cc 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -362,7 +362,7 @@ public function getSubQuery(array $fields): Expression if (isset($fieldMap[$group])) { $query->group($nestedModel->expr($fieldMap[$group])); } elseif ($nestedModel->hasField($group)) { - $query->group($nestedModel->getField($group)); + $query->group($nestedModel->getField($group)->short_name /* TODO short_name should be used by DSQL automatically when in GROUP BY, HAVING, ... */); } } diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index ae52d163b..22c9c9245 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -291,6 +291,11 @@ public function testSubGroupingByExpressions(): void $transaction->groupBy('type', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $this->assertSameSql( + 'select "client_id", "name", "type", sum("amount") "amount" from (select (\'invoice\') "type", sum("amount") "amount" from "invoice" group by "type" UNION ALL select (\'payment\') "type", sum("amount") "amount" from "payment" group by "type") "derivedTable" group by "type"', + $transaction->action('select')->render()[0] + ); + $this->assertSame([ ['type' => 'invoice', 'amount' => 23.0], ['type' => 'payment', 'amount' => 14.0], From 98cec38a8b9b2e19daf17c93d1fcb37918b401b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 6 Jan 2022 21:20:13 +0100 Subject: [PATCH 066/151] fix report test --- tests/ReportTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ReportTest.php b/tests/ReportTest.php index 23c3268a7..7fadc553b 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -13,7 +13,8 @@ class ReportTest extends TestCase private $init_db = [ 'client' => [ - ['name' => 'Vinny'], + // allow of migrator to create all columns + ['name' => 'Vinny', 'surname' => null, 'order' => null], ['name' => 'Zoe'], ], 'invoice' => [ From 564944cafde0564b96c670efa3b64aa9cb26f904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 2 Jan 2022 22:05:02 +0100 Subject: [PATCH 067/151] fix/use non-aliased field name for update/delete where --- src/Persistence/Sql.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 202af53db..577a637a8 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -589,7 +589,7 @@ public function update(Model $model, $id, array $data): void // only apply fields that has been modified $update->setMulti($this->typecastSaveRow($model, $data)); - $update->where($model->getField($model->id_field), $id); + $update->where($model->getField($model->id_field)->getPersistenceName(), $id); $st = null; try { @@ -635,7 +635,7 @@ public function delete(Model $model, $id): void $delete = $this->initQuery($model); $delete->mode('delete'); - $delete->where($model->getField($model->id_field), $id); + $delete->where($model->getField($model->id_field)->getPersistenceName(), $id); $model->hook(self::HOOK_BEFORE_DELETE_QUERY, [$delete]); try { From 735332297f2d752cea1e9dbfb090e1712f5d2356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 4 Jan 2022 05:04:42 +0100 Subject: [PATCH 068/151] fix id typecasting in SQL persistence --- src/Persistence/Sql.php | 68 +++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 577a637a8..5e632bd1c 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -136,10 +136,7 @@ public function add(Model $model, array $defaults = []): void // When we work without table, we can't have any IDs if ($model->table === false) { $model->removeField($model->id_field); - $model->addExpression($model->id_field, '1'); - //} else { - // SQL databases use ID of int by default - //$m->getField($m->id_field)->type = 'integer'; + $model->addExpression($model->id_field, '-1'); } } @@ -514,14 +511,22 @@ public function tryLoad(Model $model, $id): ?array */ public function insert(Model $model, array $data): string { + if ($model->id_field) { + $dataId = $data[$model->id_field] ?? null; + if ($dataId === null) { + unset($data[$model->id_field]); + } + } else { + $dataId = null; + } + + $dataRaw = $this->typecastSaveRow($model, $data); + unset($data); + $insert = $this->initQuery($model); $insert->mode('insert'); - if ($model->id_field && ($data[$model->id_field] ?? null) === null) { - unset($data[$model->id_field]); - } - - $insert->setMulti($this->typecastSaveRow($model, $data)); + $insert->setMulti($dataRaw); $st = null; try { @@ -533,10 +538,14 @@ public function insert(Model $model, array $data): string ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } - if ($model->id_field && ($data[$model->id_field] ?? null) !== null) { - $id = (string) $data[$model->id_field]; + if ($model->id_field) { + if ($dataId !== null) { + $id = (string) $dataId; + } else { + $id = $this->lastInsertId($model); + } } else { - $id = $this->lastInsertId($model); + $id = ''; } $model->hook(self::HOOK_AFTER_INSERT_QUERY, [$insert, $st]); @@ -580,38 +589,48 @@ public function prepareIterator(Model $model): \Traversable */ public function update(Model $model, $id, array $data): void { - if (!$model->id_field) { + $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); + + $dataId = $data[$model->id_field] ?? null; + + if (!$model->id_field || $idRaw === null || (array_key_exists($model->id_field, $data) && $dataId === null)) { throw new Exception('id_field of a model is not set. Unable to update record.'); } + $dataRaw = $this->typecastSaveRow($model, $data); + unset($data); + + if (count($dataRaw) === 0) { + return; + } + $update = $this->initQuery($model); $update->mode('update'); // only apply fields that has been modified - $update->setMulti($this->typecastSaveRow($model, $data)); - $update->where($model->getField($model->id_field)->getPersistenceName(), $id); + $update->setMulti($dataRaw); + $update->where($model->getField($model->id_field)->getPersistenceName(), $idRaw); $st = null; try { $model->hook(self::HOOK_BEFORE_UPDATE_QUERY, [$update]); - if ($data) { - $st = $update->execute(); - } + $st = $update->execute(); } catch (SqlException $e) { throw (new Exception('Unable to update due to query error', 0, $e)) ->addMoreInfo('model', $model) ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } - if (isset($data[$model->id_field]) && $model->getDirtyRef()[$model->id_field]) { + if ($dataId !== null && $model->getDirtyRef()[$model->id_field]) { // ID was changed - $model->setId($data[$model->id_field]); + $model->setId($dataId); } $model->hook(self::HOOK_AFTER_UPDATE_QUERY, [$update, $st]); // if any rows were updated in database, and we had expressions, reload - if ($model->reload_after_save === true && (!$st || $st->rowCount())) { + if ($model->reload_after_save === true && $st->rowCount()) { $d = $model->getDirtyRef(); $model->reload(); \Closure::bind(function () use ($model) { @@ -629,13 +648,16 @@ public function update(Model $model, $id, array $data): void */ public function delete(Model $model, $id): void { - if (!$model->id_field) { + $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); + + if (!$model->id_field || $idRaw === null) { throw new Exception('id_field of a model is not set. Unable to delete record.'); } $delete = $this->initQuery($model); $delete->mode('delete'); - $delete->where($model->getField($model->id_field)->getPersistenceName(), $id); + $delete->where($model->getField($model->id_field)->getPersistenceName(), $idRaw); $model->hook(self::HOOK_BEFORE_DELETE_QUERY, [$delete]); try { From e0af1c5da4c074387097e1d76a8eda954887dd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 4 Jan 2022 18:44:33 +0100 Subject: [PATCH 069/151] dedup/impl insert/update/delete methods in main Persistence --- phpstan.neon.dist | 3 - src/Persistence.php | 83 ++++++++++++++++++++++ src/Persistence/Array_/Join.php | 4 +- src/Persistence/Sql.php | 122 +++++++++++--------------------- 4 files changed, 126 insertions(+), 86 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d5ac358ab..3adbf85c1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -45,11 +45,8 @@ parameters: # for src/Field/SqlExpressionField.php - '~^Call to an undefined method Atk4\\Data\\Model::expr\(\)\.$~' # for src/Model.php - - '~^Call to an undefined method Atk4\\Data\\Persistence::update\(\)\.$~' - - '~^Call to an undefined method Atk4\\Data\\Persistence::insert\(\)\.$~' - '~^Call to an undefined method Atk4\\Data\\Persistence::export\(\)\.$~' - '~^Call to an undefined method Atk4\\Data\\Persistence::prepareIterator\(\)\.$~' - - '~^Call to an undefined method Atk4\\Data\\Persistence::delete\(\)\.$~' - '~^Call to an undefined method Atk4\\Data\\Persistence::action\(\)\.$~' # for src/Model/ReferencesTrait.php (in context of class Atk4\Data\Model) - '~^Call to an undefined method Atk4\\Data\\Reference::refLink\(\)\.$~' diff --git a/src/Persistence.php b/src/Persistence.php index 63768e5d9..518ed5cf1 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -147,6 +147,89 @@ public function load(Model $model, $id): array return $data; } + /** + * Inserts record in database and returns new record ID. + * + * @return mixed + */ + public function insert(Model $model, array $data) + { + if ($model->id_field && array_key_exists($model->id_field, $data) && $data[$model->id_field] === null) { + unset($data[$model->id_field]); + } + + $dataRaw = $this->typecastSaveRow($model, $data); + unset($data); + + $idRaw = $this->insertRaw($model, $dataRaw); + $id = $model->id_field ? $this->typecastLoadField($model->getField($model->id_field), $idRaw) : new \stdClass(); + + return $id; + } + + /** + * @return mixed + */ + protected function insertRaw(Model $model, array $dataRaw) + { + throw new Exception('Insert is not supported.'); + } + + /** + * Updates record in database. + * + * @param mixed $id + */ + public function update(Model $model, $id, array $data): void + { + $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); + if ($idRaw === null || (array_key_exists($model->id_field, $data) && $data[$model->id_field] === null)) { + throw new Exception('Model id_field is not set. Unable to update record.'); + } + + $dataRaw = $this->typecastSaveRow($model, $data); + unset($data); + + if (count($dataRaw) === 0) { + return; + } + + $this->updateRaw($model, $idRaw, $dataRaw); + } + + /** + * @param mixed $idRaw + */ + protected function updateRaw(Model $model, $idRaw, array $dataRaw): void + { + throw new Exception('Update is not supported.'); + } + + /** + * Deletes record from database. + * + * @param mixed $id + */ + public function delete(Model $model, $id): void + { + $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); + if ($idRaw === null) { + throw new Exception('Model id_field is not set. Unable to delete record.'); + } + + $this->deleteRaw($model, $idRaw); + } + + /** + * @param mixed $idRaw + */ + protected function deleteRaw(Model $model, $idRaw): void + { + throw new Exception('Delete is not supported.'); + } + /** * Will convert one row of data from native PHP types into * persistence types. This will also take care of the "actual" diff --git a/src/Persistence/Array_/Join.php b/src/Persistence/Array_/Join.php index 9aab16a2b..4d2fcd3db 100644 --- a/src/Persistence/Array_/Join.php +++ b/src/Persistence/Array_/Join.php @@ -114,11 +114,11 @@ public function beforeUpdate(Model $entity, array &$data): void $persistence = $this->persistence ?: $this->getOwner()->persistence; + // @phpstan-ignore-next-line TODO this cannot work, Persistence::update() returns void $this->setId($entity, $persistence->update( $this->makeFakeModelWithForeignTable(), $this->getId($entity), - $this->getAndUnsetSaveBuffer($entity), - $this->foreign_table + $this->getAndUnsetSaveBuffer($entity) )); } diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 5e632bd1c..1f0a4c9bd 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -37,6 +37,8 @@ class Sql extends Persistence public const HOOK_AFTER_UPDATE_QUERY = self::class . '@afterUpdateQuery'; /** @const string */ public const HOOK_BEFORE_DELETE_QUERY = self::class . '@beforeDeleteQuery'; + /** @const string */ + public const HOOK_AFTER_DELETE_QUERY = self::class . '@afterDeleteQuery'; /** @var Connection Connection object. */ public $connection; @@ -506,53 +508,6 @@ public function tryLoad(Model $model, $id): ?array return $data; } - /** - * Inserts record in database and returns new record ID. - */ - public function insert(Model $model, array $data): string - { - if ($model->id_field) { - $dataId = $data[$model->id_field] ?? null; - if ($dataId === null) { - unset($data[$model->id_field]); - } - } else { - $dataId = null; - } - - $dataRaw = $this->typecastSaveRow($model, $data); - unset($data); - - $insert = $this->initQuery($model); - $insert->mode('insert'); - - $insert->setMulti($dataRaw); - - $st = null; - try { - $model->hook(self::HOOK_BEFORE_INSERT_QUERY, [$insert]); - $st = $insert->execute(); - } catch (SqlException $e) { - throw (new Exception('Unable to execute insert query', 0, $e)) - ->addMoreInfo('model', $model) - ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); - } - - if ($model->id_field) { - if ($dataId !== null) { - $id = (string) $dataId; - } else { - $id = $this->lastInsertId($model); - } - } else { - $id = ''; - } - - $model->hook(self::HOOK_AFTER_INSERT_QUERY, [$insert, $st]); - - return $id; - } - /** * Export all DataSet. */ @@ -582,29 +537,39 @@ public function prepareIterator(Model $model): \Traversable } } - /** - * Updates record in database. - * - * @param mixed $id - */ - public function update(Model $model, $id, array $data): void + protected function insertRaw(Model $model, array $dataRaw) { - $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; - unset($id); + $insert = $this->initQuery($model); + $insert->mode('insert'); - $dataId = $data[$model->id_field] ?? null; + $insert->setMulti($dataRaw); - if (!$model->id_field || $idRaw === null || (array_key_exists($model->id_field, $data) && $dataId === null)) { - throw new Exception('id_field of a model is not set. Unable to update record.'); + $st = null; + try { + $model->hook(self::HOOK_BEFORE_INSERT_QUERY, [$insert]); + $st = $insert->execute(); + } catch (SqlException $e) { + throw (new Exception('Unable to execute insert query', 0, $e)) + ->addMoreInfo('model', $model) + ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } - $dataRaw = $this->typecastSaveRow($model, $data); - unset($data); - - if (count($dataRaw) === 0) { - return; + if ($model->id_field) { + $idRaw = $dataRaw[$model->getField($model->id_field)->getPersistenceName()] ?? null; + if ($idRaw === null) { + $idRaw = $this->lastInsertId($model); + } + } else { + $idRaw = ''; } + $model->hook(self::HOOK_AFTER_INSERT_QUERY, [$insert, $st]); + + return $idRaw; + } + + protected function updateRaw(Model $model, $idRaw, array $dataRaw): void + { $update = $this->initQuery($model); $update->mode('update'); @@ -612,9 +577,10 @@ public function update(Model $model, $id, array $data): void $update->setMulti($dataRaw); $update->where($model->getField($model->id_field)->getPersistenceName(), $idRaw); + $model->hook(self::HOOK_BEFORE_UPDATE_QUERY, [$update]); + $st = null; try { - $model->hook(self::HOOK_BEFORE_UPDATE_QUERY, [$update]); $st = $update->execute(); } catch (SqlException $e) { throw (new Exception('Unable to update due to query error', 0, $e)) @@ -622,9 +588,13 @@ public function update(Model $model, $id, array $data): void ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } - if ($dataId !== null && $model->getDirtyRef()[$model->id_field]) { - // ID was changed - $model->setId($dataId); + if ($model->id_field) { + $newIdRaw = $dataRaw[$model->getField($model->id_field)->getPersistenceName()] ?? null; + if ($newIdRaw !== null && $model->getDirtyRef()[$model->id_field]) { + // ID was changed + // TODO this cannot work with entity + $model->setId($this->typecastLoadField($model->getField($model->id_field), $newIdRaw)); + } } $model->hook(self::HOOK_AFTER_UPDATE_QUERY, [$update, $st]); @@ -641,32 +611,22 @@ public function update(Model $model, $id, array $data): void } } - /** - * Deletes record from database. - * - * @param mixed $id - */ - public function delete(Model $model, $id): void + protected function deleteRaw(Model $model, $idRaw): void { - $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; - unset($id); - - if (!$model->id_field || $idRaw === null) { - throw new Exception('id_field of a model is not set. Unable to delete record.'); - } - $delete = $this->initQuery($model); $delete->mode('delete'); $delete->where($model->getField($model->id_field)->getPersistenceName(), $idRaw); $model->hook(self::HOOK_BEFORE_DELETE_QUERY, [$delete]); try { - $delete->execute(); + $st = $delete->execute(); } catch (SqlException $e) { throw (new Exception('Unable to delete due to query error', 0, $e)) ->addMoreInfo('model', $model) ->addMoreInfo('scope', $model->getModel(true)->scope()->toWords()); } + + $model->hook(self::HOOK_AFTER_DELETE_QUERY, [$delete, $st]); } public function getFieldSqlExpression(Field $field, Expression $expression): Expression From af5333b0838430459c4db058d12bdd9fd56a051d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 4 Jan 2022 19:49:45 +0100 Subject: [PATCH 070/151] impl raw write methods --- src/Persistence/Array_.php | 38 ++++++++------------------------------ src/Persistence/Csv.php | 27 +++++---------------------- 2 files changed, 13 insertions(+), 52 deletions(-) diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index 62f897d47..cda1734a7 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -222,51 +222,29 @@ public function tryLoad(Model $model, $id): ?array return $this->typecastLoadRow($model, $this->filterRowDataOnlyModelFields($model, $row->getData())); } - /** - * Inserts record in data array and returns new record ID. - * - * @return mixed - */ - public function insert(Model $model, array $data) + protected function insertRaw(Model $model, array $dataRaw) { $this->seedData($model); - if ($model->id_field && ($data[$model->id_field] ?? null) === null) { - unset($data[$model->id_field]); - } - $data = $this->typecastSaveRow($model, $data); - - $id = $data[$model->id_field] ?? $this->generateNewId($model); + $idRaw = $dataRaw[$model->id_field] ?? $this->generateNewId($model); - $this->saveRow($model, $data, $id); + $this->saveRow($model, $dataRaw, $idRaw); - return $id; + return $idRaw; } - /** - * Updates record in data array and returns record ID. - * - * @param mixed $id - */ - public function update(Model $model, $id, array $data): void + protected function updateRaw(Model $model, $idRaw, array $dataRaw): void { $table = $this->seedDataAndGetTable($model); - $data = $this->typecastSaveRow($model, $data); - - $this->saveRow($model, array_merge($this->filterRowDataOnlyModelFields($model, $table->getRowById($model, $id)->getData()), $data), $id); + $this->saveRow($model, array_merge($this->filterRowDataOnlyModelFields($model, $table->getRowById($model, $idRaw)->getData()), $dataRaw), $idRaw); } - /** - * Deletes record in data array. - * - * @param mixed $id - */ - public function delete(Model $model, $id): void + protected function deleteRaw(Model $model, $idRaw): void { $table = $this->seedDataAndGetTable($model); - $table->deleteRow($table->getRowById($model, $id)); + $table->deleteRow($table->getRowById($model, $idRaw)); } /** diff --git a/src/Persistence/Csv.php b/src/Persistence/Csv.php index 095429ff2..54f560d51 100644 --- a/src/Persistence/Csv.php +++ b/src/Persistence/Csv.php @@ -261,12 +261,7 @@ public function prepareIterator(Model $model): \Traversable } } - /** - * Inserts record in data array and returns new record ID. - * - * @return mixed - */ - public function insert(Model $model, array $data) + protected function insertRaw(Model $model, array $dataRaw) { if (!$this->mode) { $this->mode = 'w'; @@ -274,8 +269,6 @@ public function insert(Model $model, array $data) throw new Exception('Currently reading records, so writing is not possible.'); } - $data = $this->typecastSaveRow($model, $data); - if (!$this->handle) { $this->saveHeader($model->getModel(true)); } @@ -283,30 +276,20 @@ public function insert(Model $model, array $data) $line = []; foreach ($this->header as $name) { - $line[] = $data[$name]; + $line[] = $dataRaw[$name]; } $this->putLine($line); - return $model->id_field ? $data[$model->id_field] : null; + return $model->id_field ? $dataRaw[$model->id_field] : null; } - /** - * Updates record in data array and returns record ID. - * - * @param mixed $id - */ - public function update(Model $model, $id, array $data): void + protected function updateRaw(Model $model, $idRaw, array $dataRaw): void { throw new Exception('Updating records is not supported in CSV persistence.'); } - /** - * Deletes record in data array. - * - * @param mixed $id - */ - public function delete(Model $model, $id): void + protected function deleteRaw(Model $model, $idRaw): void { throw new Exception('Deleting records is not supported in CSV persistence.'); } From 9dd61ba981eed83b594d1994b766a8bf200eb4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 17:44:11 +0100 Subject: [PATCH 071/151] add support for model nesting for SQL --- src/Model.php | 2 +- src/Persistence/Sql.php | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Model.php b/src/Model.php index f9e748f8f..cc069e953 100644 --- a/src/Model.php +++ b/src/Model.php @@ -120,7 +120,7 @@ class Model implements \IteratorAggregate * model normally lives. The interpretation of the table will be decoded * by persistence driver. * - * @var string|false + * @var string|self|false */ public $table; diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 1f0a4c9bd..d98ce2508 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -206,7 +206,10 @@ public function initQuery(Model $model): Query $query = $model->persistence_data['dsql'] = $this->dsql(); if ($model->table) { - $query->table($model->table, $model->table_alias ?? null); + $query->table( + is_object($model->table) ? $model->table->action('select') : $model->table, + $model->table_alias ?? (is_object($model->table) ? '__inner__' : null) + ); } // add With cursors @@ -636,7 +639,7 @@ public function getFieldSqlExpression(Field $field, Expression $expression): Exp $prop = [ $field->hasJoin() ? ($field->getJoin()->foreign_alias ?: $field->getJoin()->short_name) - : ($field->getOwner()->table_alias ?: $field->getOwner()->table), + : ($field->getOwner()->table_alias ?: (is_object($field->getOwner()->table) ? '__inner__' : $field->getOwner()->table)), $field->getPersistenceName(), ]; } else { @@ -658,6 +661,10 @@ public function getFieldSqlExpression(Field $field, Expression $expression): Exp public function lastInsertId(Model $model): string { + if (is_object($model->table)) { + return $model->table->persistence->lastInsertId($model->table); + } + // PostgreSQL and Oracle DBAL platforms use sequence internally for PK autoincrement, // use default name if not set explicitly $sequenceName = null; From ddc2db3d05641a2b67d197bfc637d4c034ff03b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 17:49:01 +0100 Subject: [PATCH 072/151] fix join/hasMany --- src/Model/Join.php | 16 +++++++++++++--- src/Reference/HasMany.php | 11 ++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Model/Join.php b/src/Model/Join.php index 959d0847b..f5da760d3 100644 --- a/src/Model/Join.php +++ b/src/Model/Join.php @@ -182,6 +182,15 @@ static function (Model $entity) use ($name): self { ); } + private function getModelTableString(Model $model): string + { + if (is_object($model->table)) { + return $this->getModelTableString($model->table); + } + + return $model->table; + } + /** * Will use either foreign_alias or create #join_. */ @@ -204,7 +213,8 @@ protected function init(): void if ($this->reverse === true) { if ($this->master_field && $this->master_field !== $id_field) { // TODO not implemented yet, see https://github.com/atk4/data/issues/803 throw (new Exception('Joining tables on non-id fields is not implemented yet')) - ->addMoreInfo('condition', $this->getOwner()->table . '.' . $this->master_field . ' = ' . $this->foreign_table . '.' . $this->foreign_field); + ->addMoreInfo('master_field', $this->master_field) + ->addMoreInfo('id_field', $this->id_field); } if (!$this->master_field) { @@ -212,7 +222,7 @@ protected function init(): void } if (!$this->foreign_field) { - $this->foreign_field = $this->getOwner()->table . '_' . $id_field; + $this->foreign_field = $this->getModelTableString($this->getOwner()) . '_' . $id_field; } } else { $this->reverse = false; @@ -310,7 +320,7 @@ public function hasMany(string $link, array $defaults = []) { $defaults = array_merge([ 'our_field' => $this->id_field, - 'their_field' => $this->getOwner()->table . '_' . $this->id_field, + 'their_field' => $this->getModelTableString($this->getOwner()) . '_' . $this->id_field, ], $defaults); return $this->getOwner()->hasMany($link, $defaults); diff --git a/src/Reference/HasMany.php b/src/Reference/HasMany.php index 5a8c1d2c6..131709bab 100644 --- a/src/Reference/HasMany.php +++ b/src/Reference/HasMany.php @@ -11,6 +11,15 @@ class HasMany extends Reference { + private function getModelTableString(Model $model): string + { + if (is_object($model->table)) { + return $this->getModelTableString($model->table); + } + + return $model->table; + } + public function getTheirFieldName(): string { if ($this->their_field) { @@ -20,7 +29,7 @@ public function getTheirFieldName(): string // this is pure guess, verify if such field exist, otherwise throw // TODO probably remove completely in the future $ourModel = $this->getOurModel(null); - $theirFieldName = $ourModel->table . '_' . $ourModel->id_field; + $theirFieldName = $this->getModelTableString($ourModel) . '_' . $ourModel->id_field; if (!$this->createTheirModel()->hasField($theirFieldName)) { throw (new Exception('Their model does not contain fallback field')) ->addMoreInfo('their_fallback_field', $theirFieldName); From bcd74c5d7bb0d022e73644ad82490e22dba4d551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 4 Jan 2022 15:55:15 +0100 Subject: [PATCH 073/151] impl write queries using nested Models --- src/Persistence.php | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Persistence.php b/src/Persistence.php index 518ed5cf1..1a32b1e5f 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -116,6 +116,18 @@ public function getDatabasePlatform(): Platforms\AbstractPlatform return new Persistence\GenericPlatform(); } + private function assertSameIdField(Model $model): void + { + $modelIdFieldName = $model->getField($model->id_field)->getPersistenceName(); + $tableIdFieldName = $model->table->id_field; + + if ($modelIdFieldName !== $tableIdFieldName) { + throw (new Exception('Table model with different ID field persistence name is not supported')) + ->addMoreInfo('model_id_field', $modelIdFieldName) + ->addMoreInfo('table_id_field', $tableIdFieldName); + } + } + /** * Tries to load data record, but will not fail if record can't be loaded. * @@ -161,6 +173,12 @@ public function insert(Model $model, array $data) $dataRaw = $this->typecastSaveRow($model, $data); unset($data); + if (is_object($model->table)) { + $this->assertSameIdField($model); + + return $model->table->insert($model->table->persistence->typecastLoadRow($model->table, $dataRaw)); + } + $idRaw = $this->insertRaw($model, $dataRaw); $id = $model->id_field ? $this->typecastLoadField($model->getField($model->id_field), $idRaw) : new \stdClass(); @@ -183,7 +201,6 @@ protected function insertRaw(Model $model, array $dataRaw) public function update(Model $model, $id, array $data): void { $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; - unset($id); if ($idRaw === null || (array_key_exists($model->id_field, $data) && $data[$model->id_field] === null)) { throw new Exception('Model id_field is not set. Unable to update record.'); } @@ -195,6 +212,16 @@ public function update(Model $model, $id, array $data): void return; } + if (is_object($model->table)) { + $this->assertSameIdField($model); + + $model->table->load($id)->save($model->table->persistence->typecastLoadRow($model->table, $dataRaw)); + + return; + } + + unset($id); + $this->updateRaw($model, $idRaw, $dataRaw); } @@ -214,11 +241,20 @@ protected function updateRaw(Model $model, $idRaw, array $dataRaw): void public function delete(Model $model, $id): void { $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; - unset($id); if ($idRaw === null) { throw new Exception('Model id_field is not set. Unable to delete record.'); } + if (is_object($model->table)) { + $this->assertSameIdField($model); + + $model->table->delete($id); + + return; + } + + unset($id); + $this->deleteRaw($model, $idRaw); } From a4438ea0a8ae3c9ac95313b3b0a2a98de25a304e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 17:59:21 +0100 Subject: [PATCH 074/151] use "_tm" as default alias for model-in-model table --- src/Persistence/Sql.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index d98ce2508..d0d36ffdd 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -208,7 +208,7 @@ public function initQuery(Model $model): Query if ($model->table) { $query->table( is_object($model->table) ? $model->table->action('select') : $model->table, - $model->table_alias ?? (is_object($model->table) ? '__inner__' : null) + $model->table_alias ?? (is_object($model->table) ? '_tm' : null) ); } @@ -639,7 +639,7 @@ public function getFieldSqlExpression(Field $field, Expression $expression): Exp $prop = [ $field->hasJoin() ? ($field->getJoin()->foreign_alias ?: $field->getJoin()->short_name) - : ($field->getOwner()->table_alias ?: (is_object($field->getOwner()->table) ? '__inner__' : $field->getOwner()->table)), + : ($field->getOwner()->table_alias ?? (is_object($field->getOwner()->table) ? '_tm' : $field->getOwner()->table)), $field->getPersistenceName(), ]; } else { From 79b2743ea601068544d75aaaed0a5537a4ce91eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 23:44:05 +0100 Subject: [PATCH 075/151] drop unneeded composer requirements --- composer.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/composer.json b/composer.json index 5d5f6c1f4..c12ef94f1 100644 --- a/composer.json +++ b/composer.json @@ -35,16 +35,12 @@ "homepage": "https://github.com/atk4/data", "require": { "php": ">=7.4 <8.2", - "ext-intl": "*", - "ext-pdo": "*", "atk4/core": "dev-develop", "doctrine/dbal": "^2.13.5 || ^3.2", "mvorisek/atk4-hintable": "~1.7.1" }, "require-release": { "php": ">=7.4 <8.2", - "ext-intl": "*", - "ext-pdo": "*", "atk4/core": "~3.2.0", "doctrine/dbal": "^2.13.5 || ^3.2", "mvorisek/atk4-hintable": "~1.7.1" From d5337a0ec05a3e537bd20cb37f2c02c1c2815778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 6 Jan 2022 17:49:28 +0100 Subject: [PATCH 076/151] fix/skip one test /w constant column for MSSQL --- src/Schema/TestCase.php | 3 ++- tests/ModelUnionTest.php | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index e762d9bc6..a98d6f146 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -12,6 +12,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Platforms\SQLServerPlatform; use Doctrine\DBAL\Schema\AbstractSchemaManager; abstract class TestCase extends BaseTestCase @@ -117,7 +118,7 @@ function ($matches) use ($platform) { return $platform->quoteSingleIdentifier($str); } - return $platform->quoteStringLiteral($str); + return ($platform instanceof SQLServerPlatform ? 'N' : '') . $platform->quoteStringLiteral($str); }, $sql ); diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 22c9c9245..800775cf5 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -6,6 +6,7 @@ use Atk4\Data\Schema\TestCase; use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Platforms\SQLServerPlatform; class ModelUnionTest extends TestCase { @@ -296,6 +297,10 @@ public function testSubGroupingByExpressions(): void $transaction->action('select')->render()[0] ); + if ($this->getDatabasePlatform() instanceof SQLServerPlatform) { + $this->markTestIncomplete('TODO MSSQL: Constant value column seem not supported (Invalid column name \'type\')'); + } + $this->assertSame([ ['type' => 'invoice', 'amount' => 23.0], ['type' => 'payment', 'amount' => 14.0], From 9e95d1cf5eedbb361cc33841b325677c6df28bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 8 Jan 2022 00:27:50 +0100 Subject: [PATCH 077/151] do not accept badly formatted seeds --- src/Model/Aggregate.php | 25 ++++--------------------- src/Model/AggregatesTrait.php | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 0feac147e..6404ca2f6 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -74,10 +74,6 @@ public function __construct(Model $baseModel, array $defaults = []) } /** - * Specify a single field or array of fields on which we will group model. - * - * @param mixed[] $aggregateExpressions Array of aggregate expressions with alias as key - * * @return $this */ public function groupBy(array $fields, array $aggregateExpressions = []): Model @@ -88,10 +84,8 @@ public function groupBy(array $fields, array $aggregateExpressions = []): Model $this->addField($fieldName); } - foreach ($aggregateExpressions as $name => $expr) { - $this->aggregateExpressions[$name] = $expr; - - $seed = is_array($expr) ? $expr : [$expr]; + foreach ($aggregateExpressions as $name => $seed) { + $this->aggregateExpressions[$name] = $seed; $args = []; // if field originally defined in the parent model, then it can be used as part of expression @@ -114,15 +108,6 @@ public function getRef(string $link): Reference } /** - * Method to enable commutative usage of methods enabling both of below - * Resulting in Aggregate on $model. - * - * $model->groupBy(['abc'])->withAggregateField('xyz'); - * - * and - * - * $model->withAggregateField('xyz')->groupBy(['abc']); - * * @return $this */ public function withAggregateField(string $name, $seed = []): Model @@ -139,10 +124,8 @@ public function withAggregateField(string $name, $seed = []): Model */ public function addField(string $name, $seed = []): Field { - $seed = is_array($seed) ? $seed : [$seed]; - - if (isset($seed[0]) && $seed[0] instanceof SqlExpressionField) { - return parent::addField($name, $seed[0]); + if ($seed instanceof SqlExpressionField) { + return parent::addField($name, $seed); } if ($seed['never_persist'] ?? false) { diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php index 1c260d577..062196ee6 100644 --- a/src/Model/AggregatesTrait.php +++ b/src/Model/AggregatesTrait.php @@ -12,6 +12,15 @@ trait AggregatesTrait { /** + * Method to enable commutative usage of methods enabling both of below + * Resulting in Aggregate on $model. + * + * $model->groupBy(['abc'])->withAggregateField('xyz'); + * + * and + * + * $model->withAggregateField('xyz')->groupBy(['abc']); + * * @param array|object $seed * * @return Aggregate @@ -24,9 +33,13 @@ public function withAggregateField(string $name, $seed = []): Model } /** + * Specify a single field or array of fields on which we will group model. + * + * @param array $aggregateExpressions Array of aggregate expressions with alias as key + * * @return Aggregate * - * @see Aggregate::groupBy. + * @see Aggregate::groupBy */ public function groupBy(array $fields, array $aggregateExpressions = []): Model { From 934041f09cd8930fc83a210685073191dc92030b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 02:41:08 +0100 Subject: [PATCH 078/151] fix unordered export assertions --- tests/ModelAggregateTest.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 22d9a6e31..cf7c0abe5 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -55,12 +55,12 @@ public function testGroupBy(): void { $invoiceAggregate = $this->createInvoice()->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); - $this->assertSame( + $this->assertSameExportUnordered( [ ['client_id' => 1, 'c' => 2], ['client_id' => 2, 'c' => 1], ], - $invoiceAggregate->setOrder('client_id', 'asc')->export() + $invoiceAggregate->export() ); } @@ -70,12 +70,12 @@ public function testGroupSelect(): void $aggregate->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); - $this->assertSame( + $this->assertSameExportUnordered( [ ['client' => 'Vinny', 'client_id' => 1, 'c' => 2], ['client' => 'Zoe', 'client_id' => 2, 'c' => 1], ], - $aggregate->setOrder('client_id', 'asc')->export() + $aggregate->export() ); } @@ -87,12 +87,12 @@ public function testGroupSelect2(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $this->assertSame( + $this->assertSameExportUnordered( [ ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], ], - $aggregate->setOrder('client_id', 'asc')->export() + $aggregate->export() ); } @@ -107,12 +107,12 @@ public function testGroupSelect3(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], // same as `s`, but reuse name `amount` ]); - $this->assertSame( + $this->assertSameExportUnordered( [ ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'min' => 4.0, 'max' => 4.0, 'amount' => 4.0], ], - $aggregate->setOrder('client_id', 'asc')->export() + $aggregate->export() ); } @@ -127,12 +127,12 @@ public function testGroupSelectExpr(): void $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); - $this->assertSame( + $this->assertSameExportUnordered( [ ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ], - $aggregate->setOrder('client_id', 'asc')->export() + $aggregate->export() ); } @@ -148,12 +148,12 @@ public function testGroupSelectCondition(): void $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); - $this->assertSame( + $this->assertSameExportUnordered( [ ['client' => 'Vinny', 'client_id' => 1, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], ], - $aggregate->setOrder('client_id', 'asc')->export() + $aggregate->export() ); } From eed9609f9b9654f33afbe842bbba5f2b60d9be7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 9 Jan 2022 13:19:13 +0100 Subject: [PATCH 079/151] fix unordered export assertions --- tests/ModelUnionTest.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 800775cf5..2de41555a 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -134,7 +134,7 @@ public function testActions(): void public function testActions2(): void { $transaction = $this->createTransaction(); - $this->assertSame(5, (int) $transaction->action('count')->getOne()); + $this->assertSame('5', $transaction->action('count')->getOne()); $this->assertSame(37.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); $transaction = $this->createSubtractInvoiceTransaction(); @@ -156,14 +156,14 @@ public function testBasics(): void $client = $this->createClient(); // There are total of 2 clients - $this->assertSame(2, (int) $client->action('count')->getOne()); + $this->assertSame('2', $client->action('count')->getOne()); // Client with ID=1 has invoices for 19 $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); $transaction = $this->createTransaction(); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], @@ -174,7 +174,7 @@ public function testBasics(): void // Transaction is Union Model $client->hasMany('Transaction', ['model' => $transaction]); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], @@ -184,7 +184,7 @@ public function testBasics(): void $transaction = $this->createSubtractInvoiceTransaction(); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], @@ -195,7 +195,7 @@ public function testBasics(): void // Transaction is Union Model $client->hasMany('Transaction', ['model' => $transaction]); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], @@ -301,7 +301,7 @@ public function testSubGroupingByExpressions(): void $this->markTestIncomplete('TODO MSSQL: Constant value column seem not supported (Invalid column name \'type\')'); } - $this->assertSame([ + $this->assertSameExportUnordered([ ['type' => 'invoice', 'amount' => 23.0], ['type' => 'payment', 'amount' => 14.0], ], $transaction->export(['type', 'amount'])); @@ -313,7 +313,7 @@ public function testSubGroupingByExpressions(): void $transaction->groupBy('type', ['amount' => ['sum([])', 'type' => 'atk4_money']]); - $this->assertSame([ + $this->assertSameExportUnordered([ ['type' => 'invoice', 'amount' => -23.0], ['type' => 'payment', 'amount' => 14.0], ], $transaction->export(['type', 'amount'])); @@ -384,7 +384,7 @@ public function testConditionOnUnionField(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '<', 0); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], @@ -396,7 +396,7 @@ public function testConditionOnNestedModelField(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('client_id', '>', 1); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); @@ -407,7 +407,7 @@ public function testConditionForcedOnNestedModels1(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '>', 5, true); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ], $transaction->export()); } @@ -417,7 +417,7 @@ public function testConditionForcedOnNestedModels2(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '<', -10, true); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ], $transaction->export()); } @@ -427,7 +427,7 @@ public function testConditionExpression(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition($transaction->expr('{} > 5', ['amount'])); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ], $transaction->export()); } @@ -440,7 +440,7 @@ public function testConditionOnMappedField(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->nestedInvoice->addCondition('amount', 4); - $this->assertSame([ + $this->assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], From d6543524d841f7b2d319b1dce18ca3dda074532d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 02:37:07 +0100 Subject: [PATCH 080/151] subaction must not add table alias --- src/Model/Union.php | 19 ++++++++----------- tests/ModelUnionTest.php | 20 +++++++++----------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index 487c3e3cc..380be2451 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -234,7 +234,7 @@ public function action($mode, $args = []) } } $subquery = $this->getSubQuery($fields); - $query = parent::action($mode, $args)->reset('table')->table($subquery); + $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->table); foreach ($this->group as $group) { $query->group($group); @@ -279,7 +279,7 @@ public function action($mode, $args = []) } // Substitute FROM table with our subquery expression - return parent::action($mode, $args)->reset('table')->table($subquery); + return parent::action($mode, $args)->reset('table')->table($subquery, $this->table); } /** @@ -372,17 +372,16 @@ public function getSubQuery(array $fields): Expression $args[$cnt++] = $query; } - // last element is table name itself - $args[$cnt] = $this->table; + $unionExpr = $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ')', $args); - return $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ') {' . $cnt . '}', $args); + return $unionExpr; } public function getSubAction(string $action, array $actionArgs = []): Expression { $cnt = 0; $expr = []; - $exprArgs = []; + $args = []; foreach ($this->union as [$model, $fieldMap]) { $modelActionArgs = $actionArgs; @@ -402,14 +401,12 @@ public function getSubAction(string $action, array $actionArgs = []): Expression // subquery should not be wrapped in parenthesis, SQLite is especially picky about that $query->wrapInParentheses = false; - $exprArgs[$cnt++] = $query; + $args[$cnt++] = $query; } - $expr = '(' . implode(' UNION ALL ', $expr) . ') {' . $cnt . '}'; - // last element is table name itself - $exprArgs[$cnt] = $this->table; + $unionExpr = $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ')', $args); - return $this->persistence->dsql()->expr($expr, $exprArgs); + return $unionExpr; } // {{{ Debug Methods diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 2de41555a..0d8689525 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -69,17 +69,17 @@ public function testNestedQuery1(): void $transaction = $this->createTransaction(); $this->assertSameSql( - '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"', + '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment")', $transaction->getSubQuery(['name'])->render()[0] ); $this->assertSameSql( - '(select "name" "name", "amount" "amount" from "invoice" UNION ALL select "name" "name", "amount" "amount" from "payment") "derivedTable"', + '(select "name" "name", "amount" "amount" from "invoice" UNION ALL select "name" "name", "amount" "amount" from "payment")', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); $this->assertSameSql( - '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"', + '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment")', $transaction->getSubQuery(['name'])->render()[0] ); } @@ -94,7 +94,7 @@ public function testMissingField(): void $transaction->addField('type'); $this->assertSameSql( - '(select (\'invoice\') "type", "amount" "amount" from "invoice" UNION ALL select NULL "type", "amount" "amount" from "payment") "derivedTable"', + '(select (\'invoice\') "type", "amount" "amount" from "invoice" UNION ALL select NULL "type", "amount" "amount" from "payment")', $transaction->getSubQuery(['type', 'amount'])->render()[0] ); } @@ -146,7 +146,7 @@ public function testSubAction1(): void $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( - '(select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment") "derivedTable"', + '(select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment")', $transaction->getSubAction('fx', ['sum', 'amount'])->render()[0] ); } @@ -209,7 +209,7 @@ public function testGrouping1(): void $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( - '(select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable"', + '(select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name")', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); @@ -218,7 +218,7 @@ public function testGrouping1(): void $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( - '(select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable"', + '(select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name")', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); } @@ -337,8 +337,7 @@ public function testReference(): void $this->assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - 'select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = :a ' . - 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"', + 'select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b)', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); @@ -350,8 +349,7 @@ public function testReference(): void $this->assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a ' . - 'UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "derivedTable"', + 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b)', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); } From 6bf9c9af95e817bfd559fba7c8b2e80b5309e517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 02:39:02 +0100 Subject: [PATCH 081/151] subaction must not wrap in parenthesis --- src/Model/Union.php | 41 ++++++++++++++++++++++++---------------- tests/ModelUnionTest.php | 14 +++++++------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index 380be2451..9fb59ad31 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -282,14 +282,27 @@ public function action($mode, $args = []) return parent::action($mode, $args)->reset('table')->table($subquery, $this->table); } + /** + * @param Query[] $subqueries + */ + private function createUnionQuery(array $subqueries): Query + { + $unionQuery = $this->persistence->dsql(); + $unionQuery->mode = 'union_all'; + \Closure::bind(function () use ($unionQuery, $subqueries) { + $unionQuery->template = implode(' UNION ALL ', array_fill(0, count($subqueries), '[]')); + }, null, Query::class)(); + $unionQuery->args['custom'] = $subqueries; + + return $unionQuery; + } + /** * Configures nested models to have a specified set of fields available. */ - public function getSubQuery(array $fields): Expression + public function getSubQuery(array $fields): Query { - $cnt = 0; - $expr = []; - $args = []; + $subqueries = []; foreach ($this->union as [$nestedModel, $fieldMap]) { // map fields for related model @@ -340,7 +353,6 @@ public function getSubQuery(array $fields): Expression } // now prepare query - $expr[] = '[' . $cnt . ']'; $query = $this->persistence->action($nestedModel, 'select', [false]); if ($nestedModel instanceof self) { @@ -369,25 +381,22 @@ public function getSubQuery(array $fields): Expression // subquery should not be wrapped in parenthesis, SQLite is especially picky about that $query->wrapInParentheses = false; - $args[$cnt++] = $query; + $subqueries[] = $query; } - $unionExpr = $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ')', $args); + $unionQuery = $this->createUnionQuery($subqueries); - return $unionExpr; + return $unionQuery; } - public function getSubAction(string $action, array $actionArgs = []): Expression + public function getSubAction(string $action, array $actionArgs = []): Query { - $cnt = 0; - $expr = []; - $args = []; + $subqueries = []; foreach ($this->union as [$model, $fieldMap]) { $modelActionArgs = $actionArgs; // now prepare query - $expr[] = '[' . $cnt . ']'; if ($fieldName = $actionArgs[1] ?? null) { $modelActionArgs[1] = $this->getFieldExpr( $model, @@ -401,12 +410,12 @@ public function getSubAction(string $action, array $actionArgs = []): Expression // subquery should not be wrapped in parenthesis, SQLite is especially picky about that $query->wrapInParentheses = false; - $args[$cnt++] = $query; + $subqueries[] = $query; } - $unionExpr = $this->persistence->dsql()->expr('(' . implode(' UNION ALL ', $expr) . ')', $args); + $unionQuery = $this->createUnionQuery($subqueries); - return $unionExpr; + return $unionQuery; } // {{{ Debug Methods diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 0d8689525..bdae19bca 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -69,17 +69,17 @@ public function testNestedQuery1(): void $transaction = $this->createTransaction(); $this->assertSameSql( - '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment")', + 'select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment"', $transaction->getSubQuery(['name'])->render()[0] ); $this->assertSameSql( - '(select "name" "name", "amount" "amount" from "invoice" UNION ALL select "name" "name", "amount" "amount" from "payment")', + 'select "name" "name", "amount" "amount" from "invoice" UNION ALL select "name" "name", "amount" "amount" from "payment"', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); $this->assertSameSql( - '(select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment")', + 'select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment"', $transaction->getSubQuery(['name'])->render()[0] ); } @@ -94,7 +94,7 @@ public function testMissingField(): void $transaction->addField('type'); $this->assertSameSql( - '(select (\'invoice\') "type", "amount" "amount" from "invoice" UNION ALL select NULL "type", "amount" "amount" from "payment")', + 'select (\'invoice\') "type", "amount" "amount" from "invoice" UNION ALL select NULL "type", "amount" "amount" from "payment"', $transaction->getSubQuery(['type', 'amount'])->render()[0] ); } @@ -146,7 +146,7 @@ public function testSubAction1(): void $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( - '(select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment")', + 'select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment"', $transaction->getSubAction('fx', ['sum', 'amount'])->render()[0] ); } @@ -209,7 +209,7 @@ public function testGrouping1(): void $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( - '(select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name")', + 'select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name"', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); @@ -218,7 +218,7 @@ public function testGrouping1(): void $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( - '(select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name")', + 'select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name"', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); } From 8f2713271880fc5f366c13983999dce6f31f1bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 03:01:02 +0100 Subject: [PATCH 082/151] prepare for Union::groupBy always wraps --- src/Model/Union.php | 20 ++++++--------- tests/ModelUnionTest.php | 55 +++++++++++++--------------------------- 2 files changed, 26 insertions(+), 49 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index 9fb59ad31..9e515b647 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -117,18 +117,14 @@ public function addNestedModel(Model $model, array $fieldMap = []): Model } /** - * Specify a single field or array of fields. - * - * @param string|array $group - * * @phpstan-return Model */ - public function groupBy($group, array $aggregate = []): Model // @phpstan-ignore-line + public function groupBy(array $fields, array $aggregateExpressions = []): Model // @phpstan-ignore-line { - $this->aggregate = $aggregate; - $this->group = is_string($group) ? [$group] : $group; + $this->aggregate = $aggregateExpressions; + $this->group = $fields; - foreach ($aggregate as $fieldName => $seed) { + foreach ($aggregateExpressions as $fieldName => $seed) { $seed = (array) $seed; $field = $this->hasField($fieldName) ? $this->getField($fieldName) : null; @@ -147,8 +143,8 @@ public function groupBy($group, array $aggregate = []): Model // @phpstan-ignore foreach ($this->union as [$nestedModel, $fieldMap]) { if ($nestedModel instanceof self) { - $nestedModel->aggregate = $aggregate; - $nestedModel->group = is_string($group) ? [$group] : $group; + $nestedModel->aggregate = $aggregateExpressions; + $nestedModel->group = $fields; } } @@ -185,8 +181,8 @@ public function addCondition($key, $operator = null, $value = null, $forceNested if (isset($fieldMap[$key])) { // field is included in mapping - use mapping expression $field = $fieldMap[$key] instanceof Expression - ? $fieldMap[$key] - : $this->getFieldExpr($nestedModel, $key, $fieldMap[$key]); + ? $fieldMap[$key] + : $this->getFieldExpr($nestedModel, $key, $fieldMap[$key]); } elseif (is_string($key) && $nestedModel->hasField($key)) { // model has such field - use that field directly $field = $nestedModel->getField($key); diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index bdae19bca..47808a545 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -206,41 +206,20 @@ public function testGrouping1(): void { $transaction = $this->createTransaction(); - $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); - - $this->assertSameSql( - 'select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name"', - $transaction->getSubQuery(['name', 'amount'])->render()[0] - ); - - $transaction = $this->createSubtractInvoiceTransaction(); - - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); - - $this->assertSameSql( - 'select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name"', - $transaction->getSubQuery(['name', 'amount'])->render()[0] - ); - } - - public function testGrouping2(): void - { - $transaction = $this->createTransaction(); - - $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( 'select "name", sum("amount") "amount" from (select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', - $transaction->action('select', [['name', 'amount']])->render()[0] + $transactionAggregate->action('select', [['name', 'amount']])->render()[0] ); $transaction = $this->createSubtractInvoiceTransaction(); - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( 'select "name", sum("amount") "amount" from (select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', - $transaction->action('select', [['name', 'amount']])->render()[0] + $transactionAggregate->action('select', [['name', 'amount']])->render()[0] ); } @@ -248,31 +227,33 @@ public function testGrouping2(): void * If all nested models have a physical field to which a grouped column can be mapped into, then we should group all our * sub-queries. */ - public function testGrouping3(): void + public function testGrouping2(): void { $transaction = $this->createTransaction(); $transaction->removeField('client_id'); - $transaction->groupBy('name', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); - $transaction->setOrder('name'); + // TODO enable later, test failing with MSSQL $transaction->setOrder('name'); + $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate->setOrder('name'); $this->assertSame([ ['name' => 'chair purchase', 'amount' => 8.0], ['name' => 'full pay', 'amount' => 4.0], ['name' => 'prepay', 'amount' => 10.0], ['name' => 'table purchase', 'amount' => 15.0], - ], $transaction->export()); + ], $transactionAggregate->export()); $transaction = $this->createSubtractInvoiceTransaction(); $transaction->removeField('client_id'); - $transaction->groupBy('name', ['amount' => ['sum([])', 'type' => 'atk4_money']]); - $transaction->setOrder('name'); + // TODO enable later, test failing with MSSQL $transaction->setOrder('name'); + $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate->setOrder('name'); $this->assertSame([ ['name' => 'chair purchase', 'amount' => -8.0], ['name' => 'full pay', 'amount' => 4.0], ['name' => 'prepay', 'amount' => 10.0], ['name' => 'table purchase', 'amount' => -15.0], - ], $transaction->export()); + ], $transactionAggregate->export()); } /** @@ -290,11 +271,11 @@ public function testSubGroupingByExpressions(): void $transaction->nestedPayment->addExpression('type', '\'payment\''); $transaction->addField('type'); - $transaction->groupBy('type', ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate = $transaction->groupBy(['type'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( 'select "client_id", "name", "type", sum("amount") "amount" from (select (\'invoice\') "type", sum("amount") "amount" from "invoice" group by "type" UNION ALL select (\'payment\') "type", sum("amount") "amount" from "payment" group by "type") "derivedTable" group by "type"', - $transaction->action('select')->render()[0] + $transactionAggregate->action('select')->render()[0] ); if ($this->getDatabasePlatform() instanceof SQLServerPlatform) { @@ -304,19 +285,19 @@ public function testSubGroupingByExpressions(): void $this->assertSameExportUnordered([ ['type' => 'invoice', 'amount' => 23.0], ['type' => 'payment', 'amount' => 14.0], - ], $transaction->export(['type', 'amount'])); + ], $transactionAggregate->export(['type', 'amount'])); $transaction = $this->createSubtractInvoiceTransaction(); $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->nestedPayment->addExpression('type', '\'payment\''); $transaction->addField('type'); - $transaction->groupBy('type', ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate = $transaction->groupBy(['type'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameExportUnordered([ ['type' => 'invoice', 'amount' => -23.0], ['type' => 'payment', 'amount' => 14.0], - ], $transaction->export(['type', 'amount'])); + ], $transactionAggregate->export(['type', 'amount'])); } public function testReference(): void From fbc54aeb1683cbe510507561a2a3dcb1a8f0da14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 10:09:18 +0100 Subject: [PATCH 083/151] drop deprecated hook alias --- src/Model/Union.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index 9e515b647..d5b87f741 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -29,9 +29,6 @@ class Union extends Model /** @const string */ public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; - /** @deprecated use HOOK_INIT_SELECT_QUERY instead - will be removed dec-2020 */ - public const HOOK_AFTER_UNION_SELECT = self::HOOK_INIT_SELECT_QUERY; - /** * Union model should always be read-only. * From 8e563e52105d2942500c51c9f67ef2c319764f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 10:13:37 +0100 Subject: [PATCH 084/151] fix expr seeding --- src/Model/Aggregate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 6404ca2f6..4efb96502 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -93,7 +93,7 @@ public function groupBy(array $fields, array $aggregateExpressions = []): Model $args = [$this->baseModel->getField($name)]; } - $seed['expr'] = $this->baseModel->expr($seed[0] ?? $seed['expr'], $args); + $seed[0 /* TODO 'expr' was here, 0 fixes tests, but 'expr' in seed might this be defined */] = $this->baseModel->expr($seed[0] ?? $seed['expr'], $args); // now add the expressions here $this->addExpression($name, $seed); From a883a756536ea25c5c37be1d8be9fb25702ef579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 10:30:33 +0100 Subject: [PATCH 085/151] allow to group by non-selecting expr, but then do not add that field --- src/Model/Aggregate.php | 17 +++++++++++------ tests/ModelAggregateTest.php | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 4efb96502..6410c9870 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -10,6 +10,7 @@ use Atk4\Data\Model; use Atk4\Data\Persistence; use Atk4\Data\Persistence\Sql\Expression; +use Atk4\Data\Persistence\Sql\Expressionable; use Atk4\Data\Persistence\Sql\Query; use Atk4\Data\Reference; @@ -49,10 +50,10 @@ class Aggregate extends Model /** @var Model */ public $baseModel; - /** @var string[] */ + /** @var array */ public $groupByFields = []; - /** @var mixed[] */ + /** @var array */ public $aggregateExpressions = []; public function __construct(Model $baseModel, array $defaults = []) @@ -81,6 +82,10 @@ public function groupBy(array $fields, array $aggregateExpressions = []): Model $this->groupByFields = array_unique(array_merge($this->groupByFields, $fields)); foreach ($fields as $fieldName) { + if ($fieldName instanceof Expression) { + continue; + } + $this->addField($fieldName); } @@ -197,7 +202,7 @@ protected function initQueryOrder(Query $query): void foreach ($this->order as $order) { $isDesc = strtolower($order[1]) === 'desc'; - if ($order[0] instanceof Expression) { + if ($order[0] instanceof Expressionable) { $query->order($order[0], $isDesc); } elseif (is_string($order[0])) { $query->order($this->getField($order[0]), $isDesc); @@ -216,10 +221,10 @@ protected function initQueryGrouping(Query $query): void $this->table_alias = $this->baseModel->table_alias; foreach ($this->groupByFields as $field) { - if ($this->baseModel->hasField($field)) { - $expression = $this->baseModel->getField($field); + if ($field instanceof Expression) { + $expression = $field; } else { - $expression = $this->expr($field); + $expression = $this->baseModel->getField($field); } $query->group($expression); diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index cf7c0abe5..f44576bc9 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -323,12 +323,12 @@ public function testAggregateFieldExpression(): void { $aggregate = $this->createInvoiceAggregate(); - $aggregate->groupBy(['abc'], [ + $aggregate->groupBy([$aggregate->expr('{}', ['abc'])], [ 'xyz' => ['expr' => 'sum([amount])'], ]); $this->assertSameSql( - 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", "abc", sum("amount") "xyz" from "invoice" group by abc', + 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", sum("amount") "xyz" from "invoice" group by "abc"', $aggregate->action('select')->render()[0] ); } From 58b83ee3ed7cd0afdfb16a2de848329499a37811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 10:41:21 +0100 Subject: [PATCH 086/151] fix grouping for PostgreSQL --- src/Model/Aggregate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 6410c9870..3ccb0fef3 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -224,7 +224,7 @@ protected function initQueryGrouping(Query $query): void if ($field instanceof Expression) { $expression = $field; } else { - $expression = $this->baseModel->getField($field); + $expression = $this->baseModel->getField($field)->short_name /* TODO short_name should be used by DSQL automatically when in GROUP BY, HAVING, ... */; } $query->group($expression); From fb296e193f268e59e992670fc557d49bddbb963f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 14:13:14 +0100 Subject: [PATCH 087/151] use "_tu" as default alias for Union model --- src/Model/Union.php | 6 +++--- tests/ModelUnionTest.php | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index d5b87f741..94a0e7bb7 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -77,7 +77,7 @@ class Union extends Model public $aggregate = []; /** @var string Derived table alias */ - public $table = 'derivedTable'; + public $table = '_tu'; /** * For a sub-model with a specified mapping, return expression @@ -227,7 +227,7 @@ public function action($mode, $args = []) } } $subquery = $this->getSubQuery($fields); - $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->table); + $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->table_alias ?? $this->table); foreach ($this->group as $group) { $query->group($group); @@ -272,7 +272,7 @@ public function action($mode, $args = []) } // Substitute FROM table with our subquery expression - return parent::action($mode, $args)->reset('table')->table($subquery, $this->table); + return parent::action($mode, $args)->reset('table')->table($subquery, $this->table_alias ?? $this->table); } /** diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 47808a545..b681f007b 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -104,29 +104,29 @@ public function testActions(): void $transaction = $this->createTransaction(); $this->assertSameSql( - 'select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", "amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "derivedTable"', + 'select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", "amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "_tu"', $transaction->action('select')->render()[0] ); $this->assertSameSql( - 'select "name" from (select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "derivedTable"', + 'select "name" from (select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "_tu"', $transaction->action('field', ['name'])->render()[0] ); $this->assertSameSql( - 'select sum("cnt") from (select count(*) "cnt" from "invoice" UNION ALL select count(*) "cnt" from "payment") "derivedTable"', + 'select sum("cnt") from (select count(*) "cnt" from "invoice" UNION ALL select count(*) "cnt" from "payment") "_tu"', $transaction->action('count')->render()[0] ); $this->assertSameSql( - 'select sum("val") from (select sum("amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"', + 'select sum("val") from (select sum("amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "_tu"', $transaction->action('fx', ['sum', 'amount'])->render()[0] ); $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( - 'select sum("val") from (select sum(-"amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "derivedTable"', + 'select sum("val") from (select sum(-"amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "_tu"', $transaction->action('fx', ['sum', 'amount'])->render()[0] ); } @@ -209,7 +209,7 @@ public function testGrouping1(): void $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( - 'select "name", sum("amount") "amount" from (select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', + 'select "name", sum("amount") "amount" from (select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "_tu" group by "name"', $transactionAggregate->action('select', [['name', 'amount']])->render()[0] ); @@ -218,7 +218,7 @@ public function testGrouping1(): void $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( - 'select "name", sum("amount") "amount" from (select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "derivedTable" group by "name"', + 'select "name", sum("amount") "amount" from (select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "_tu" group by "name"', $transactionAggregate->action('select', [['name', 'amount']])->render()[0] ); } @@ -274,7 +274,7 @@ public function testSubGroupingByExpressions(): void $transactionAggregate = $transaction->groupBy(['type'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( - 'select "client_id", "name", "type", sum("amount") "amount" from (select (\'invoice\') "type", sum("amount") "amount" from "invoice" group by "type" UNION ALL select (\'payment\') "type", sum("amount") "amount" from "payment" group by "type") "derivedTable" group by "type"', + 'select "client_id", "name", "type", sum("amount") "amount" from (select (\'invoice\') "type", sum("amount") "amount" from "invoice" group by "type" UNION ALL select (\'payment\') "type", sum("amount") "amount" from "payment" group by "type") "_tu" group by "type"', $transactionAggregate->action('select')->render()[0] ); @@ -353,7 +353,7 @@ public function testFieldAggregate(): void return; // @phpstan-ignore-next-line $this->assertSameSql( - 'select "client"."id", "client"."name", (select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "derivedTable") "balance" from "client" where "client"."id" = 1 limit 0, 1', + 'select "client"."id", "client"."name", (select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "_tu") "balance" from "client" where "client"."id" = 1 limit 0, 1', $client->load(1)->action('select')->render()[0] ); } From 035f36b140799bd3d5a51b8e14ff29eb2586417d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 16:22:08 +0100 Subject: [PATCH 088/151] createInvoiceAggregate must not add default "client" field --- tests/ModelAggregateTest.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index f44576bc9..11958ce7f 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -48,7 +48,7 @@ protected function createInvoice(): Model\Invoice protected function createInvoiceAggregate(): Aggregate { - return $this->createInvoice()->withAggregateField('client'); + return new Aggregate($this->createInvoice()); } public function testGroupBy(): void @@ -67,6 +67,7 @@ public function testGroupBy(): void public function testGroupSelect(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); @@ -82,6 +83,7 @@ public function testGroupSelect(): void public function testGroupSelect2(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], @@ -99,6 +101,7 @@ public function testGroupSelect2(): void public function testGroupSelect3(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], @@ -119,6 +122,7 @@ public function testGroupSelect3(): void public function testGroupSelectExpr(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], @@ -139,6 +143,7 @@ public function testGroupSelectExpr(): void public function testGroupSelectCondition(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->baseModel->addCondition('name', 'chair purchase'); $aggregate->groupBy(['client_id'], [ @@ -160,6 +165,7 @@ public function testGroupSelectCondition(): void public function testGroupSelectCondition2(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], @@ -185,6 +191,7 @@ public function testGroupSelectCondition2(): void public function testGroupSelectCondition3(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], @@ -209,6 +216,7 @@ public function testGroupSelectCondition3(): void public function testGroupSelectCondition4(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], @@ -229,6 +237,7 @@ public function testGroupSelectCondition4(): void public function testGroupSelectScope(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], @@ -258,6 +267,7 @@ public function testGroupSelectScope(): void public function testGroupOrder(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], @@ -274,6 +284,7 @@ public function testGroupOrder(): void public function testGroupLimit(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], @@ -291,6 +302,7 @@ public function testGroupLimit(): void public function testGroupLimit2(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], @@ -308,6 +320,7 @@ public function testGroupLimit2(): void public function testGroupCount(): void { $aggregate = $this->createInvoiceAggregate(); + $aggregate->addField('client'); $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], @@ -328,7 +341,7 @@ public function testAggregateFieldExpression(): void ]); $this->assertSameSql( - 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", sum("amount") "xyz" from "invoice" group by "abc"', + 'select sum("amount") "xyz" from "invoice" group by "abc"', $aggregate->action('select')->render()[0] ); } From 65ada49bb2335275af6403a8eab3f45cee503291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 16:30:25 +0100 Subject: [PATCH 089/151] impl Aggregate using model-in-model --- src/Model/Aggregate.php | 137 +++++++++-------------------------- tests/ModelAggregateTest.php | 25 +++++-- 2 files changed, 51 insertions(+), 111 deletions(-) diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index 3ccb0fef3..b68cea4f3 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -10,7 +10,6 @@ use Atk4\Data\Model; use Atk4\Data\Persistence; use Atk4\Data\Persistence\Sql\Expression; -use Atk4\Data\Persistence\Sql\Expressionable; use Atk4\Data\Persistence\Sql\Query; use Atk4\Data\Reference; @@ -38,7 +37,8 @@ * You can also pass seed (for example field type) when aggregating: * $aggregate->groupBy(['first', 'last'], ['salary' => ['sum([])', 'type' => 'atk4_money']]; * - * @property \Atk4\Data\Persistence\Sql $persistence + * @property Persistence\Sql $persistence + * @property Model $table * * @method Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model */ @@ -47,23 +47,16 @@ class Aggregate extends Model /** @const string */ public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; - /** @var Model */ - public $baseModel; - /** @var array */ public $groupByFields = []; - /** @var array */ - public $aggregateExpressions = []; - public function __construct(Model $baseModel, array $defaults = []) { if (!$baseModel->persistence instanceof Persistence\Sql) { throw new Exception('Base model must have Sql persistence to use grouping'); } - $this->baseModel = clone $baseModel; - $this->table = $baseModel->table; + $this->table = $baseModel; // this model does not have ID field $this->id_field = null; @@ -90,15 +83,13 @@ public function groupBy(array $fields, array $aggregateExpressions = []): Model } foreach ($aggregateExpressions as $name => $seed) { - $this->aggregateExpressions[$name] = $seed; - $args = []; // if field originally defined in the parent model, then it can be used as part of expression - if ($this->baseModel->hasField($name)) { - $args = [$this->baseModel->getField($name)]; + if ($this->table->hasField($name)) { + $args = [$this->table->getField($name)]; } - $seed[0 /* TODO 'expr' was here, 0 fixes tests, but 'expr' in seed might this be defined */] = $this->baseModel->expr($seed[0] ?? $seed['expr'], $args); + $seed[0 /* TODO 'expr' was here, 0 fixes tests, but 'expr' in seed might this be defined */] = $this->table->expr($seed[0] ?? $seed['expr'], $args); // now add the expressions here $this->addExpression($name, $seed); @@ -107,12 +98,21 @@ public function groupBy(array $fields, array $aggregateExpressions = []): Model return $this; } + /** + * TODO this should be removed, nasty hack to pass the tests. + */ public function getRef(string $link): Reference { - return $this->baseModel->getRef($link); + $ref = clone $this->table->getRef($link); + $ref->unsetOwner(); + $ref->setOwner($this); + + return $ref; } /** + * TODO this method should be removed, we do not offer similar methods for standard Model. + * * @return $this */ public function withAggregateField(string $name, $seed = []): Model @@ -137,9 +137,9 @@ public function addField(string $name, $seed = []): Field return parent::addField($name, $seed); } - if ($this->baseModel->hasField($name)) { - $field = clone $this->baseModel->getField($name); - $field->unsetOwner(); // will be new owner + if ($this->table->hasField($name)) { + $field = clone $this->table->getField($name); + $field->unsetOwner(); } else { $field = null; } @@ -150,38 +150,40 @@ public function addField(string $name, $seed = []): Field } /** - * @param string $mode - * @param array $args - * * @return Query */ - public function action($mode, $args = []) + public function action(string $mode, array $args = []) { switch ($mode) { case 'select': $fields = $this->onlyFields ?: array_keys($this->getFields()); // select but no need your fields - $query = $this->baseModel->action($mode, [false]); + $query = parent::action($mode, [false]); + if (isset($query->args['where'])) { + $query->args['having'] = $query->args['where']; + unset($query->args['where']); + } - $this->initQueryFields($query, array_unique($fields + $this->groupByFields)); - $this->initQueryOrder($query); + $this->persistence->initQueryFields($this, $query, array_unique($fields + $this->groupByFields)); $this->initQueryGrouping($query); - $this->initQueryConditions($query); - $this->initQueryLimit($query); $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); return $query; case 'count': - $query = $this->baseModel->action($mode, $args); + $query = parent::action($mode, $args); + if (isset($query->args['where'])) { + $query->args['having'] = $query->args['where']; + unset($query->args['where']); + } $query->reset('field')->field($this->expr('1')); $this->initQueryGrouping($query); $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); - return $query->dsql()->field('count(*)')->table($this->expr('([]) der', [$query])); + return $query->dsql()->field('count(*)')->table($this->expr('([]) {}', [$query, '_tc'])); case 'field': case 'fx': return parent::action($mode, $args); @@ -191,96 +193,23 @@ public function action($mode, $args = []) } } - protected function initQueryFields(Query $query, array $fields = []): void - { - $this->persistence->initQueryFields($this, $query, $fields); - } - - protected function initQueryOrder(Query $query): void - { - if ($this->order) { - foreach ($this->order as $order) { - $isDesc = strtolower($order[1]) === 'desc'; - - if ($order[0] instanceof Expressionable) { - $query->order($order[0], $isDesc); - } elseif (is_string($order[0])) { - $query->order($this->getField($order[0]), $isDesc); - } else { - throw (new Exception('Unsupported order parameter')) - ->addMoreInfo('model', $this) - ->addMoreInfo('field', $order[0]); - } - } - } - } - protected function initQueryGrouping(Query $query): void { - // use table alias of base model - $this->table_alias = $this->baseModel->table_alias; - foreach ($this->groupByFields as $field) { if ($field instanceof Expression) { $expression = $field; } else { - $expression = $this->baseModel->getField($field)->short_name /* TODO short_name should be used by DSQL automatically when in GROUP BY, HAVING, ... */; + $expression = $this->table->getField($field)->short_name /* TODO short_name should be used by DSQL automatically when in GROUP BY, HAVING, ... */; } $query->group($expression); } } - protected function initQueryConditions(Query $query, Model\Scope\AbstractScope $condition = null): void - { - $condition ??= $this->scope(); - - if (!$condition->isEmpty()) { - // peel off the single nested scopes to convert (((field = value))) to field = value - $condition = $condition->simplify(); - - // simple condition - if ($condition instanceof Model\Scope\Condition) { - $query->having(...$condition->toQueryArguments()); - } - - // nested conditions - if ($condition instanceof Model\Scope) { - $expression = $condition->isOr() ? $query->orExpr() : $query->andExpr(); - - foreach ($condition->getNestedConditions() as $nestedCondition) { - $this->initQueryConditions($expression, $nestedCondition); - } - - $query->having($expression); - } - } - } - - protected function initQueryLimit(Query $query): void - { - if ($this->limit && ($this->limit[0] || $this->limit[1])) { - if ($this->limit[0] === null) { - $this->limit[0] = \PHP_INT_MAX; - } - - $query->limit($this->limit[0], $this->limit[1]); - } - } - - // {{{ Debug Methods - - /** - * Returns array with useful debug info for var_dump. - */ public function __debugInfo(): array { return array_merge(parent::__debugInfo(), [ 'groupByFields' => $this->groupByFields, - 'aggregateExpressions' => $this->aggregateExpressions, - 'baseModel' => $this->baseModel->__debugInfo(), ]); } - - // }}} } diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 11958ce7f..f2b487c96 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -53,7 +53,9 @@ protected function createInvoiceAggregate(): Aggregate public function testGroupBy(): void { - $invoiceAggregate = $this->createInvoice()->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); + $invoiceAggregate = $this->createInvoice()->groupBy(['client_id'], [ + 'c' => ['expr' => 'count(*)', 'type' => 'integer'], + ]); $this->assertSameExportUnordered( [ @@ -69,7 +71,9 @@ public function testGroupSelect(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], ['c' => ['expr' => 'count(*)', 'type' => 'integer']]); + $aggregate->groupBy(['client_id'], [ + 'c' => ['expr' => 'count(*)', 'type' => 'integer'], + ]); $this->assertSameExportUnordered( [ @@ -144,7 +148,7 @@ public function testGroupSelectCondition(): void { $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->baseModel->addCondition('name', 'chair purchase'); + $aggregate->table->addCondition('name', 'chair purchase'); $aggregate->groupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], @@ -276,7 +280,14 @@ public function testGroupOrder(): void $aggregate->setOrder('client_id', 'asc'); $this->assertSameSql( - 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client", "client_id", sum("amount") "amount" from "invoice" group by "client_id" order by "client_id"', + 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "_tm"."client_id") "client", "client_id", sum("amount") "amount" from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id" order by "client_id"', + $aggregate->action('select')->render()[0] + ); + + // TODO subselect should not select "client" field + $aggregate->removeField('client'); + $this->assertSameSql( + 'select "client_id", sum("amount") "amount" from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id" order by "client_id"', $aggregate->action('select')->render()[0] ); } @@ -307,7 +318,7 @@ public function testGroupLimit2(): void $aggregate->groupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->setLimit(1, 1); + $aggregate->setLimit(2, 1); $this->assertSame( [ @@ -327,7 +338,7 @@ public function testGroupCount(): void ]); $this->assertSameSql( - 'select count(*) from ((select 1 from "invoice" group by "client_id")) der', + 'select count(*) from ((select 1 from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id")) "_tc"', $aggregate->action('count')->render()[0] ); } @@ -341,7 +352,7 @@ public function testAggregateFieldExpression(): void ]); $this->assertSameSql( - 'select sum("amount") "xyz" from "invoice" group by "abc"', + 'select sum("amount") "xyz" from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "abc"', $aggregate->action('select')->render()[0] ); } From abfaabe6011ad54ae03d29b258eb3e7e86af5aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 11 Jan 2022 08:59:52 +0100 Subject: [PATCH 090/151] do not skip WITH test for MariaDB --- tests/WithTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/WithTest.php b/tests/WithTest.php index bcc285ea1..14db20558 100644 --- a/tests/WithTest.php +++ b/tests/WithTest.php @@ -50,7 +50,7 @@ public function testWith(): void if ($this->getDatabasePlatform() instanceof MySQLPlatform) { $serverVersion = $this->db->connection->connection()->getWrappedConnection()->getServerVersion(); - if (str_starts_with($serverVersion, '5.')) { + if (preg_match('~^5\.(?!5\.5-.+?-MariaDB)~', $serverVersion)) { $this->markTestIncomplete('MySQL Server 5.x does not support WITH clause'); } } From 12c2c2a08681cf38fae5f6f3b901978f5a1f479f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 11 Jan 2022 13:09:32 +0100 Subject: [PATCH 091/151] add model-in-model tests incl. hooks --- src/Model.php | 10 +- tests/ModelNestedTest.php | 238 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 tests/ModelNestedTest.php diff --git a/src/Model.php b/src/Model.php index cc069e953..78a473c50 100644 --- a/src/Model.php +++ b/src/Model.php @@ -1616,10 +1616,8 @@ public function save(array $data = []) * This is a temporary method to avoid code duplication, but insert / import should * be implemented differently. */ - protected function _rawInsert(array $row): void + protected function _insert(array $row): void { - $this->unload(); - // Find any row values that do not correspond to fields, and they may correspond to // references instead $refs = []; @@ -1668,10 +1666,10 @@ protected function _rawInsert(array $row): void */ public function insert(array $row) { - $model = $this->createEntity(); - $model->_rawInsert($row); + $entity = $this->createEntity(); + $entity->_insert($row); - return $this->id_field ? $model->getId() : null; + return $this->id_field ? $entity->getId() : null; } /** diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php new file mode 100644 index 000000000..076647c53 --- /dev/null +++ b/tests/ModelNestedTest.php @@ -0,0 +1,238 @@ +setDb([ + 'user' => [ + ['id' => 1, 'name' => 'John', 'birthday_date' => '1980-2-1'], + ['id' => 2, 'name' => 'Sue', 'birthday_date' => '2005-4-3'], + ], + ]); + } + + /** @var array */ + public $hookLog = []; + + protected function createTestModel(): Model + { + $mWithLoggingClass = get_class(new class() extends Model { + /** @var \WeakReference */ + protected $testCaseWeakRef; + /** @var string */ + protected $testModelAlias; + + public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = null) + { + if (!str_starts_with($spot, '__atk__method__') && $spot !== Model::HOOK_NORMALIZE) { + $convertValueToLogFx = function ($v) use (&$convertValueToLogFx) { + if (is_array($v)) { + return array_map($convertValueToLogFx, $v); + } elseif (is_scalar($v) || $v === null) { + return $v; + } elseif ($v instanceof self) { + return $this->testModelAlias; + } + + $res = preg_replace('~(?<=^Atk4\\\\Data\\\\Persistence\\\\Sql\\\\)\w+\\\\(?=\w+$)~', '', get_debug_type($v)); + if (Connection::isComposerDbal2x() && $res === 'Doctrine\DBAL\Statement') { + $res = DbalResult::class; + } + + return $res; + }; + + $this->testCaseWeakRef->get()->hookLog[] = [$convertValueToLogFx($this), $spot, $convertValueToLogFx($args)]; + } + + return parent::hook($spot, $args, $brokenBy); + } + }); + + $mInner = new $mWithLoggingClass($this->db, [ + 'testCaseWeakRef' => \WeakReference::create($this), + 'testModelAlias' => 'inner', + 'table' => 'user', + ]); + $mInner->addField('name'); + $mInner->addField('y', ['actual' => 'birthday_date', 'type' => 'date']); + + $m = new $mWithLoggingClass($this->db, [ + 'testCaseWeakRef' => \WeakReference::create($this), + 'testModelAlias' => 'main', + 'table' => $mInner, + ]); + $m->addField('name'); + $m->addField('birthday', ['actual' => 'y', 'type' => 'date']); + + return $m; + } + + public function testSelectSql(): void + { + $m = $this->createTestModel(); + $m->table->setOrder('name', 'desc'); + $m->table->setLimit(5); + $m->setOrder('birthday'); + + $this->assertSame( + ($this->db->connection->dsql()) + ->table( + ($this->db->connection->dsql()) + ->field('id') + ->field('name') + ->field('birthday_date', 'y') + ->table('user') + ->order('name', true) + ->limit(5), + '_tm' + ) + ->field('id') + ->field('name') + ->field('y', 'birthday') + ->order('y') + ->render()[0], + $m->action('select')->render()[0] + ); + + $this->assertSame([ + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ], $this->hookLog); + } + + public function testSelectExport(): void + { + $m = $this->createTestModel(); + + $this->assertSameExportUnordered([ + ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ], $m->export()); + + $this->assertSame([ + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ], $this->hookLog); + } + + public function testInsert(): void + { + $m = $this->createTestModel(); + + $m->createEntity()->setMulti([ + 'name' => 'Karl', + 'birthday' => new \DateTime('2000-6-1'), + ])->save(); + + $this->assertSame([ + ['main', Model::HOOK_VALIDATE, ['save']], + ['main', Model::HOOK_BEFORE_SAVE, [false]], + ['main', Model::HOOK_BEFORE_INSERT, [['id' => null, 'name' => 'Karl', 'birthday' => \DateTime::class]]], + ['inner', Model::HOOK_VALIDATE, ['save']], + ['inner', Model::HOOK_BEFORE_SAVE, [false]], + ['inner', Model::HOOK_BEFORE_INSERT, [['id' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], + ['inner', Persistence\Sql::HOOK_BEFORE_INSERT_QUERY, [Query::class]], + ['inner', Persistence\Sql::HOOK_AFTER_INSERT_QUERY, [Query::class, DbalResult::class]], + ['inner', Model::HOOK_AFTER_INSERT, []], + ['inner', Model::HOOK_AFTER_SAVE, [false]], + ['main', Model::HOOK_AFTER_INSERT, []], + ['main', Model::HOOK_BEFORE_UNLOAD, []], + ['main', Model::HOOK_AFTER_UNLOAD, []], + ['main', Model::HOOK_BEFORE_LOAD, [3]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_AFTER_SAVE, [false]], + ], $this->hookLog); + + $this->assertSameExportUnordered([ + ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['id' => 3, 'name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], + ], $m->export()); + } + + public function testUpdate(): void + { + $m = $this->createTestModel(); + + $m->load(2)->setMulti([ + 'name' => 'Susan', + 'birthday' => new \DateTime('2020-10-10'), + ])->save(); + + $this->assertSame([ + ['main', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_VALIDATE, ['save']], + ['main', Model::HOOK_BEFORE_SAVE, [true]], + ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan', 'birthday' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', Model::HOOK_VALIDATE, ['save']], + ['inner', Model::HOOK_BEFORE_SAVE, [true]], + ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan', 'y' => \DateTime::class]]], + ['inner', Persistence\Sql::HOOK_BEFORE_UPDATE_QUERY, [Query::class]], + ['inner', Persistence\Sql::HOOK_AFTER_UPDATE_QUERY, [Query::class, DbalResult::class]], + ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_AFTER_SAVE, [true]], + ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan', 'birthday' => \DateTime::class]]], + ['main', Model::HOOK_AFTER_SAVE, [true]], + ], $this->hookLog); + + $this->assertSameExportUnordered([ + ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['id' => 2, 'name' => 'Susan', 'birthday' => new \DateTime('2020-10-10')], + ], $m->export()); + } + + public function testDelete(): void + { + $m = $this->createTestModel(); + + $m->load(2)->delete(); + + $this->assertSame([ + ['main', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_BEFORE_DELETE, []], + ['inner', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], + ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', Model::HOOK_BEFORE_DELETE, []], + ['inner', Persistence\Sql::HOOK_BEFORE_DELETE_QUERY, [Query::class]], + ['inner', Persistence\Sql::HOOK_AFTER_DELETE_QUERY, [Query::class, DbalResult::class]], + ['inner', Model::HOOK_AFTER_DELETE, []], + ['inner', Model::HOOK_BEFORE_UNLOAD, []], + ['inner', Model::HOOK_AFTER_UNLOAD, []], + ['main', Model::HOOK_AFTER_DELETE, []], + ['main', Model::HOOK_BEFORE_UNLOAD, []], + ['main', Model::HOOK_AFTER_UNLOAD, []], + ], $this->hookLog); + + $this->assertSameExportUnordered([ + ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ], $m->export()); + } +} From aab0bf9c13aa812e7e75b1000957cfbc21e44509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 11 Jan 2022 14:01:30 +0100 Subject: [PATCH 092/151] test with "_id" ID column name --- src/Schema/TestCase.php | 10 ++++++---- tests/ModelNestedTest.php | 38 ++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index f4acf933c..fab30ef3f 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -255,11 +255,13 @@ public function setDb(array $dbData, bool $importData = true): void } $first_row = current($data); + $idColumnName = null; if ($first_row) { - $migrator->id('id'); + $idColumnName = isset($first_row['_id']) ? '_id' : 'id'; + $migrator->id($idColumnName); foreach ($first_row as $field => $row) { - if ($field === 'id') { + if ($field === $idColumnName) { continue; } @@ -292,8 +294,8 @@ public function setDb(array $dbData, bool $importData = true): void $query->table($tableName); $query->setMulti($row); - if (!isset($row['id']) && $hasId) { - $query->set('id', $id); + if (!isset($row[$idColumnName]) && $hasId) { + $query->set($idColumnName, $id); } $query->mode('insert')->execute(); diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php index 076647c53..bff936f74 100644 --- a/tests/ModelNestedTest.php +++ b/tests/ModelNestedTest.php @@ -20,8 +20,8 @@ protected function setUp(): void $this->setDb([ 'user' => [ - ['id' => 1, 'name' => 'John', 'birthday_date' => '1980-2-1'], - ['id' => 2, 'name' => 'Sue', 'birthday_date' => '2005-4-3'], + ['_id' => 1, 'name' => 'John', '_birthday' => '1980-2-1'], + ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-4-3'], ], ]); } @@ -69,14 +69,20 @@ public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = nu 'testModelAlias' => 'inner', 'table' => 'user', ]); + $mInner->removeField('id'); + $mInner->addField('_id', ['type' => 'integer']); + $mInner->id_field = '_id'; $mInner->addField('name'); - $mInner->addField('y', ['actual' => 'birthday_date', 'type' => 'date']); + $mInner->addField('y', ['actual' => '_birthday', 'type' => 'date']); $m = new $mWithLoggingClass($this->db, [ 'testCaseWeakRef' => \WeakReference::create($this), 'testModelAlias' => 'main', 'table' => $mInner, ]); + $m->removeField('id'); + $m->addField('_id', ['type' => 'integer']); + $m->id_field = '_id'; $m->addField('name'); $m->addField('birthday', ['actual' => 'y', 'type' => 'date']); @@ -94,15 +100,15 @@ public function testSelectSql(): void ($this->db->connection->dsql()) ->table( ($this->db->connection->dsql()) - ->field('id') + ->field('_id') ->field('name') - ->field('birthday_date', 'y') + ->field('_birthday', 'y') ->table('user') ->order('name', true) ->limit(5), '_tm' ) - ->field('id') + ->field('_id') ->field('name') ->field('y', 'birthday') ->order('y') @@ -121,8 +127,8 @@ public function testSelectExport(): void $m = $this->createTestModel(); $this->assertSameExportUnordered([ - ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['_id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], ], $m->export()); $this->assertSame([ @@ -143,10 +149,10 @@ public function testInsert(): void $this->assertSame([ ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [false]], - ['main', Model::HOOK_BEFORE_INSERT, [['id' => null, 'name' => 'Karl', 'birthday' => \DateTime::class]]], + ['main', Model::HOOK_BEFORE_INSERT, [['_id' => null, 'name' => 'Karl', 'birthday' => \DateTime::class]]], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [false]], - ['inner', Model::HOOK_BEFORE_INSERT, [['id' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_INSERT, [['_id' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], ['inner', Persistence\Sql::HOOK_BEFORE_INSERT_QUERY, [Query::class]], ['inner', Persistence\Sql::HOOK_AFTER_INSERT_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_INSERT, []], @@ -162,9 +168,9 @@ public function testInsert(): void ], $this->hookLog); $this->assertSameExportUnordered([ - ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], - ['id' => 3, 'name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], + ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['_id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['_id' => 3, 'name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], ], $m->export()); } @@ -200,8 +206,8 @@ public function testUpdate(): void ], $this->hookLog); $this->assertSameExportUnordered([ - ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['id' => 2, 'name' => 'Susan', 'birthday' => new \DateTime('2020-10-10')], + ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['_id' => 2, 'name' => 'Susan', 'birthday' => new \DateTime('2020-10-10')], ], $m->export()); } @@ -232,7 +238,7 @@ public function testDelete(): void ], $this->hookLog); $this->assertSameExportUnordered([ - ['id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], ], $m->export()); } } From 1a96086ef8161ec0bf633a9e8db2ad0d8aea9191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 11 Jan 2022 23:58:19 +0100 Subject: [PATCH 093/151] fix different ID column across nested models --- src/Field.php | 2 +- src/Model.php | 13 ------- src/Persistence.php | 51 ++++++++++++++------------ src/Persistence/Sql.php | 7 ++-- tests/ModelNestedTest.php | 76 ++++++++++++++++++++------------------- 5 files changed, 74 insertions(+), 75 deletions(-) diff --git a/src/Field.php b/src/Field.php index 0d780016b..19164ac3c 100644 --- a/src/Field.php +++ b/src/Field.php @@ -499,7 +499,7 @@ public function __debugInfo(): array ]; foreach ([ - 'system', 'never_persist', 'never_save', 'read_only', 'ui', 'joinName', + 'actual', 'system', 'never_persist', 'never_save', 'read_only', 'ui', 'joinName', ] as $key) { if ($this->{$key} !== null) { $arr[$key] = $this->{$key}; diff --git a/src/Model.php b/src/Model.php index 78a473c50..bd1bed894 100644 --- a/src/Model.php +++ b/src/Model.php @@ -1612,10 +1612,6 @@ public function save(array $data = []) }); } - /** - * This is a temporary method to avoid code duplication, but insert / import should - * be implemented differently. - */ protected function _insert(array $row): void { // Find any row values that do not correspond to fields, and they may correspond to @@ -1658,10 +1654,6 @@ protected function _insert(array $row): void } /** - * Faster method to add data, that does not modify active record. - * - * Will be further optimized in the future. - * * @return mixed */ public function insert(array $row) @@ -1673,11 +1665,6 @@ public function insert(array $row) } /** - * Even more faster method to add data, does not modify your - * current record and will not return anything. - * - * Will be further optimized in the future. - * * @return $this */ public function import(array $rows) diff --git a/src/Persistence.php b/src/Persistence.php index 1a32b1e5f..0eed8e4ed 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -116,18 +116,6 @@ public function getDatabasePlatform(): Platforms\AbstractPlatform return new Persistence\GenericPlatform(); } - private function assertSameIdField(Model $model): void - { - $modelIdFieldName = $model->getField($model->id_field)->getPersistenceName(); - $tableIdFieldName = $model->table->id_field; - - if ($modelIdFieldName !== $tableIdFieldName) { - throw (new Exception('Table model with different ID field persistence name is not supported')) - ->addMoreInfo('model_id_field', $modelIdFieldName) - ->addMoreInfo('table_id_field', $tableIdFieldName); - } - } - /** * Tries to load data record, but will not fail if record can't be loaded. * @@ -174,13 +162,28 @@ public function insert(Model $model, array $data) unset($data); if (is_object($model->table)) { - $this->assertSameIdField($model); + $innerInsertId = $model->table->insert($this->typecastLoadRow($model->table, $dataRaw)); + if (!$model->id_field) { + return false; + } - return $model->table->insert($model->table->persistence->typecastLoadRow($model->table, $dataRaw)); + $idField = $model->getField($model->id_field); + $insertId = $this->typecastLoadField( + $idField, + $idField->getPersistenceName() === $model->table->id_field + ? $this->typecastSaveField($model->table->getField($model->table->id_field), $innerInsertId) + : $dataRaw[$idField->getPersistenceName()] + ); + + return $insertId; } $idRaw = $this->insertRaw($model, $dataRaw); - $id = $model->id_field ? $this->typecastLoadField($model->getField($model->id_field), $idRaw) : new \stdClass(); + if (!$model->id_field) { + return false; + } + + $id = $this->typecastLoadField($model->getField($model->id_field), $idRaw); return $id; } @@ -201,6 +204,7 @@ protected function insertRaw(Model $model, array $dataRaw) public function update(Model $model, $id, array $data): void { $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); if ($idRaw === null || (array_key_exists($model->id_field, $data) && $data[$model->id_field] === null)) { throw new Exception('Model id_field is not set. Unable to update record.'); } @@ -213,15 +217,15 @@ public function update(Model $model, $id, array $data): void } if (is_object($model->table)) { - $this->assertSameIdField($model); + $idPersistenceName = $model->getField($model->id_field)->getPersistenceName(); + $innerId = $this->typecastLoadField($model->table->getField($idPersistenceName), $idRaw); + $innerModel = $model->table->loadBy($idPersistenceName, $innerId); - $model->table->load($id)->save($model->table->persistence->typecastLoadRow($model->table, $dataRaw)); + $innerModel->save($this->typecastLoadRow($model->table, $dataRaw)); return; } - unset($id); - $this->updateRaw($model, $idRaw, $dataRaw); } @@ -241,20 +245,21 @@ protected function updateRaw(Model $model, $idRaw, array $dataRaw): void public function delete(Model $model, $id): void { $idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null; + unset($id); if ($idRaw === null) { throw new Exception('Model id_field is not set. Unable to delete record.'); } if (is_object($model->table)) { - $this->assertSameIdField($model); + $idPersistenceName = $model->getField($model->id_field)->getPersistenceName(); + $innerId = $this->typecastLoadField($model->table->getField($idPersistenceName), $idRaw); + $innerModel = $model->table->loadBy($idPersistenceName, $innerId); - $model->table->delete($id); + $innerModel->delete(); return; } - unset($id); - $this->deleteRaw($model, $idRaw); } diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index d0d36ffdd..e5bd19f70 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -422,7 +422,8 @@ public function action(Model $model, string $type, array $args = []) $this->setLimitOrder($model, $query); if ($model->isEntity() && $model->isLoaded()) { - $query->where($model->getField($model->id_field), $model->getId()); + $idRaw = $this->typecastSaveField($model->getField($model->id_field), $model->getId()); + $query->where($model->getField($model->id_field), $idRaw); } return $query; @@ -478,7 +479,9 @@ public function tryLoad(Model $model, $id): ?array throw (new Exception('Unable to load field by "id" when Model->id_field is not defined.')) ->addMoreInfo('id', $id); } - $query->where($model->getField($model->id_field), $id); + + $idRaw = $this->typecastSaveField($model->getField($model->id_field), $id); + $query->where($model->getField($model->id_field), $idRaw); } $query->limit($id === self::ID_LOAD_ANY ? 1 : 2); diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php index bff936f74..309583226 100644 --- a/tests/ModelNestedTest.php +++ b/tests/ModelNestedTest.php @@ -20,8 +20,8 @@ protected function setUp(): void $this->setDb([ 'user' => [ - ['_id' => 1, 'name' => 'John', '_birthday' => '1980-2-1'], - ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-4-3'], + ['_id' => 1, 'name' => 'John', '_birthday' => '1980-02-01'], + ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-04-03'], ], ]); } @@ -70,8 +70,8 @@ public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = nu 'table' => 'user', ]); $mInner->removeField('id'); - $mInner->addField('_id', ['type' => 'integer']); - $mInner->id_field = '_id'; + $mInner->id_field = 'uid'; + $mInner->addField('uid', ['actual' => '_id', 'type' => 'integer']); $mInner->addField('name'); $mInner->addField('y', ['actual' => '_birthday', 'type' => 'date']); @@ -81,8 +81,7 @@ public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = nu 'table' => $mInner, ]); $m->removeField('id'); - $m->addField('_id', ['type' => 'integer']); - $m->id_field = '_id'; + $m->id_field = 'birthday'; $m->addField('name'); $m->addField('birthday', ['actual' => 'y', 'type' => 'date']); @@ -100,7 +99,7 @@ public function testSelectSql(): void ($this->db->connection->dsql()) ->table( ($this->db->connection->dsql()) - ->field('_id') + ->field('_id', 'uid') ->field('name') ->field('_birthday', 'y') ->table('user') @@ -108,7 +107,6 @@ public function testSelectSql(): void ->limit(5), '_tm' ) - ->field('_id') ->field('name') ->field('y', 'birthday') ->order('y') @@ -127,8 +125,8 @@ public function testSelectExport(): void $m = $this->createTestModel(); $this->assertSameExportUnordered([ - ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['_id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], ], $m->export()); $this->assertSame([ @@ -141,18 +139,19 @@ public function testInsert(): void { $m = $this->createTestModel(); - $m->createEntity()->setMulti([ - 'name' => 'Karl', - 'birthday' => new \DateTime('2000-6-1'), - ])->save(); + $entity = $m->createEntity() + ->setMulti([ + 'name' => 'Karl', + 'birthday' => new \DateTime('2000-6-1'), + ])->save(); $this->assertSame([ ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [false]], - ['main', Model::HOOK_BEFORE_INSERT, [['_id' => null, 'name' => 'Karl', 'birthday' => \DateTime::class]]], + ['main', Model::HOOK_BEFORE_INSERT, [['name' => 'Karl', 'birthday' => \DateTime::class]]], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [false]], - ['inner', Model::HOOK_BEFORE_INSERT, [['_id' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_INSERT, [['uid' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], ['inner', Persistence\Sql::HOOK_BEFORE_INSERT_QUERY, [Query::class]], ['inner', Persistence\Sql::HOOK_AFTER_INSERT_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_INSERT, []], @@ -160,17 +159,20 @@ public function testInsert(): void ['main', Model::HOOK_AFTER_INSERT, []], ['main', Model::HOOK_BEFORE_UNLOAD, []], ['main', Model::HOOK_AFTER_UNLOAD, []], - ['main', Model::HOOK_BEFORE_LOAD, [3]], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], ['main', Model::HOOK_AFTER_SAVE, [false]], ], $this->hookLog); + $this->assertSame(3, $m->table->loadBy('name', 'Karl')->getId()); + $this->assertSameExportUnordered([[new \DateTime('2000-6-1')]], [[$entity->getId()]]); + $this->assertSameExportUnordered([ - ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['_id' => 2, 'name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], - ['_id' => 3, 'name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], + ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ['name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], ], $m->export()); } @@ -178,36 +180,37 @@ public function testUpdate(): void { $m = $this->createTestModel(); - $m->load(2)->setMulti([ - 'name' => 'Susan', - 'birthday' => new \DateTime('2020-10-10'), - ])->save(); + $m->load(new \DateTime('2005-4-3')) + ->setMulti([ + 'name' => 'Susan', + ])->save(); $this->assertSame([ - ['main', Model::HOOK_BEFORE_LOAD, [2]], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [true]], - ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan', 'birthday' => \DateTime::class]]], - ['inner', Model::HOOK_BEFORE_LOAD, [2]], + ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], + ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['inner', Model::HOOK_AFTER_LOAD, []], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [true]], - ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], ['inner', Persistence\Sql::HOOK_BEFORE_UPDATE_QUERY, [Query::class]], ['inner', Persistence\Sql::HOOK_AFTER_UPDATE_QUERY, [Query::class, DbalResult::class]], - ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['inner', Model::HOOK_AFTER_SAVE, [true]], - ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan', 'birthday' => \DateTime::class]]], + ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['main', Model::HOOK_AFTER_SAVE, [true]], ], $this->hookLog); $this->assertSameExportUnordered([ - ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['_id' => 2, 'name' => 'Susan', 'birthday' => new \DateTime('2020-10-10')], + ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Susan', 'birthday' => new \DateTime('2005-4-3')], ], $m->export()); } @@ -215,15 +218,16 @@ public function testDelete(): void { $m = $this->createTestModel(); - $m->load(2)->delete(); + $m->delete(new \DateTime('2005-4-3')); $this->assertSame([ - ['main', Model::HOOK_BEFORE_LOAD, [2]], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_BEFORE_DELETE, []], - ['inner', Model::HOOK_BEFORE_LOAD, [2]], + ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['inner', Model::HOOK_AFTER_LOAD, []], ['inner', Model::HOOK_BEFORE_DELETE, []], @@ -238,7 +242,7 @@ public function testDelete(): void ], $this->hookLog); $this->assertSameExportUnordered([ - ['_id' => 1, 'name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], ], $m->export()); } } From d48a0379d6833b23b768bd358f0f676cefb2cfe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 12 Jan 2022 00:50:26 +0100 Subject: [PATCH 094/151] assert nested transactions --- tests/ModelNestedTest.php | 65 ++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedTest.php index 309583226..128d2c234 100644 --- a/tests/ModelNestedTest.php +++ b/tests/ModelNestedTest.php @@ -37,31 +37,48 @@ protected function createTestModel(): Model /** @var string */ protected $testModelAlias; + /** + * @param mixed $v + * + * @return mixed + */ + protected function convertValueToLog($v) + { + if (is_array($v)) { + return array_map(fn ($v) => $this->convertValueToLog($v), $v); + } elseif (is_scalar($v) || $v === null) { + return $v; + } elseif ($v instanceof self) { + return $this->testModelAlias; + } + + $res = preg_replace('~(?<=^Atk4\\\\Data\\\\Persistence\\\\Sql\\\\)\w+\\\\(?=\w+$)~', '', get_debug_type($v)); + if (Connection::isComposerDbal2x() && $res === 'Doctrine\DBAL\Statement') { + $res = DbalResult::class; + } + + return $res; + } + public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = null) { if (!str_starts_with($spot, '__atk__method__') && $spot !== Model::HOOK_NORMALIZE) { - $convertValueToLogFx = function ($v) use (&$convertValueToLogFx) { - if (is_array($v)) { - return array_map($convertValueToLogFx, $v); - } elseif (is_scalar($v) || $v === null) { - return $v; - } elseif ($v instanceof self) { - return $this->testModelAlias; - } - - $res = preg_replace('~(?<=^Atk4\\\\Data\\\\Persistence\\\\Sql\\\\)\w+\\\\(?=\w+$)~', '', get_debug_type($v)); - if (Connection::isComposerDbal2x() && $res === 'Doctrine\DBAL\Statement') { - $res = DbalResult::class; - } - - return $res; - }; - - $this->testCaseWeakRef->get()->hookLog[] = [$convertValueToLogFx($this), $spot, $convertValueToLogFx($args)]; + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), $spot, $this->convertValueToLog($args)]; } return parent::hook($spot, $args, $brokenBy); } + + public function atomic(\Closure $fx) + { + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), '>>>']; + + $res = parent::atomic($fx); + + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), '<<<']; + + return $res; + } }); $mInner = new $mWithLoggingClass($this->db, [ @@ -146,9 +163,11 @@ public function testInsert(): void ])->save(); $this->assertSame([ + ['main', '>>>'], ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [false]], ['main', Model::HOOK_BEFORE_INSERT, [['name' => 'Karl', 'birthday' => \DateTime::class]]], + ['inner', '>>>'], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [false]], ['inner', Model::HOOK_BEFORE_INSERT, [['uid' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], @@ -156,6 +175,7 @@ public function testInsert(): void ['inner', Persistence\Sql::HOOK_AFTER_INSERT_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_INSERT, []], ['inner', Model::HOOK_AFTER_SAVE, [false]], + ['inner', '<<<'], ['main', Model::HOOK_AFTER_INSERT, []], ['main', Model::HOOK_BEFORE_UNLOAD, []], ['main', Model::HOOK_AFTER_UNLOAD, []], @@ -164,6 +184,7 @@ public function testInsert(): void ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], ['main', Model::HOOK_AFTER_SAVE, [false]], + ['main', '<<<'], ], $this->hookLog); $this->assertSame(3, $m->table->loadBy('name', 'Karl')->getId()); @@ -191,12 +212,14 @@ public function testUpdate(): void ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', '>>>'], ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [true]], ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', '>>>'], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [true]], ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], @@ -204,8 +227,10 @@ public function testUpdate(): void ['inner', Persistence\Sql::HOOK_AFTER_UPDATE_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['inner', Model::HOOK_AFTER_SAVE, [true]], + ['inner', '<<<'], ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['main', Model::HOOK_AFTER_SAVE, [true]], + ['main', '<<<'], ], $this->hookLog); $this->assertSameExportUnordered([ @@ -226,17 +251,21 @@ public function testDelete(): void ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', '>>>'], ['main', Model::HOOK_BEFORE_DELETE, []], ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', '>>>'], ['inner', Model::HOOK_BEFORE_DELETE, []], ['inner', Persistence\Sql::HOOK_BEFORE_DELETE_QUERY, [Query::class]], ['inner', Persistence\Sql::HOOK_AFTER_DELETE_QUERY, [Query::class, DbalResult::class]], ['inner', Model::HOOK_AFTER_DELETE, []], + ['inner', '<<<'], ['inner', Model::HOOK_BEFORE_UNLOAD, []], ['inner', Model::HOOK_AFTER_UNLOAD, []], ['main', Model::HOOK_AFTER_DELETE, []], + ['main', '<<<'], ['main', Model::HOOK_BEFORE_UNLOAD, []], ['main', Model::HOOK_AFTER_UNLOAD, []], ], $this->hookLog); From a96f9d8c6fa8d375bff57984d944e7d666d7d497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 19 Jan 2022 15:43:35 +0100 Subject: [PATCH 095/151] fixed mysql server 8.0.28 released --- tests/ModelAggregateTest.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index f2b487c96..cc19fc3c0 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -8,7 +8,6 @@ use Atk4\Data\Model\Scope; use Atk4\Data\Model\Scope\Condition; use Atk4\Data\Schema\TestCase; -use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\SqlitePlatform; class ModelAggregateTest extends TestCase @@ -250,14 +249,6 @@ public function testGroupSelectScope(): void // TODO Sqlite bind param does not work, expr needed, even if casted to float with DBAL type (comparison works only if casted to/bind as int) $numExpr = $this->getDatabasePlatform() instanceof SqlitePlatform ? $aggregate->expr('4') : 4; $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', $numExpr)); - - // MySQL Server v8.0.27 (and possibly some lower versions) returns a wrong result - // see https://bugs.mysql.com/bug.php?id=106063 - // remove this fix once v8.0.28 is released and Docker image is available - if ($this->getDatabasePlatform() instanceof MySQLPlatform) { - array_pop($scope->elements); - } - $aggregate->addCondition($scope); $this->assertSame( From f451c5cc562d53c12bd0ccb8fa2a41033cd19d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 23 Jan 2022 14:39:08 +0100 Subject: [PATCH 096/151] drop withAggregateField method --- docs/aggregates.rst | 36 ++++++++++++++++++----------------- src/Model/Aggregate.php | 12 ------------ src/Model/AggregatesTrait.php | 21 -------------------- 3 files changed, 19 insertions(+), 50 deletions(-) diff --git a/docs/aggregates.rst b/docs/aggregates.rst index ac982e07c..ce7eed53a 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -16,28 +16,30 @@ Grouping Aggregate model can be used for grouping:: - $aggregate = $orders->groupBy(['country_id']); + $aggregate = $orders->groupBy(['country_id']); -`$aggregate` above is a new object that is most appropriate for the model's persistence and which can be manipulated +`$aggregate` above is a new object that is most appropriate for the model's persistence and which can be manipulated in various ways to fine-tune aggregation. Below is one sample use:: - $aggregate = $orders->withAggregateField('country')->groupBy(['country_id'], [ - 'count' => ['expr' => 'count(*)', 'type' => 'integer'], - 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] - ], - ); + $aggregate = new Aggregate($orders); + $aggregate->addField('country'); + $aggregate->groupBy(['country_id'], [ + 'count' => ['expr' => 'count(*)', 'type' => 'integer'], + 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] + ], + ); - // $aggregate will have following rows: - // ['country' => 'UK', 'count' => 20, 'total_amount' => 123.20]; - // .. + // $aggregate will have following rows: + // ['country' => 'UK', 'count' => 20, 'total_amount' => 123.20]; + // .. Below is how opening balance can be built:: - $ledger = new GeneralLedger($db); - $ledger->addCondition('date', '<', $from); - - // we actually need grouping by nominal - $ledger->groupBy(['nominal_id'], [ - 'opening_balance' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] - ]); + $ledger = new GeneralLedger($db); + $ledger->addCondition('date', '<', $from); + + // we actually need grouping by nominal + $ledger->groupBy(['nominal_id'], [ + 'opening_balance' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] + ]); diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index b68cea4f3..cfa7bf76a 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -110,18 +110,6 @@ public function getRef(string $link): Reference return $ref; } - /** - * TODO this method should be removed, we do not offer similar methods for standard Model. - * - * @return $this - */ - public function withAggregateField(string $name, $seed = []): Model - { - static::addField($name, $seed); - - return $this; - } - /** * Adds new field into model. * diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php index 062196ee6..591915681 100644 --- a/src/Model/AggregatesTrait.php +++ b/src/Model/AggregatesTrait.php @@ -11,27 +11,6 @@ */ trait AggregatesTrait { - /** - * Method to enable commutative usage of methods enabling both of below - * Resulting in Aggregate on $model. - * - * $model->groupBy(['abc'])->withAggregateField('xyz'); - * - * and - * - * $model->withAggregateField('xyz')->groupBy(['abc']); - * - * @param array|object $seed - * - * @return Aggregate - * - * @see Aggregate::withAggregateField. - */ - public function withAggregateField(string $name, $seed = []): Model - { - return (new Aggregate($this))->withAggregateField($name, $seed); - } - /** * Specify a single field or array of fields on which we will group model. * From e9edb8b3f69a43a83eca50bfa2cae7765778d4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 23 Jan 2022 14:43:06 +0100 Subject: [PATCH 097/151] drop AggregatesTrait trait --- docs/aggregates.rst | 2 +- src/Model.php | 1 - src/Model/Aggregate.php | 6 +++++- src/Model/AggregatesTrait.php | 27 --------------------------- tests/ModelAggregateTest.php | 2 +- 5 files changed, 7 insertions(+), 31 deletions(-) delete mode 100644 src/Model/AggregatesTrait.php diff --git a/docs/aggregates.rst b/docs/aggregates.rst index ce7eed53a..f4a8d4c00 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -16,7 +16,7 @@ Grouping Aggregate model can be used for grouping:: - $aggregate = $orders->groupBy(['country_id']); + $aggregate = new Aggregate($orders)->groupBy(['country_id']); `$aggregate` above is a new object that is most appropriate for the model's persistence and which can be manipulated in various ways to fine-tune aggregation. Below is one sample use:: diff --git a/src/Model.php b/src/Model.php index 03c2f033b..046fa802f 100644 --- a/src/Model.php +++ b/src/Model.php @@ -48,7 +48,6 @@ class Model implements \IteratorAggregate use InitializerTrait { init as private _init; } - use Model\AggregatesTrait; use Model\JoinsTrait; use Model\ReferencesTrait; use Model\UserActionsTrait; diff --git a/src/Model/Aggregate.php b/src/Model/Aggregate.php index cfa7bf76a..334bfa16f 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/Aggregate.php @@ -68,9 +68,13 @@ public function __construct(Model $baseModel, array $defaults = []) } /** + * Specify a single field or array of fields on which we will group model. + * + * @param array $aggregateExpressions Array of aggregate expressions with alias as key + * * @return $this */ - public function groupBy(array $fields, array $aggregateExpressions = []): Model + public function groupBy(array $fields, array $aggregateExpressions = []) { $this->groupByFields = array_unique(array_merge($this->groupByFields, $fields)); diff --git a/src/Model/AggregatesTrait.php b/src/Model/AggregatesTrait.php deleted file mode 100644 index 591915681..000000000 --- a/src/Model/AggregatesTrait.php +++ /dev/null @@ -1,27 +0,0 @@ - $aggregateExpressions Array of aggregate expressions with alias as key - * - * @return Aggregate - * - * @see Aggregate::groupBy - */ - public function groupBy(array $fields, array $aggregateExpressions = []): Model - { - return (new Aggregate($this))->groupBy($fields, $aggregateExpressions); - } -} diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index cc19fc3c0..13c117241 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -52,7 +52,7 @@ protected function createInvoiceAggregate(): Aggregate public function testGroupBy(): void { - $invoiceAggregate = $this->createInvoice()->groupBy(['client_id'], [ + $invoiceAggregate = (new Aggregate($this->createInvoice()))->groupBy(['client_id'], [ 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); From 254bb676960b05e99a37bd0fa1a6f1fe1a2a5d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 30 Jan 2022 11:42:13 +0100 Subject: [PATCH 098/151] rename Aggregate to AggregateModel --- docs/aggregates.rst | 10 +++++----- src/Model/{Aggregate.php => AggregateModel.php} | 11 ++++------- tests/ModelAggregateTest.php | 8 ++++---- 3 files changed, 13 insertions(+), 16 deletions(-) rename src/Model/{Aggregate.php => AggregateModel.php} (93%) diff --git a/docs/aggregates.rst b/docs/aggregates.rst index f4a8d4c00..9c4125f45 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -7,21 +7,21 @@ Model Aggregates .. php:namespace:: Atk4\Data\Model -.. php:class:: Aggregate +.. php:class:: AggregateModel -In order to create model aggregates the Aggregate model needs to be used: +In order to create model aggregates the AggregateModel model needs to be used: Grouping -------- -Aggregate model can be used for grouping:: +AggregateModel model can be used for grouping:: - $aggregate = new Aggregate($orders)->groupBy(['country_id']); + $aggregate = new AggregateModel($orders)->groupBy(['country_id']); `$aggregate` above is a new object that is most appropriate for the model's persistence and which can be manipulated in various ways to fine-tune aggregation. Below is one sample use:: - $aggregate = new Aggregate($orders); + $aggregate = new AggregateModel($orders); $aggregate->addField('country'); $aggregate->groupBy(['country_id'], [ 'count' => ['expr' => 'count(*)', 'type' => 'integer'], diff --git a/src/Model/Aggregate.php b/src/Model/AggregateModel.php similarity index 93% rename from src/Model/Aggregate.php rename to src/Model/AggregateModel.php index 334bfa16f..e2a384d41 100644 --- a/src/Model/Aggregate.php +++ b/src/Model/AggregateModel.php @@ -14,10 +14,10 @@ use Atk4\Data\Reference; /** - * Aggregate model allows you to query using "group by" clause on your existing model. + * AggregateModel model allows you to query using "group by" clause on your existing model. * It's quite simple to set up. * - * $aggregate = new Aggregate($mymodel); + * $aggregate = new AggregateModel($mymodel); * $aggregate->groupBy(['first','last'], ['salary'=>'sum([])']; * * your resulting model will have 3 fields: @@ -31,9 +31,6 @@ * If this field exist in the original model it will be added and you'll get exception otherwise. Finally you are * permitted to add expressions. * - * The base model must not be Union model or another Aggregate model, however it's possible to use Aggregate model as nestedModel inside Union model. - * Union model implements identical grouping rule on its own. - * * You can also pass seed (for example field type) when aggregating: * $aggregate->groupBy(['first', 'last'], ['salary' => ['sum([])', 'type' => 'atk4_money']]; * @@ -42,7 +39,7 @@ * * @method Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model */ -class Aggregate extends Model +class AggregateModel extends Model { /** @const string */ public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; @@ -180,7 +177,7 @@ public function action(string $mode, array $args = []) case 'fx': return parent::action($mode, $args); default: - throw (new Exception('Aggregate model does not support this action')) + throw (new Exception('AggregateModel model does not support this action')) ->addMoreInfo('mode', $mode); } } diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 13c117241..894f39bb7 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -4,7 +4,7 @@ namespace Atk4\Data\Tests; -use Atk4\Data\Model\Aggregate; +use Atk4\Data\Model\AggregateModel; use Atk4\Data\Model\Scope; use Atk4\Data\Model\Scope\Condition; use Atk4\Data\Schema\TestCase; @@ -45,14 +45,14 @@ protected function createInvoice(): Model\Invoice return $invoice; } - protected function createInvoiceAggregate(): Aggregate + protected function createInvoiceAggregate(): AggregateModel { - return new Aggregate($this->createInvoice()); + return new AggregateModel($this->createInvoice()); } public function testGroupBy(): void { - $invoiceAggregate = (new Aggregate($this->createInvoice()))->groupBy(['client_id'], [ + $invoiceAggregate = (new AggregateModel($this->createInvoice()))->groupBy(['client_id'], [ 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); From 37fffd74192e508a5b18676d2b4ff25fd22ac441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 30 Jan 2022 11:56:11 +0100 Subject: [PATCH 099/151] fix merge --- src/Model/Union.php | 8 ++++++-- tests/ReportTest.php | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Model/Union.php b/src/Model/Union.php index 94a0e7bb7..cd869608f 100644 --- a/src/Model/Union.php +++ b/src/Model/Union.php @@ -114,9 +114,13 @@ public function addNestedModel(Model $model, array $fieldMap = []): Model } /** - * @phpstan-return Model + * Specify a single field or array of fields on which we will group model. + * + * @param array $aggregateExpressions Array of aggregate expressions with alias as key + * + * @return $this */ - public function groupBy(array $fields, array $aggregateExpressions = []): Model // @phpstan-ignore-line + public function groupBy(array $fields, array $aggregateExpressions = []): Model { $this->aggregate = $aggregateExpressions; $this->group = $fields; diff --git a/tests/ReportTest.php b/tests/ReportTest.php index 7fadc553b..516f20e54 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -4,7 +4,7 @@ namespace Atk4\Data\Tests; -use Atk4\Data\Model\Aggregate; +use Atk4\Data\Model\AggregateModel; use Atk4\Data\Schema\TestCase; class ReportTest extends TestCase @@ -34,11 +34,11 @@ protected function setUp(): void $this->setDb($this->init_db); } - protected function createInvoiceAggregate(): Aggregate + protected function createInvoiceAggregate(): AggregateModel { $invoice = new Model\Invoice($this->db); $invoice->getRef('client_id')->addTitle(); - $invoiceAggregate = new Aggregate($invoice); + $invoiceAggregate = new AggregateModel($invoice); $invoiceAggregate->addField('client'); return $invoiceAggregate; From 91db9f8bc0219abc5a006015d46b2f8e4084bf36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 30 Jan 2022 12:07:58 +0100 Subject: [PATCH 100/151] rename Union to UnionModel --- README.md | 1 - docs/unions.rst | 24 ++++++++++++------------ src/Model/{Union.php => UnionModel.php} | 18 +++++++++--------- tests/Model/Transaction.php | 4 ++-- tests/ModelUnionTest.php | 4 ++-- 5 files changed, 25 insertions(+), 26 deletions(-) rename src/Model/{Union.php => UnionModel.php} (95%) diff --git a/README.md b/README.md index 577c5e472..72a626253 100644 --- a/README.md +++ b/README.md @@ -485,7 +485,6 @@ Most ORMs hard-code features like soft-delete, audit-log, timestamps. In Agile D We are still working on our Extension library but we plan to include: - [Audit Log](https://www.agiletoolkit.org/data/extensions/audit) - record all operations in a model (as well as previous field values), offers a reliable Undo functionality. -- [Reporting](https://www.agiletoolkit.org/data/extensions/report) - offers UnionModel - ACL - flexible system to restrict access to certain records, fields or models based on permissions of your logged-in user or custom logic. - Filestore - allow you to work with files inside your model. Files are actually diff --git a/docs/unions.rst b/docs/unions.rst index 2265f3a47..d5a3ef1a3 100644 --- a/docs/unions.rst +++ b/docs/unions.rst @@ -7,9 +7,9 @@ Model Unions .. php:namespace:: Atk4\Data\Model -.. php:class:: Union +.. php:class:: UnionModel -In some cases data from multiple models need to be combined. In this case the Union model comes very handy. +In some cases data from multiple models need to be combined. In this case the UnionModel model comes very handy. In the case used below Client model schema may have multiple invoices and multiple payments. Payment is not related to the invoice.:: class Client extends \Atk4\Data\Model { @@ -49,10 +49,10 @@ Then data can be queried:: $unionPaymentInvoice->export(); -Union Model Fields +Define Fields ------------------ -Below is an example of 3 different ways to define fields for the Union model:: +Below is an example of 3 different ways to define fields for the UnionModel model:: // Will link the "name" field with all the nested models. $unionPaymentInvoice->addField('client_id'); @@ -60,7 +60,7 @@ Below is an example of 3 different ways to define fields for the Union model:: // Expression will not affect nested models in any way $unionPaymentInvoice->addExpression('name_capital', 'upper([name])'); - // Union model can be joined with extra tables and define some fields from those joins + // UnionModel model can be joined with extra tables and define some fields from those joins $unionPaymentInvoice ->join('client', 'client_id') ->addField('client_name', 'name'); @@ -70,7 +70,7 @@ Below is an example of 3 different ways to define fields for the Union model:: Field Mapping ------------- -Sometimes the field that is defined in the Union model may be named differently inside nested models. +Sometimes the field that is defined in the UnionModel model may be named differently inside nested models. E.g. Invoice has field "description" and payment has field "note". When defining a nested model a field map array needs to be specified:: @@ -78,7 +78,7 @@ When defining a nested model a field map array needs to be specified:: $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); $unionPaymentInvoice->addField('description'); -The key of the field map array must match the Union field. The value is an expression. (See :ref:`Model`). +The key of the field map array must match the UnionModel field. The value is an expression. (See :ref:`Model`). This format can also be used to reverse sign on amounts. When we are creating "Transactions", then invoices would be subtracted from the amount, while payments will be added:: @@ -97,24 +97,24 @@ Should more flexibility be needed, more expressions (or fields) can be added dir A new field "type" has been added that will be defined as a static constant. -Referencing an Union Model +Referencing an UnionModel Model -------------------------- -Like any other model, Union model can be assigned through a reference. In the case here one Client can have multiple transactions. +Like any other model, UnionModel model can be assigned through a reference. In the case here one Client can have multiple transactions. Initially a related union can be defined:: $client->hasMany('Transaction', new Transaction()); -When condition is added on an Union model it will send it down to every nested model. This way the resulting SQL query remains optimized. +When condition is added on an UnionModel model it will send it down to every nested model. This way the resulting SQL query remains optimized. The exception is when field is not mapped to nested model (if it's an Expression or associated with a Join). -In most cases optimization on the query and Union model is not necessary as it will be done automatically. +In most cases optimization on the query and UnionModel model is not necessary as it will be done automatically. Grouping Results ---------------- -Union model has also a built-in grouping support:: +UnionModel model has also a built-in grouping support:: $unionPaymentInvoice->groupBy('client_id', ['amount' => 'sum']); diff --git a/src/Model/Union.php b/src/Model/UnionModel.php similarity index 95% rename from src/Model/Union.php rename to src/Model/UnionModel.php index cd869608f..5868fc1f5 100644 --- a/src/Model/Union.php +++ b/src/Model/UnionModel.php @@ -12,7 +12,7 @@ use Atk4\Data\Persistence\Sql\Query; /** - * Union model combines multiple nested models through a UNION in order to retrieve + * UnionModel model combines multiple nested models through a UNION in order to retrieve * it's value set. The beauty of this class is that it will add fields transparently * and will map them appropriately from the nested model if you request * those fields from the union model. @@ -24,20 +24,20 @@ * * @method Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model */ -class Union extends Model +class UnionModel extends Model { /** @const string */ public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; /** - * Union model should always be read-only. + * UnionModel should always be read-only. * * @var bool */ public $read_only = true; /** - * Union normally does not have ID field. Setting this to null will + * UnionModel normally does not have ID field. Setting this to null will * disable various per-id operations, such as load(). * * If you can define unique ID field, you can specify it inside your @@ -153,7 +153,7 @@ public function groupBy(array $fields, array $aggregateExpressions = []): Model } /** - * If Union model has such field, then add condition to it. + * If UnionModel has such field, then add condition to it. * Otherwise adds condition to all nested models. * * @param mixed $key @@ -169,7 +169,7 @@ public function addCondition($key, $operator = null, $value = null, $forceNested return parent::addCondition($key); } - // if Union model has such field, then add condition to it + // if UnionModel has such field, then add condition to it if ($this->hasField($key) && !$forceNested) { return parent::addCondition(...func_get_args()); } @@ -271,7 +271,7 @@ public function action($mode, $args = []) break; default: - throw (new Exception('Union model does not support this action')) + throw (new Exception('UnionModel model does not support this action')) ->addMoreInfo('mode', $mode); } @@ -306,7 +306,7 @@ public function getSubQuery(array $fields): Query $queryFieldExpressions = []; foreach ($fields as $fieldName) { try { - // Union can be joined with additional table/query + // UnionModel can be joined with additional table/query // We don't touch those fields if (!$this->hasField($fieldName)) { @@ -321,7 +321,7 @@ public function getSubQuery(array $fields): Query continue; } - // Union can have some fields defined as expressions. We don't touch those either. + // UnionModel can have some fields defined as expressions. We don't touch those either. // Imants: I have no idea why this condition was set, but it's limiting our ability // to use expression fields in mapping if ($field instanceof SqlExpressionField && !isset($this->aggregate[$fieldName])) { diff --git a/tests/Model/Transaction.php b/tests/Model/Transaction.php index 108693d36..87a64736a 100644 --- a/tests/Model/Transaction.php +++ b/tests/Model/Transaction.php @@ -4,9 +4,9 @@ namespace Atk4\Data\Tests\Model; -use Atk4\Data\Model\Union; +use Atk4\Data\Model\UnionModel; -class Transaction extends Union +class Transaction extends UnionModel { /** @var Invoice */ public $nestedInvoice; diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index b681f007b..569e442e9 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -171,7 +171,7 @@ public function testBasics(): void ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); - // Transaction is Union Model + // Transaction is UnionModel Model $client->hasMany('Transaction', ['model' => $transaction]); $this->assertSameExportUnordered([ @@ -192,7 +192,7 @@ public function testBasics(): void ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); - // Transaction is Union Model + // Transaction is UnionModel Model $client->hasMany('Transaction', ['model' => $transaction]); $this->assertSameExportUnordered([ From 2a4f6df8eed159c8861ab8e5eada1cd708312cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 10 Jan 2022 16:52:17 +0100 Subject: [PATCH 101/151] DEBUG add infinite recursion detection --- src/Persistence/Sql/Expression.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Persistence/Sql/Expression.php b/src/Persistence/Sql/Expression.php index 014448345..68a3d5f3a 100644 --- a/src/Persistence/Sql/Expression.php +++ b/src/Persistence/Sql/Expression.php @@ -363,6 +363,17 @@ protected function isUnescapablePattern($value): bool private function _render(): array { + // DEBUG, remove before merge or rewrite without debug_backtrace() with much higher limit + // some tests for PostgreSQL & MSSQL need stack trace depth more than 500 + $stackTraceLimit = isset($this->connection) + && \Closure::bind(fn ($v) => isset($v->connection), null, Connection::class)($this->connection) + && $this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform + ? 200 + : 1000; + if (count(debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, $stackTraceLimit)) === $stackTraceLimit) { + throw new Exception('Infinite recursion detected'); + } + // - [xxx] = param // - {xxx} = escape // - {{xxx}} = escapeSoft From 4d827b8d87877514a17eb9ce1a975e9bca83000d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 30 Jan 2022 12:46:26 +0100 Subject: [PATCH 102/151] drop wrong union grouping pushdown --- docs/unions.rst | 10 --- src/Model/UnionModel.php | 148 +++++---------------------------------- tests/ModelUnionTest.php | 31 ++++---- 3 files changed, 34 insertions(+), 155 deletions(-) diff --git a/docs/unions.rst b/docs/unions.rst index d5a3ef1a3..d820e2461 100644 --- a/docs/unions.rst +++ b/docs/unions.rst @@ -110,13 +110,3 @@ When condition is added on an UnionModel model it will send it down to every nes The exception is when field is not mapped to nested model (if it's an Expression or associated with a Join). In most cases optimization on the query and UnionModel model is not necessary as it will be done automatically. - -Grouping Results ----------------- - -UnionModel model has also a built-in grouping support:: - - $unionPaymentInvoice->groupBy('client_id', ['amount' => 'sum']); - -When specifying a grouping field and it is associated with nested models then grouping will be enabled on every nested model. - diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 5868fc1f5..911b39948 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -4,6 +4,7 @@ namespace Atk4\Data\Model; +use Atk4\Core\Exception as CoreException; use Atk4\Data\Exception; use Atk4\Data\Field; use Atk4\Data\Field\SqlExpressionField; @@ -50,32 +51,15 @@ class UnionModel extends Model /** * Contain array of array containing model and mappings. * - * $union = [ [ $model1, ['amount'=>'total_gross'] ] , [$model2, []] ]; + * $union = [ + * [$model1, ['amount' => 'total_gross'] ], + * [$model2, []] + * ]; * * @var array */ public $union = []; - /** - * When aggregation happens, this field will contain list of fields - * we use in groupBy. Multiple fields can be in the array. All - * the remaining fields will be hidden (marked as system()) and - * have their "aggregates" added into the selectQuery (if possible). - * - * @var array - */ - public $group = []; - - /** - * When grouping, the functions will be applied as per aggregate - * fields, e.g. 'balance'=>['sum', 'amount']. - * - * You can also use Expression instead of array. - * - * @var array - */ - public $aggregate = []; - /** @var string Derived table alias */ public $table = '_tu'; @@ -114,46 +98,7 @@ public function addNestedModel(Model $model, array $fieldMap = []): Model } /** - * Specify a single field or array of fields on which we will group model. - * - * @param array $aggregateExpressions Array of aggregate expressions with alias as key - * - * @return $this - */ - public function groupBy(array $fields, array $aggregateExpressions = []): Model - { - $this->aggregate = $aggregateExpressions; - $this->group = $fields; - - foreach ($aggregateExpressions as $fieldName => $seed) { - $seed = (array) $seed; - - $field = $this->hasField($fieldName) ? $this->getField($fieldName) : null; - - // first element of seed should be expression itself - if (isset($seed[0]) && is_string($seed[0])) { - $seed[0] = $this->expr($seed[0], $field ? [$field] : null); - } - - if ($field) { - $this->removeField($fieldName); - } - - $this->addExpression($fieldName, $seed); - } - - foreach ($this->union as [$nestedModel, $fieldMap]) { - if ($nestedModel instanceof self) { - $nestedModel->aggregate = $aggregateExpressions; - $nestedModel->group = $fields; - } - } - - return $this; - } - - /** - * If UnionModel has such field, then add condition to it. + * If UnionModel model has such field, then add condition to it. * Otherwise adds condition to all nested models. * * @param mixed $key @@ -202,8 +147,9 @@ public function addCondition($key, $operator = null, $value = null, $forceNested break; } - } catch (\Atk4\Core\Exception $e) { - throw $e->addMoreInfo('nestedModel', get_class($nestedModel)); + } catch (CoreException $e) { + throw $e + ->addMoreInfo('nestedModel', $nestedModel); } } @@ -211,14 +157,9 @@ public function addCondition($key, $operator = null, $value = null, $forceNested } /** - * Execute action. - * - * @param string $mode - * @param array $args - * * @return Query */ - public function action($mode, $args = []) + public function action(string $mode, array $args = []) { $subquery = null; switch ($mode) { @@ -233,10 +174,6 @@ public function action($mode, $args = []) $subquery = $this->getSubQuery($fields); $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->table_alias ?? $this->table); - foreach ($this->group as $group) { - $query->group($group); - } - $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); return $query; @@ -324,57 +261,26 @@ public function getSubQuery(array $fields): Query // UnionModel can have some fields defined as expressions. We don't touch those either. // Imants: I have no idea why this condition was set, but it's limiting our ability // to use expression fields in mapping - if ($field instanceof SqlExpressionField && !isset($this->aggregate[$fieldName])) { - continue; - } - - // if we group we do not select non-aggregate fields - // TODO this breaks composide design - remove this if statement, fields must be manually removed or added to grouping! - if (count($this->group) > 0 && !in_array($fieldName, $this->group, true) && !isset($this->aggregate[$fieldName])) { + if ($field instanceof SqlExpressionField /*&& !isset($this->aggregate[$fieldName])*/) { continue; } $fieldExpression = $this->getFieldExpr($nestedModel, $fieldName, $fieldMap[$fieldName] ?? null); - if (isset($this->aggregate[$fieldName])) { - $seed = (array) $this->aggregate[$fieldName]; - - // first element of seed should be expression itself - $fieldExpression = $nestedModel->expr($seed[0], [$fieldExpression]); - } - $queryFieldExpressions[$fieldName] = $fieldExpression; - } catch (\Atk4\Core\Exception $e) { - throw $e->addMoreInfo('nestedModel', get_class($nestedModel)); + } catch (CoreException $e) { + throw $e + ->addMoreInfo('nestedModel', $nestedModel); } } // now prepare query $query = $this->persistence->action($nestedModel, 'select', [false]); - if ($nestedModel instanceof self) { - $subquery = $nestedModel->getSubQuery($fields); - //$query = parent::action($mode, $args); - $query->reset('table')->table($subquery); - - foreach ($nestedModel->group as $group) { - $query->group($group); - } - } - foreach ($queryFieldExpressions as $fAlias => $fExpr) { $query->field($fExpr, $fAlias); } - // also for sub-queries - foreach ($this->group as $group) { - if (isset($fieldMap[$group])) { - $query->group($nestedModel->expr($fieldMap[$group])); - } elseif ($nestedModel->hasField($group)) { - $query->group($nestedModel->getField($group)->short_name /* TODO short_name should be used by DSQL automatically when in GROUP BY, HAVING, ... */); - } - } - // subquery should not be wrapped in parenthesis, SQLite is especially picky about that $query->wrapInParentheses = false; @@ -415,30 +321,10 @@ public function getSubAction(string $action, array $actionArgs = []): Query return $unionQuery; } - // {{{ Debug Methods - - /** - * Returns array with useful debug info for var_dump. - */ public function __debugInfo(): array { - $unionModels = []; - foreach ($this->union as [$nestedModel, $fieldMap]) { - $unionModels[get_class($nestedModel)] = array_merge( - ['fieldMap' => $fieldMap], - $nestedModel->__debugInfo() - ); - } - - return array_merge( - parent::__debugInfo(), - [ - 'group' => $this->group, - 'aggregate' => $this->aggregate, - 'unionModels' => $unionModels, - ] - ); + return array_merge(parent::__debugInfo(), [ + 'unionModels' => $this->union, + ]); } - - // }}} } diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 569e442e9..cf907e5df 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -4,8 +4,8 @@ namespace Atk4\Data\Tests; +use Atk4\Data\Model\AggregateModel; use Atk4\Data\Schema\TestCase; -use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\SQLServerPlatform; class ModelUnionTest extends TestCase @@ -206,19 +206,21 @@ public function testGrouping1(): void { $transaction = $this->createTransaction(); - $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate = new AggregateModel($transaction); + $transactionAggregate->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $this->assertSameSql( - 'select "name", sum("amount") "amount" from (select "name" "name", sum("amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "_tu" group by "name"', + 'select "name", sum("amount") "amount" from (select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", "amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "_tu") "_tm" group by "name"', $transactionAggregate->action('select', [['name', 'amount']])->render()[0] ); $transaction = $this->createSubtractInvoiceTransaction(); - $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate = new AggregateModel($transaction); + $transactionAggregate->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameSql( - 'select "name", sum("amount") "amount" from (select "name" "name", sum(-"amount") "amount" from "invoice" group by "name" UNION ALL select "name" "name", sum("amount") "amount" from "payment" group by "name") "_tu" group by "name"', + 'select "name", sum("amount") "amount" from (select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", -"amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "_tu") "_tm" group by "name"', $transactionAggregate->action('select', [['name', 'amount']])->render()[0] ); } @@ -232,7 +234,8 @@ public function testGrouping2(): void $transaction = $this->createTransaction(); $transaction->removeField('client_id'); // TODO enable later, test failing with MSSQL $transaction->setOrder('name'); - $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate = new AggregateModel($transaction); + $transactionAggregate->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); $transactionAggregate->setOrder('name'); $this->assertSame([ @@ -245,7 +248,8 @@ public function testGrouping2(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->removeField('client_id'); // TODO enable later, test failing with MSSQL $transaction->setOrder('name'); - $transactionAggregate = $transaction->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate = new AggregateModel($transaction); + $transactionAggregate->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); $transactionAggregate->setOrder('name'); $this->assertSame([ @@ -262,19 +266,17 @@ public function testGrouping2(): void */ public function testSubGroupingByExpressions(): void { - if ($this->getDatabasePlatform() instanceof OraclePlatform) { // TODO - $this->markTestIncomplete('TODO - for some reasons Oracle does not accept the query'); - } - $transaction = $this->createTransaction(); $transaction->nestedInvoice->addExpression('type', '\'invoice\''); $transaction->nestedPayment->addExpression('type', '\'payment\''); $transaction->addField('type'); - $transactionAggregate = $transaction->groupBy(['type'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate = new AggregateModel($transaction); + $transactionAggregate->groupBy(['type'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + // TODO subselects should not select "client" and "name" fields $this->assertSameSql( - 'select "client_id", "name", "type", sum("amount") "amount" from (select (\'invoice\') "type", sum("amount") "amount" from "invoice" group by "type" UNION ALL select (\'payment\') "type", sum("amount") "amount" from "payment" group by "type") "_tu" group by "type"', + 'select "type", sum("amount") "amount" from (select "client_id", "name", "amount", "type" from (select "client_id" "client_id", "name" "name", "amount" "amount", (\'invoice\') "type" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount", (\'payment\') "type" from "payment") "_tu") "_tm" group by "type"', $transactionAggregate->action('select')->render()[0] ); @@ -292,7 +294,8 @@ public function testSubGroupingByExpressions(): void $transaction->nestedPayment->addExpression('type', '\'payment\''); $transaction->addField('type'); - $transactionAggregate = $transaction->groupBy(['type'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate = new AggregateModel($transaction); + $transactionAggregate->groupBy(['type'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); $this->assertSameExportUnordered([ ['type' => 'invoice', 'amount' => -23.0], From cdbd07a4235e3a89e73732f7c09da547886da9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 30 Jan 2022 12:10:27 +0100 Subject: [PATCH 103/151] fix doc --- README.md | 4 ++-- docs/overview.rst | 4 ++-- src/Model.php | 2 +- src/Model/AggregateModel.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 577c5e472..81aa93595 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ $grid->setModel($data); $html = $grid->render(); ``` -Or if you want to display them as a Chart, using https://github.com/atk4/chart and https://github.com/atk4/report +Or if you want to display them as a Chart using https://github.com/atk4/chart ``` php $chart = new \Atk4\Chart\BarChart(); @@ -190,7 +190,7 @@ $data = new JobReport($db); // BarChart wants aggregated data $data->addExpression('month', 'month([date])'); -$aggregate = new \Atk4\Report\GroupModel($data); +$aggregate = new AggregateModel($data); $aggregate->groupBy('month', ['profit_margin' => 'sum']); // associate presentation with data diff --git a/docs/overview.rst b/docs/overview.rst index 8d31cf293..e38a81ad7 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -204,8 +204,8 @@ https://github.com/atk4/report/blob/develop/src/GroupModel.php This code is specific to SQL databases, but can be used with any Model, so in order to use grouping with Agile Data, your code would be:: - $m = new \Atk4\Report\GroupModel(new Sale($db)); - $m->groupBy(['contractor_to', 'type'], [ // groups by 2 columns + $aggregate = new AggregateModel(new Sale($db)); + $aggregate->groupBy(['contractor_to', 'type'], [ // groups by 2 columns 'c' => 'count(*)', // defines aggregate formulas for fields 'qty' => 'sum([])', // [] refers back to qty 'total' => 'sum([amount])', // can specify any field here diff --git a/src/Model.php b/src/Model.php index 0360fa623..d42bc0484 100644 --- a/src/Model.php +++ b/src/Model.php @@ -1089,7 +1089,7 @@ public function addWith(self $model, string $alias, array $mapping = [], bool $r } /** - * Set order for model records. Multiple calls. + * Set order for model records. Multiple calls are allowed. * * @param string|array $field * @param string $direction "asc" or "desc" diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index e2a384d41..b909d0f10 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -65,7 +65,7 @@ public function __construct(Model $baseModel, array $defaults = []) } /** - * Specify a single field or array of fields on which we will group model. + * Specify a single field or array of fields on which we will group model. Multiple calls are allowed. * * @param array $aggregateExpressions Array of aggregate expressions with alias as key * From da5856f41e3a4534ab04ff079cafb7e7792ddba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 30 Jan 2022 13:01:00 +0100 Subject: [PATCH 104/151] rename groupBy to setGroupBy as it mutates the model --- README.md | 2 +- docs/aggregates.rst | 6 +++--- docs/overview.rst | 2 +- src/Model/AggregateModel.php | 6 +++--- tests/ModelAggregateTest.php | 30 +++++++++++++++--------------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 81aa93595..44a69fc19 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ $data = new JobReport($db); // BarChart wants aggregated data $data->addExpression('month', 'month([date])'); $aggregate = new AggregateModel($data); -$aggregate->groupBy('month', ['profit_margin' => 'sum']); +$aggregate->setGroupBy('month', ['profit_margin' => 'sum']); // associate presentation with data $chart->setModel($aggregate, ['month', 'profit_margin']); diff --git a/docs/aggregates.rst b/docs/aggregates.rst index 9c4125f45..ae1f4f360 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -16,14 +16,14 @@ Grouping AggregateModel model can be used for grouping:: - $aggregate = new AggregateModel($orders)->groupBy(['country_id']); + $aggregate = new AggregateModel($orders)->setGroupBy(['country_id']); `$aggregate` above is a new object that is most appropriate for the model's persistence and which can be manipulated in various ways to fine-tune aggregation. Below is one sample use:: $aggregate = new AggregateModel($orders); $aggregate->addField('country'); - $aggregate->groupBy(['country_id'], [ + $aggregate->setGroupBy(['country_id'], [ 'count' => ['expr' => 'count(*)', 'type' => 'integer'], 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] ], @@ -39,7 +39,7 @@ Below is how opening balance can be built:: $ledger->addCondition('date', '<', $from); // we actually need grouping by nominal - $ledger->groupBy(['nominal_id'], [ + $ledger->setGroupBy(['nominal_id'], [ 'opening_balance' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] ]); diff --git a/docs/overview.rst b/docs/overview.rst index e38a81ad7..de4f3f69f 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -205,7 +205,7 @@ This code is specific to SQL databases, but can be used with any Model, so in order to use grouping with Agile Data, your code would be:: $aggregate = new AggregateModel(new Sale($db)); - $aggregate->groupBy(['contractor_to', 'type'], [ // groups by 2 columns + $aggregate->setGroupBy(['contractor_to', 'type'], [ // groups by 2 columns 'c' => 'count(*)', // defines aggregate formulas for fields 'qty' => 'sum([])', // [] refers back to qty 'total' => 'sum([amount])', // can specify any field here diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index b909d0f10..edc8a54f9 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -18,7 +18,7 @@ * It's quite simple to set up. * * $aggregate = new AggregateModel($mymodel); - * $aggregate->groupBy(['first','last'], ['salary'=>'sum([])']; + * $aggregate->setGroupBy(['first','last'], ['salary'=>'sum([])']; * * your resulting model will have 3 fields: * first, last, salary @@ -32,7 +32,7 @@ * permitted to add expressions. * * You can also pass seed (for example field type) when aggregating: - * $aggregate->groupBy(['first', 'last'], ['salary' => ['sum([])', 'type' => 'atk4_money']]; + * $aggregate->setGroupBy(['first', 'last'], ['salary' => ['sum([])', 'type' => 'atk4_money']]; * * @property Persistence\Sql $persistence * @property Model $table @@ -71,7 +71,7 @@ public function __construct(Model $baseModel, array $defaults = []) * * @return $this */ - public function groupBy(array $fields, array $aggregateExpressions = []) + public function setGroupBy(array $fields, array $aggregateExpressions = []) { $this->groupByFields = array_unique(array_merge($this->groupByFields, $fields)); diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 894f39bb7..1cbe3d652 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -52,7 +52,7 @@ protected function createInvoiceAggregate(): AggregateModel public function testGroupBy(): void { - $invoiceAggregate = (new AggregateModel($this->createInvoice()))->groupBy(['client_id'], [ + $invoiceAggregate = (new AggregateModel($this->createInvoice()))->setGroupBy(['client_id'], [ 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); @@ -70,7 +70,7 @@ public function testGroupSelect(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); @@ -88,7 +88,7 @@ public function testGroupSelect2(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -106,7 +106,7 @@ public function testGroupSelect3(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'min' => ['expr' => 'min([amount])', 'type' => 'atk4_money'], 'max' => ['expr' => 'max([amount])', 'type' => 'atk4_money'], @@ -127,7 +127,7 @@ public function testGroupSelectExpr(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -149,7 +149,7 @@ public function testGroupSelectCondition(): void $aggregate->addField('client'); $aggregate->table->addCondition('name', 'chair purchase'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -170,7 +170,7 @@ public function testGroupSelectCondition2(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -196,7 +196,7 @@ public function testGroupSelectCondition3(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -221,7 +221,7 @@ public function testGroupSelectCondition4(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -242,7 +242,7 @@ public function testGroupSelectScope(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -264,7 +264,7 @@ public function testGroupOrder(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -288,7 +288,7 @@ public function testGroupLimit(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); $aggregate->setLimit(1); @@ -306,7 +306,7 @@ public function testGroupLimit2(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); $aggregate->setLimit(2, 1); @@ -324,7 +324,7 @@ public function testGroupCount(): void $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); - $aggregate->groupBy(['client_id'], [ + $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); @@ -338,7 +338,7 @@ public function testAggregateFieldExpression(): void { $aggregate = $this->createInvoiceAggregate(); - $aggregate->groupBy([$aggregate->expr('{}', ['abc'])], [ + $aggregate->setGroupBy([$aggregate->expr('{}', ['abc'])], [ 'xyz' => ['expr' => 'sum([amount])'], ]); From 3db6a8a719889ff739fa082e20d160e400782057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 14 Mar 2022 17:01:46 +0100 Subject: [PATCH 105/151] adjust to latest develop --- README.md | 4 +++- docs/aggregates.rst | 4 ++-- docs/overview.rst | 6 +++--- src/Model/AggregateModel.php | 10 +++++++--- tests/ModelAggregateTest.php | 10 +++++----- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 24eb4beef..93ea3231f 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,9 @@ $data = new JobReport($db); // BarChart wants aggregated data $data->addExpression('month', ['expr' => 'month([date])']); $aggregate = new AggregateModel($data); -$aggregate->setGroupBy('month', ['profit_margin' => 'sum']); +$aggregate->setGroupBy(['month'], [ + 'profit_margin' => ['expr' => 'sum'], +]); // associate presentation with data $chart->setModel($aggregate, ['month', 'profit_margin']); diff --git a/docs/aggregates.rst b/docs/aggregates.rst index ae1f4f360..e723ca0a6 100644 --- a/docs/aggregates.rst +++ b/docs/aggregates.rst @@ -25,7 +25,7 @@ in various ways to fine-tune aggregation. Below is one sample use:: $aggregate->addField('country'); $aggregate->setGroupBy(['country_id'], [ 'count' => ['expr' => 'count(*)', 'type' => 'integer'], - 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] + 'total_amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], ], ); @@ -40,6 +40,6 @@ Below is how opening balance can be built:: // we actually need grouping by nominal $ledger->setGroupBy(['nominal_id'], [ - 'opening_balance' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'] + 'opening_balance' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], ]); diff --git a/docs/overview.rst b/docs/overview.rst index 2dc2e9ea6..f27d919af 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -206,9 +206,9 @@ order to use grouping with Agile Data, your code would be:: $aggregate = new AggregateModel(new Sale($db)); $aggregate->setGroupBy(['contractor_to', 'type'], [ // groups by 2 columns - 'c' => 'count(*)', // defines aggregate formulas for fields - 'qty' => 'sum([])', // [] refers back to qty - 'total' => 'sum([amount])', // can specify any field here + 'c' => ['expr' => 'count(*)'], // defines aggregate formulas for fields + 'qty' => ['expr' => 'sum([])'], // [] refers back to qty + 'total' => ['expr' => 'sum([amount])'], // can specify any field here ]); diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index edc8a54f9..e66c38f06 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -18,7 +18,9 @@ * It's quite simple to set up. * * $aggregate = new AggregateModel($mymodel); - * $aggregate->setGroupBy(['first','last'], ['salary'=>'sum([])']; + * $aggregate->setGroupBy(['first', 'last'], [ + * 'salary' => ['expr' => 'sum([])'], + * ]; * * your resulting model will have 3 fields: * first, last, salary @@ -32,7 +34,9 @@ * permitted to add expressions. * * You can also pass seed (for example field type) when aggregating: - * $aggregate->setGroupBy(['first', 'last'], ['salary' => ['sum([])', 'type' => 'atk4_money']]; + * $aggregate->setGroupBy(['first', 'last'], [ + * 'salary' => ['expr' => 'sum([])', 'type' => 'atk4_money'], + * ]; * * @property Persistence\Sql $persistence * @property Model $table @@ -90,7 +94,7 @@ public function setGroupBy(array $fields, array $aggregateExpressions = []) $args = [$this->table->getField($name)]; } - $seed[0 /* TODO 'expr' was here, 0 fixes tests, but 'expr' in seed might this be defined */] = $this->table->expr($seed[0] ?? $seed['expr'], $args); + $seed['expr'] = $this->table->expr($seed['expr'], $args); // now add the expressions here $this->addExpression($name, $seed); diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 1cbe3d652..8000cc5da 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -132,7 +132,7 @@ public function testGroupSelectExpr(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); + $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $this->assertSameExportUnordered( [ @@ -154,7 +154,7 @@ public function testGroupSelectCondition(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); + $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $this->assertSameExportUnordered( [ @@ -175,7 +175,7 @@ public function testGroupSelectCondition2(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); + $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $aggregate->addCondition( 'double', '>', @@ -201,7 +201,7 @@ public function testGroupSelectCondition3(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); + $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $aggregate->addCondition( 'double', // TODO Sqlite bind param does not work, expr needed, even if casted to float with DBAL type (comparison works only if casted to/bind as int) @@ -226,7 +226,7 @@ public function testGroupSelectCondition4(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $aggregate->addExpression('double', ['[s]+[amount]', 'type' => 'atk4_money']); + $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $aggregate->addCondition('client_id', 2); $this->assertSame( From 485d1efd1d439dcd0029e601e1520a930e4b9ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 14 Mar 2022 17:13:30 +0100 Subject: [PATCH 106/151] adjust to latest develop --- docs/unions.rst | 6 +++--- src/Model/UnionModel.php | 2 +- tests/ModelUnionTest.php | 34 +++++++++++++++++++++++----------- tests/ReportTest.php | 4 +++- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/docs/unions.rst b/docs/unions.rst index d820e2461..e519fb329 100644 --- a/docs/unions.rst +++ b/docs/unions.rst @@ -58,7 +58,7 @@ Below is an example of 3 different ways to define fields for the UnionModel mode $unionPaymentInvoice->addField('client_id'); // Expression will not affect nested models in any way - $unionPaymentInvoice->addExpression('name_capital', 'upper([name])'); + $unionPaymentInvoice->addExpression('name_capital', ['expr' => 'upper([name])']); // UnionModel model can be joined with extra tables and define some fields from those joins $unionPaymentInvoice @@ -91,8 +91,8 @@ Should more flexibility be needed, more expressions (or fields) can be added dir $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice(), ['amount' => '-[amount]']); $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); - $nestedPayment->addExpression('type', '"payment"'); - $nestedInvoice->addExpression('type', '"invoice"'); + $nestedPayment->addExpression('type', ['expr' => '\'payment\'']); + $nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); $unionPaymentInvoice->addField('type'); A new field "type" has been added that will be defined as a static constant. diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 911b39948..3dd183d3b 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -261,7 +261,7 @@ public function getSubQuery(array $fields): Query // UnionModel can have some fields defined as expressions. We don't touch those either. // Imants: I have no idea why this condition was set, but it's limiting our ability // to use expression fields in mapping - if ($field instanceof SqlExpressionField /*&& !isset($this->aggregate[$fieldName])*/) { + if ($field instanceof SqlExpressionField /* && !isset($this->aggregate[$fieldName]) */) { continue; } diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index cf907e5df..5139147bf 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -90,7 +90,7 @@ public function testNestedQuery1(): void public function testMissingField(): void { $transaction = $this->createTransaction(); - $transaction->nestedInvoice->addExpression('type', '\'invoice\''); + $transaction->nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); $transaction->addField('type'); $this->assertSameSql( @@ -207,7 +207,9 @@ public function testGrouping1(): void $transaction = $this->createTransaction(); $transactionAggregate = new AggregateModel($transaction); - $transactionAggregate->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate->setGroupBy(['name'], [ + 'amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + ]); $this->assertSameSql( 'select "name", sum("amount") "amount" from (select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", "amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "_tu") "_tm" group by "name"', @@ -217,7 +219,9 @@ public function testGrouping1(): void $transaction = $this->createSubtractInvoiceTransaction(); $transactionAggregate = new AggregateModel($transaction); - $transactionAggregate->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate->setGroupBy(['name'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], + ]); $this->assertSameSql( 'select "name", sum("amount") "amount" from (select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", -"amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "_tu") "_tm" group by "name"', @@ -235,7 +239,9 @@ public function testGrouping2(): void $transaction->removeField('client_id'); // TODO enable later, test failing with MSSQL $transaction->setOrder('name'); $transactionAggregate = new AggregateModel($transaction); - $transactionAggregate->groupBy(['name'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate->setGroupBy(['name'], [ + 'amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + ]); $transactionAggregate->setOrder('name'); $this->assertSame([ @@ -249,7 +255,9 @@ public function testGrouping2(): void $transaction->removeField('client_id'); // TODO enable later, test failing with MSSQL $transaction->setOrder('name'); $transactionAggregate = new AggregateModel($transaction); - $transactionAggregate->groupBy(['name'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate->setGroupBy(['name'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], + ]); $transactionAggregate->setOrder('name'); $this->assertSame([ @@ -267,12 +275,14 @@ public function testGrouping2(): void public function testSubGroupingByExpressions(): void { $transaction = $this->createTransaction(); - $transaction->nestedInvoice->addExpression('type', '\'invoice\''); - $transaction->nestedPayment->addExpression('type', '\'payment\''); + $transaction->nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); + $transaction->nestedPayment->addExpression('type', ['expr' => '\'payment\'']); $transaction->addField('type'); $transactionAggregate = new AggregateModel($transaction); - $transactionAggregate->groupBy(['type'], ['amount' => ['sum([amount])', 'type' => 'atk4_money']]); + $transactionAggregate->setGroupBy(['type'], [ + 'amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], + ]); // TODO subselects should not select "client" and "name" fields $this->assertSameSql( @@ -290,12 +300,14 @@ public function testSubGroupingByExpressions(): void ], $transactionAggregate->export(['type', 'amount'])); $transaction = $this->createSubtractInvoiceTransaction(); - $transaction->nestedInvoice->addExpression('type', '\'invoice\''); - $transaction->nestedPayment->addExpression('type', '\'payment\''); + $transaction->nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); + $transaction->nestedPayment->addExpression('type', ['expr' => '\'payment\'']); $transaction->addField('type'); $transactionAggregate = new AggregateModel($transaction); - $transactionAggregate->groupBy(['type'], ['amount' => ['sum([])', 'type' => 'atk4_money']]); + $transactionAggregate->setGroupBy(['type'], [ + 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], + ]); $this->assertSameExportUnordered([ ['type' => 'invoice', 'amount' => -23.0], diff --git a/tests/ReportTest.php b/tests/ReportTest.php index 516f20e54..6de07d012 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -48,7 +48,9 @@ public function testAliasGroupSelect(): void { $invoiceAggregate = $this->createInvoiceAggregate(); - $invoiceAggregate->groupBy(['client_id'], ['c' => ['count(*)', 'type' => 'integer']]); + $invoiceAggregate->setGroupBy(['client_id'], [ + 'c' => ['expr' => 'count(*)', 'type' => 'integer'], + ]); $this->assertSame( [ From e2cd2e2201af3f99ef09c2bea6ca017ce9997483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 15 Apr 2022 20:58:14 +0200 Subject: [PATCH 107/151] adjust to latest develop --- src/Model/AggregateModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index e66c38f06..63ab4f35b 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -192,7 +192,7 @@ protected function initQueryGrouping(Query $query): void if ($field instanceof Expression) { $expression = $field; } else { - $expression = $this->table->getField($field)->short_name /* TODO short_name should be used by DSQL automatically when in GROUP BY, HAVING, ... */; + $expression = $this->table->getField($field)->shortName /* TODO shortName should be used by DSQL automatically when in GROUP BY, HAVING, ... */; } $query->group($expression); From 8ae2036abab340714dbf93a71d45235ab1a88792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 21 Mar 2022 15:25:45 +0100 Subject: [PATCH 108/151] simplify --- src/Model/AggregateModel.php | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index 63ab4f35b..70d681c09 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -19,7 +19,7 @@ * * $aggregate = new AggregateModel($mymodel); * $aggregate->setGroupBy(['first', 'last'], [ - * 'salary' => ['expr' => 'sum([])'], + * 'salary' => ['expr' => 'sum([])', 'type' => 'atk4_money'], * ]; * * your resulting model will have 3 fields: @@ -33,11 +33,6 @@ * If this field exist in the original model it will be added and you'll get exception otherwise. Finally you are * permitted to add expressions. * - * You can also pass seed (for example field type) when aggregating: - * $aggregate->setGroupBy(['first', 'last'], [ - * 'salary' => ['expr' => 'sum([])', 'type' => 'atk4_money'], - * ]; - * * @property Persistence\Sql $persistence * @property Model $table * @@ -59,11 +54,9 @@ public function __construct(Model $baseModel, array $defaults = []) $this->table = $baseModel; - // this model does not have ID field - $this->id_field = null; - - // this model should always be read-only + // this model should always be read-only and does not have ID field $this->read_only = true; + $this->id_field = null; parent::__construct($baseModel->persistence, $defaults); } @@ -88,13 +81,13 @@ public function setGroupBy(array $fields, array $aggregateExpressions = []) } foreach ($aggregateExpressions as $name => $seed) { - $args = []; + $exprArgs = []; // if field originally defined in the parent model, then it can be used as part of expression if ($this->table->hasField($name)) { - $args = [$this->table->getField($name)]; + $exprArgs = [$this->table->getField($name)]; } - $seed['expr'] = $this->table->expr($seed['expr'], $args); + $seed['expr'] = $this->table->expr($seed['expr'], $exprArgs); // now add the expressions here $this->addExpression($name, $seed); From 9c9fa6f597fed907312bfd2c2048c740de83cb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 21 Mar 2022 15:25:55 +0100 Subject: [PATCH 109/151] fix cloned field - we should either disallow refs or wrap twice --- src/Model/AggregateModel.php | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index 70d681c09..a5daef96e 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -11,7 +11,6 @@ use Atk4\Data\Persistence; use Atk4\Data\Persistence\Sql\Expression; use Atk4\Data\Persistence\Sql\Query; -use Atk4\Data\Reference; /** * AggregateModel model allows you to query using "group by" clause on your existing model. @@ -96,18 +95,6 @@ public function setGroupBy(array $fields, array $aggregateExpressions = []) return $this; } - /** - * TODO this should be removed, nasty hack to pass the tests. - */ - public function getRef(string $link): Reference - { - $ref = clone $this->table->getRef($link); - $ref->unsetOwner(); - $ref->setOwner($this); - - return $ref; - } - /** * Adds new field into model. * @@ -126,6 +113,12 @@ public function addField(string $name, $seed = []): Field if ($this->table->hasField($name)) { $field = clone $this->table->getField($name); $field->unsetOwner(); + $refLink = \Closure::bind(fn () => $field->referenceLink, null, Field::class)(); + if ($refLink !== null && !$this->hasRef($refLink)) { + $ref = clone $this->table->getRef($refLink); + $ref->unsetOwner(); + $this->add($ref); + } } else { $field = null; } From 2da2f6aa9045581bc0cbf38f636396fb3887c36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 15 Apr 2022 21:09:57 +0200 Subject: [PATCH 110/151] compact assertSameExportUnordered assertions --- tests/ModelAggregateTest.php | 66 +++++++++++++----------------------- 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 8000cc5da..2eb15d0bb 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -56,13 +56,10 @@ public function testGroupBy(): void 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); - $this->assertSameExportUnordered( - [ - ['client_id' => 1, 'c' => 2], - ['client_id' => 2, 'c' => 1], - ], - $invoiceAggregate->export() - ); + $this->assertSameExportUnordered([ + ['client_id' => 1, 'c' => 2], + ['client_id' => 2, 'c' => 1], + ], $invoiceAggregate->export()); } public function testGroupSelect(): void @@ -74,13 +71,10 @@ public function testGroupSelect(): void 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); - $this->assertSameExportUnordered( - [ - ['client' => 'Vinny', 'client_id' => 1, 'c' => 2], - ['client' => 'Zoe', 'client_id' => 2, 'c' => 1], - ], - $aggregate->export() - ); + $this->assertSameExportUnordered([ + ['client' => 'Vinny', 'client_id' => 1, 'c' => 2], + ['client' => 'Zoe', 'client_id' => 2, 'c' => 1], + ], $aggregate->export()); } public function testGroupSelect2(): void @@ -92,13 +86,10 @@ public function testGroupSelect2(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $this->assertSameExportUnordered( - [ - ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], - ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], - ], - $aggregate->export() - ); + $this->assertSameExportUnordered([ + ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], + ], $aggregate->export()); } public function testGroupSelect3(): void @@ -113,13 +104,10 @@ public function testGroupSelect3(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], // same as `s`, but reuse name `amount` ]); - $this->assertSameExportUnordered( - [ - ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], - ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'min' => 4.0, 'max' => 4.0, 'amount' => 4.0], - ], - $aggregate->export() - ); + $this->assertSameExportUnordered([ + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'min' => 4.0, 'max' => 4.0, 'amount' => 4.0], + ], $aggregate->export()); } public function testGroupSelectExpr(): void @@ -134,13 +122,10 @@ public function testGroupSelectExpr(): void $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); - $this->assertSameExportUnordered( - [ - ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], - ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], - ], - $aggregate->export() - ); + $this->assertSameExportUnordered([ + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], $aggregate->export()); } public function testGroupSelectCondition(): void @@ -156,13 +141,10 @@ public function testGroupSelectCondition(): void $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); - $this->assertSameExportUnordered( - [ - ['client' => 'Vinny', 'client_id' => 1, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], - ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], - ], - $aggregate->export() - ); + $this->assertSameExportUnordered([ + ['client' => 'Vinny', 'client_id' => 1, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], $aggregate->export()); } public function testGroupSelectCondition2(): void From 07d1838a8b5da1364cb55d2bcbdf0db474cb70e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 21 Mar 2022 17:27:49 +0100 Subject: [PATCH 111/151] fix array merge --- src/Model/AggregateModel.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index a5daef96e..6f97c3d3a 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -135,16 +135,18 @@ public function action(string $mode, array $args = []) { switch ($mode) { case 'select': - $fields = $this->onlyFields ?: array_keys($this->getFields()); + $fields = array_unique(array_merge( + $this->onlyFields ?: array_keys($this->getFields()), + array_filter($this->groupByFields, fn ($v) => !$v instanceof Expression) + )); - // select but no need your fields $query = parent::action($mode, [false]); if (isset($query->args['where'])) { $query->args['having'] = $query->args['where']; unset($query->args['where']); } - $this->persistence->initQueryFields($this, $query, array_unique($fields + $this->groupByFields)); + $this->persistence->initQueryFields($this, $query, $fields); $this->initQueryGrouping($query); $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); @@ -163,9 +165,10 @@ public function action(string $mode, array $args = []) $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); return $query->dsql()->field('count(*)')->table($this->expr('([]) {}', [$query, '_tc'])); - case 'field': - case 'fx': - return parent::action($mode, $args); +// case 'field': +// case 'fx': +// case 'fx0': +// return parent::action($mode, $args); default: throw (new Exception('AggregateModel model does not support this action')) ->addMoreInfo('mode', $mode); From 5d8ee39747865bc6f9d216524f26d702b6fb2c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 21 Mar 2022 17:33:20 +0100 Subject: [PATCH 112/151] fix count query --- src/Model/AggregateModel.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index 6f97c3d3a..5a3bb313b 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -153,18 +153,16 @@ public function action(string $mode, array $args = []) return $query; case 'count': - $query = parent::action($mode, $args); - if (isset($query->args['where'])) { - $query->args['having'] = $query->args['where']; - unset($query->args['where']); - } + $innerQuery = $this->action('select'); + $innerQuery->reset('field')->field($this->expr('1')); - $query->reset('field')->field($this->expr('1')); - $this->initQueryGrouping($query); + $query = $innerQuery->dsql() + ->field('count(*)', $args['alias'] ?? null) + ->table($this->expr('([]) {}', [$innerQuery, '_tc'])); $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); - return $query->dsql()->field('count(*)')->table($this->expr('([]) {}', [$query, '_tc'])); + return $query; // case 'field': // case 'fx': // case 'fx0': From 431da2e9a671c6901c0dd3fa52fa262f65583937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 21 Mar 2022 17:40:13 +0100 Subject: [PATCH 113/151] no false for initQueryFields --- src/Model/AggregateModel.php | 6 +++--- src/Persistence/Sql.php | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index 5a3bb313b..92cfbe54c 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -135,12 +135,12 @@ public function action(string $mode, array $args = []) { switch ($mode) { case 'select': - $fields = array_unique(array_merge( + $fields = $args[0] ?? array_unique(array_merge( $this->onlyFields ?: array_keys($this->getFields()), array_filter($this->groupByFields, fn ($v) => !$v instanceof Expression) )); - $query = parent::action($mode, [false]); + $query = parent::action($mode, [[]]); if (isset($query->args['where'])) { $query->args['having'] = $query->args['where']; unset($query->args['where']); @@ -153,7 +153,7 @@ public function action(string $mode, array $args = []) return $query; case 'count': - $innerQuery = $this->action('select'); + $innerQuery = $this->action('select', [[]]); $innerQuery->reset('field')->field($this->expr('1')); $query = $innerQuery->dsql() diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index 8945d6523..9ab51e5ef 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -258,15 +258,10 @@ public function initField(Query $query, Field $field): void /** * Adds model fields in Query. * - * @param array|false|null $fields + * @param array|null $fields */ public function initQueryFields(Model $model, Query $query, $fields = null): void { - // do nothing on purpose - if ($fields === false) { - return; - } - // init fields if (is_array($fields)) { // Set of fields is strictly defined for purposes of export, From 1a821a8e6e95977314e152e3de0e9c7f99832ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 21 Mar 2022 15:25:55 +0100 Subject: [PATCH 114/151] fix cloned field - we should either disallow refs or wrap twice From 8e8bfd5391584cfb290916b515a5eb56d579d3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 21 Mar 2022 17:01:04 +0100 Subject: [PATCH 115/151] fix ii - do not clone field --- src/Model/AggregateModel.php | 24 +++++++----------------- tests/ModelAggregateTest.php | 2 +- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index 92cfbe54c..60463cb42 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -106,26 +106,16 @@ public function addField(string $name, $seed = []): Field return parent::addField($name, $seed); } - if ($seed['never_persist'] ?? false) { - return parent::addField($name, $seed); - } - if ($this->table->hasField($name)) { - $field = clone $this->table->getField($name); - $field->unsetOwner(); - $refLink = \Closure::bind(fn () => $field->referenceLink, null, Field::class)(); - if ($refLink !== null && !$this->hasRef($refLink)) { - $ref = clone $this->table->getRef($refLink); - $ref->unsetOwner(); - $this->add($ref); - } - } else { - $field = null; + $innerField = $this->table->getField($name); + $seed['type'] ??= $innerField->type; + $seed['enum'] ??= $innerField->enum; + $seed['values'] ??= $innerField->values; + $seed['caption'] ??= $innerField->caption; + $seed['ui'] ??= $innerField->ui; } - return $field - ? parent::addField($name, $field)->setDefaults($seed) - : parent::addField($name, $seed); + return parent::addField($name, $seed); } /** diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 2eb15d0bb..a52635ecd 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -253,7 +253,7 @@ public function testGroupOrder(): void $aggregate->setOrder('client_id', 'asc'); $this->assertSameSql( - 'select (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "_tm"."client_id") "client", "client_id", sum("amount") "amount" from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id" order by "client_id"', + 'select "client", "client_id", sum("amount") "amount" from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id" order by "client_id"', $aggregate->action('select')->render()[0] ); From 3716814f899136e2373b143a0b669bc7cb8e5634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 15 Apr 2022 21:42:23 +0200 Subject: [PATCH 116/151] fix merge - no false for initQueryFields --- src/Model/UnionModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 3dd183d3b..5801f523c 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -275,7 +275,7 @@ public function getSubQuery(array $fields): Query } // now prepare query - $query = $this->persistence->action($nestedModel, 'select', [false]); + $query = $this->persistence->action($nestedModel, 'select', [[]]); foreach ($queryFieldExpressions as $fAlias => $fExpr) { $query->field($fExpr, $fAlias); From a46433db4a651a0f96eff27e9c9e9532021cd65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 15 Apr 2022 22:23:41 +0200 Subject: [PATCH 117/151] fix unaggregated fields for pgsql --- src/Model/AggregateModel.php | 2 +- tests/ModelAggregateTest.php | 31 +++++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index 60463cb42..d37d11c9f 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -72,7 +72,7 @@ public function setGroupBy(array $fields, array $aggregateExpressions = []) $this->groupByFields = array_unique(array_merge($this->groupByFields, $fields)); foreach ($fields as $fieldName) { - if ($fieldName instanceof Expression) { + if ($fieldName instanceof Expression || $this->hasField($fieldName)) { continue; } diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index a52635ecd..1d0b2059b 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -37,6 +37,15 @@ protected function setUp(): void $this->setDb($this->init_db); } + /** + * TODO when aggregating on ID field, we can assume uniqueness, and any other model field + * should NOT be needed to be added to GROUP BY explicitly. + */ + private static function fixAllNonAggregatedFieldsInGroupBy(AggregateModel $model): void + { + $model->setGroupBy(['client']); + } + protected function createInvoice(): Model\Invoice { $invoice = new Model\Invoice($this->db); @@ -52,14 +61,14 @@ protected function createInvoiceAggregate(): AggregateModel public function testGroupBy(): void { - $invoiceAggregate = (new AggregateModel($this->createInvoice()))->setGroupBy(['client_id'], [ + $aggregate = (new AggregateModel($this->createInvoice()))->setGroupBy(['client_id'], [ 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); $this->assertSameExportUnordered([ ['client_id' => 1, 'c' => 2], ['client_id' => 2, 'c' => 1], - ], $invoiceAggregate->export()); + ], $aggregate->export()); } public function testGroupSelect(): void @@ -70,6 +79,7 @@ public function testGroupSelect(): void $aggregate->setGroupBy(['client_id'], [ 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $this->assertSameExportUnordered([ ['client' => 'Vinny', 'client_id' => 1, 'c' => 2], @@ -85,6 +95,7 @@ public function testGroupSelect2(): void $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $this->assertSameExportUnordered([ ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], @@ -103,6 +114,7 @@ public function testGroupSelect3(): void 'max' => ['expr' => 'max([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], // same as `s`, but reuse name `amount` ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $this->assertSameExportUnordered([ ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'min' => 4.0, 'max' => 15.0, 'amount' => 19.0], @@ -119,6 +131,7 @@ public function testGroupSelectExpr(): void 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); @@ -138,6 +151,7 @@ public function testGroupSelectCondition(): void 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); @@ -156,6 +170,7 @@ public function testGroupSelectCondition2(): void 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $aggregate->addCondition( @@ -182,6 +197,7 @@ public function testGroupSelectCondition3(): void 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $aggregate->addCondition( @@ -207,6 +223,7 @@ public function testGroupSelectCondition4(): void 's' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $aggregate->addCondition('client_id', 2); @@ -227,6 +244,7 @@ public function testGroupSelectScope(): void $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); // TODO Sqlite bind param does not work, expr needed, even if casted to float with DBAL type (comparison works only if casted to/bind as int) $numExpr = $this->getDatabasePlatform() instanceof SqlitePlatform ? $aggregate->expr('4') : 4; @@ -249,16 +267,18 @@ public function testGroupOrder(): void $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->setOrder('client_id', 'asc'); $this->assertSameSql( - 'select "client", "client_id", sum("amount") "amount" from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id" order by "client_id"', + 'select "client", "client_id", sum("amount") "amount" from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id", "client" order by "client_id"', $aggregate->action('select')->render()[0] ); // TODO subselect should not select "client" field $aggregate->removeField('client'); + $aggregate->groupByFields = array_diff($aggregate->groupByFields, ['client']); $this->assertSameSql( 'select "client_id", sum("amount") "amount" from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id" order by "client_id"', $aggregate->action('select')->render()[0] @@ -273,6 +293,7 @@ public function testGroupLimit(): void $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->setLimit(1); $this->assertSame( @@ -291,6 +312,7 @@ public function testGroupLimit2(): void $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->setLimit(2, 1); $this->assertSame( @@ -309,9 +331,10 @@ public function testGroupCount(): void $aggregate->setGroupBy(['client_id'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); + self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $this->assertSameSql( - 'select count(*) from ((select 1 from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id")) "_tc"', + 'select count(*) from ((select 1 from (select "id", "client_id", "name", "amount", (select "name" from "client" "_c_2bfe9d72a4aa" where "id" = "invoice"."client_id") "client" from "invoice") "_tm" group by "client_id", "client")) "_tc"', $aggregate->action('count')->render()[0] ); } From 265958ff3a62d79c838920f05d17879c99f2027e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 15 Apr 2022 22:26:54 +0200 Subject: [PATCH 118/151] fix unaggregated fields for pgsql --- tests/ReportTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/ReportTest.php b/tests/ReportTest.php index 6de07d012..4b2af1877 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -51,6 +51,9 @@ public function testAliasGroupSelect(): void $invoiceAggregate->setGroupBy(['client_id'], [ 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); + \Closure::bind(function () use ($invoiceAggregate) { + ModelAggregateTest::fixAllNonAggregatedFieldsInGroupBy($invoiceAggregate); + }, null, ModelAggregateTest::class)(); $this->assertSame( [ From 95af3333f01a621029e75e41d226e4f334225634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 15 Apr 2022 22:40:58 +0200 Subject: [PATCH 119/151] compact test code --- tests/ModelAggregateTest.php | 81 ++++++++++++++---------------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/tests/ModelAggregateTest.php b/tests/ModelAggregateTest.php index 1d0b2059b..d9231add7 100644 --- a/tests/ModelAggregateTest.php +++ b/tests/ModelAggregateTest.php @@ -12,9 +12,11 @@ class ModelAggregateTest extends TestCase { - /** @var array */ - private $init_db = - [ + protected function setUp(): void + { + parent::setUp(); + + $this->setDb([ 'client' => [ // allow of migrator to create all columns ['name' => 'Vinny', 'surname' => null, 'order' => null], @@ -29,12 +31,7 @@ class ModelAggregateTest extends TestCase ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], - ]; - - protected function setUp(): void - { - parent::setUp(); - $this->setDb($this->init_db); + ]); } /** @@ -61,7 +58,9 @@ protected function createInvoiceAggregate(): AggregateModel public function testGroupBy(): void { - $aggregate = (new AggregateModel($this->createInvoice()))->setGroupBy(['client_id'], [ + $aggregate = $this->createInvoiceAggregate(); + + $aggregate->setGroupBy(['client_id'], [ 'c' => ['expr' => 'count(*)', 'type' => 'integer'], ]); @@ -180,12 +179,9 @@ public function testGroupSelectCondition2(): void $this->getDatabasePlatform() instanceof SqlitePlatform ? $aggregate->expr('10') : 10 ); - $this->assertSame( - [ - ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], - ], - $aggregate->export() - ); + $this->assertSame([ + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ], $aggregate->export()); } public function testGroupSelectCondition3(): void @@ -206,12 +202,9 @@ public function testGroupSelectCondition3(): void $this->getDatabasePlatform() instanceof SqlitePlatform ? $aggregate->expr('38') : 38 ); - $this->assertSame( - [ - ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], - ], - $aggregate->export() - ); + $this->assertSame([ + ['client' => 'Vinny', 'client_id' => 1, 's' => 19.0, 'amount' => 19.0, 'double' => 38.0], + ], $aggregate->export()); } public function testGroupSelectCondition4(): void @@ -228,12 +221,9 @@ public function testGroupSelectCondition4(): void $aggregate->addExpression('double', ['expr' => '[s] + [amount]', 'type' => 'atk4_money']); $aggregate->addCondition('client_id', 2); - $this->assertSame( - [ - ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], - ], - $aggregate->export() - ); + $this->assertSame([ + ['client' => 'Zoe', 'client_id' => 2, 's' => 4.0, 'amount' => 4.0, 'double' => 8.0], + ], $aggregate->export()); } public function testGroupSelectScope(): void @@ -251,15 +241,12 @@ public function testGroupSelectScope(): void $scope = Scope::createAnd(new Condition('client_id', 2), new Condition('amount', $numExpr)); $aggregate->addCondition($scope); - $this->assertSame( - [ - ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], - ], - $aggregate->export() - ); + $this->assertSame([ + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], + ], $aggregate->export()); } - public function testGroupOrder(): void + public function testGroupOrderSql(): void { $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); @@ -295,13 +282,11 @@ public function testGroupLimit(): void ]); self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->setLimit(1); + $aggregate->setOrder('client_id', 'asc'); - $this->assertSame( - [ - ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], - ], - $aggregate->setOrder('client_id', 'asc')->export() - ); + $this->assertSame([ + ['client' => 'Vinny', 'client_id' => 1, 'amount' => 19.0], + ], $aggregate->export()); } public function testGroupLimit2(): void @@ -314,16 +299,14 @@ public function testGroupLimit2(): void ]); self::fixAllNonAggregatedFieldsInGroupBy($aggregate); $aggregate->setLimit(2, 1); + $aggregate->setOrder('client_id', 'asc'); - $this->assertSame( - [ - ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], - ], - $aggregate->setOrder('client_id', 'asc')->export() - ); + $this->assertSame([ + ['client' => 'Zoe', 'client_id' => 2, 'amount' => 4.0], + ], $aggregate->export()); } - public function testGroupCount(): void + public function testGroupCountSql(): void { $aggregate = $this->createInvoiceAggregate(); $aggregate->addField('client'); @@ -339,7 +322,7 @@ public function testGroupCount(): void ); } - public function testAggregateFieldExpression(): void + public function testAggregateFieldExpressionSql(): void { $aggregate = $this->createInvoiceAggregate(); From f8a26539f048813e65cb976ffaf2f6bf5a92097b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 15 Apr 2022 22:57:34 +0200 Subject: [PATCH 120/151] compact test code --- tests/ModelUnionTest.php | 21 +++++++++------------ tests/ReportTest.php | 15 ++++++--------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 5139147bf..992f0fa7f 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -10,9 +10,11 @@ class ModelUnionTest extends TestCase { - /** @var array */ - private $init_db = - [ + protected function setUp(): void + { + parent::setUp(); + + $this->setDb([ 'client' => [ // allow of migrator to create all columns ['name' => 'Vinny', 'surname' => null, 'order' => null], @@ -27,12 +29,7 @@ class ModelUnionTest extends TestCase ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], - ]; - - protected function setUp(): void - { - parent::setUp(); - $this->setDb($this->init_db); + ]); } protected function createTransaction(): Model\Transaction @@ -356,7 +353,7 @@ public function testReference(): void * * See also: http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 */ - public function testFieldAggregate(): void + public function testFieldAggregateUnion(): void { $client = $this->createClient(); $client->hasMany('tr', ['model' => $this->createTransaction()]) @@ -396,7 +393,7 @@ public function testConditionOnNestedModelField(): void ], $transaction->export()); } - public function testConditionForcedOnNestedModels1(): void + public function testConditionForcedOnNestedModel1(): void { $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '>', 5, true); @@ -406,7 +403,7 @@ public function testConditionForcedOnNestedModels1(): void ], $transaction->export()); } - public function testConditionForcedOnNestedModels2(): void + public function testConditionForcedOnNestedModel2(): void { $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '<', -10, true); diff --git a/tests/ReportTest.php b/tests/ReportTest.php index 4b2af1877..20a85edea 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -9,9 +9,11 @@ class ReportTest extends TestCase { - /** @var array */ - private $init_db = - [ + protected function setUp(): void + { + parent::setUp(); + + $this->setDb([ 'client' => [ // allow of migrator to create all columns ['name' => 'Vinny', 'surname' => null, 'order' => null], @@ -26,12 +28,7 @@ class ReportTest extends TestCase ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], - ]; - - protected function setUp(): void - { - parent::setUp(); - $this->setDb($this->init_db); + ]); } protected function createInvoiceAggregate(): AggregateModel From b4aa593ea2c46e6784a2462e25daeb23240180c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 00:19:02 +0200 Subject: [PATCH 121/151] skip unsupported sql tests --- tests/ModelUnionTest.php | 50 ++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 992f0fa7f..0c3425d6a 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -234,7 +234,10 @@ public function testGrouping2(): void { $transaction = $this->createTransaction(); $transaction->removeField('client_id'); - // TODO enable later, test failing with MSSQL $transaction->setOrder('name'); + if (!$this->getDatabasePlatform() instanceof SQLServerPlatform) { + // TODO where should be no ORDER BY in subquery + $transaction->setOrder('name'); + } $transactionAggregate = new AggregateModel($transaction); $transactionAggregate->setGroupBy(['name'], [ 'amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], @@ -250,7 +253,10 @@ public function testGrouping2(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->removeField('client_id'); - // TODO enable later, test failing with MSSQL $transaction->setOrder('name'); + if (!$this->getDatabasePlatform() instanceof SQLServerPlatform) { + // TODO where should be no ORDER BY in subquery + $transaction->setOrder('name'); + } $transactionAggregate = new AggregateModel($transaction); $transactionAggregate->setGroupBy(['name'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], @@ -317,20 +323,19 @@ public function testReference(): void $client = $this->createClient(); $client->hasMany('tr', ['model' => $this->createTransaction()]); + if (\PHP_MAJOR_VERSION >= 7) { // always true, TODO aggregate on reference is broken + $this->assertTrue(true); + + return; + } + $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); - // TODO aggregated fields are pushdown, but where condition is not - // I belive the fields pushdown is even wrong as not every aggregated result produces same result when aggregated again - // then fix also self::testFieldAggregate() - $this->assertTrue(true); - - return; - // @phpstan-ignore-next-line $this->assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - 'select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b)', + 'select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "_t_e7d707a26e7f"', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); @@ -342,30 +347,31 @@ public function testReference(): void $this->assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b)', + 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "_t_e7d707a26e7f"', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); } - /** - * Aggregation is supposed to work in theory, but MySQL uses "semi-joins" for this type of query which does not support UNION, - * and therefore it complains about "client"."id" field. - * - * See also: http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 - */ public function testFieldAggregateUnion(): void { $client = $this->createClient(); $client->hasMany('tr', ['model' => $this->createTransaction()]) ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); - // TODO some fields are pushdown, but some not, same issue as in self::testReference() - $this->assertTrue(true); + if ($this->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySQLPlatform + || $this->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform + || $this->getDatabasePlatform() instanceof SQLServerPlatform + || $this->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\OraclePlatform) { + // TODO failing on all DBs expect Sqlite, MySQL uses "semi-joins" for this type of query which does not support UNION + // and therefore it complains about "client"."id" field, see: + // http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 + $this->assertTrue(true); + + return; + } - return; - // @phpstan-ignore-next-line $this->assertSameSql( - 'select "client"."id", "client"."name", (select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = "client"."id" UNION ALL select sum("amount") "val" from "payment" where "client_id" = "client"."id") "_tu") "balance" from "client" where "client"."id" = 1 limit 0, 1', + 'select "id", "name", "surname", "order", (select coalesce(sum("val"), 0) from (select coalesce(sum("amount"), 0) "val" from "invoice" UNION ALL select coalesce(sum("amount"), 0) "val" from "payment") "_t_e7d707a26e7f" where "client_id" = "client"."id") "balance" from "client" group by "id" having "id" = :a', $client->load(1)->action('select')->render()[0] ); } From 222724a133ec396573c7332b3241b990f16fc0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 00:20:11 +0200 Subject: [PATCH 122/151] compact test code --- tests/Schema/ModelTest.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/Schema/ModelTest.php b/tests/Schema/ModelTest.php index 7d3ffa01f..a03d610ba 100644 --- a/tests/Schema/ModelTest.php +++ b/tests/Schema/ModelTest.php @@ -107,16 +107,13 @@ public function testCreateModel(): void $user_model = $this->createMigrator()->createModel($this->db, 'user'); - $this->assertSame( - [ - 'name', - 'password', - 'is_admin', - 'notes', - 'main_role_id', // our_field here not role_id (reference name) - ], - array_keys($user_model->getFields()) - ); + $this->assertSame([ + 'name', + 'password', + 'is_admin', + 'notes', + 'main_role_id', // our_field here not role_id (reference name) + ], array_keys($user_model->getFields())); } /** From d57af828a1d882b3f796f0926e6f70f25b5922f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 00:29:16 +0200 Subject: [PATCH 123/151] rm ReportTest - same as ModelAggregateTest::testGroupLimit2 --- tests/ReportTest.php | 63 -------------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 tests/ReportTest.php diff --git a/tests/ReportTest.php b/tests/ReportTest.php deleted file mode 100644 index 20a85edea..000000000 --- a/tests/ReportTest.php +++ /dev/null @@ -1,63 +0,0 @@ -setDb([ - 'client' => [ - // allow of migrator to create all columns - ['name' => 'Vinny', 'surname' => null, 'order' => null], - ['name' => 'Zoe'], - ], - 'invoice' => [ - ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], - ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], - ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], - ], - 'payment' => [ - ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], - ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], - ], - ]); - } - - protected function createInvoiceAggregate(): AggregateModel - { - $invoice = new Model\Invoice($this->db); - $invoice->getRef('client_id')->addTitle(); - $invoiceAggregate = new AggregateModel($invoice); - $invoiceAggregate->addField('client'); - - return $invoiceAggregate; - } - - public function testAliasGroupSelect(): void - { - $invoiceAggregate = $this->createInvoiceAggregate(); - - $invoiceAggregate->setGroupBy(['client_id'], [ - 'c' => ['expr' => 'count(*)', 'type' => 'integer'], - ]); - \Closure::bind(function () use ($invoiceAggregate) { - ModelAggregateTest::fixAllNonAggregatedFieldsInGroupBy($invoiceAggregate); - }, null, ModelAggregateTest::class)(); - - $this->assertSame( - [ - ['client' => 'Vinny', 'client_id' => 1, 'c' => 2], - ['client' => 'Zoe', 'client_id' => 2, 'c' => 1], - ], - $invoiceAggregate->setOrder('client_id', 'asc')->export() - ); - } -} From 5520638303591d0e2a978bcb57ea7f956ec556ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 00:36:55 +0200 Subject: [PATCH 124/151] rename hook const --- src/Model/AggregateModel.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index d37d11c9f..72734f3ba 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -40,7 +40,7 @@ class AggregateModel extends Model { /** @const string */ - public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; + public const HOOK_INIT_AGGREGATE_SELECT_QUERY = self::class . '@initAggregateSelectQuery'; /** @var array */ public $groupByFields = []; @@ -139,7 +139,7 @@ public function action(string $mode, array $args = []) $this->persistence->initQueryFields($this, $query, $fields); $this->initQueryGrouping($query); - $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); + $this->hook(self::HOOK_INIT_AGGREGATE_SELECT_QUERY, [$query]); return $query; case 'count': @@ -150,8 +150,6 @@ public function action(string $mode, array $args = []) ->field('count(*)', $args['alias'] ?? null) ->table($this->expr('([]) {}', [$innerQuery, '_tc'])); - $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); - return $query; // case 'field': // case 'fx': From c6c89212d4e2a8142b23c72e971ead2107784029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 00:52:11 +0200 Subject: [PATCH 125/151] fix cs --- src/Model/AggregateModel.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Model/AggregateModel.php b/src/Model/AggregateModel.php index 72734f3ba..39a096d35 100644 --- a/src/Model/AggregateModel.php +++ b/src/Model/AggregateModel.php @@ -81,25 +81,19 @@ public function setGroupBy(array $fields, array $aggregateExpressions = []) foreach ($aggregateExpressions as $name => $seed) { $exprArgs = []; - // if field originally defined in the parent model, then it can be used as part of expression + // if field is defined in the parent model then it can be used in expression if ($this->table->hasField($name)) { $exprArgs = [$this->table->getField($name)]; } $seed['expr'] = $this->table->expr($seed['expr'], $exprArgs); - // now add the expressions here $this->addExpression($name, $seed); } return $this; } - /** - * Adds new field into model. - * - * @param array|object $seed - */ public function addField(string $name, $seed = []): Field { if ($seed instanceof SqlExpressionField) { From c966abe565a96c122f5fcdaf0e65cacd007bdcdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 00:37:16 +0200 Subject: [PATCH 126/151] rename hook const --- src/Model/UnionModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 5801f523c..ff589eea0 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -28,7 +28,7 @@ class UnionModel extends Model { /** @const string */ - public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; + public const HOOK_INIT_UNION_SELECT_QUERY = self::class . '@initUnionSelectQuery'; /** * UnionModel should always be read-only. @@ -174,7 +174,7 @@ public function action(string $mode, array $args = []) $subquery = $this->getSubQuery($fields); $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->table_alias ?? $this->table); - $this->hook(self::HOOK_INIT_SELECT_QUERY, [$query]); + $this->hook(self::HOOK_INIT_UNION_SELECT_QUERY, [$query]); return $query; case 'count': From 26d4b264a99c361e34eb4096f558ed6bc0708a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 01:18:09 +0200 Subject: [PATCH 127/151] rm link to archived reports repo from README --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f8d52765e..0e9ba9553 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ Yes and no. Agile Data is data persistence framework - like ORM it helps you escape raw SQL. Unlike ORM, it maps objects into "data set" and not "data record". Operating with data sets offers higher level of abstraction: ``` php -$vip_clients = (new Client($db))->addCondition('is_vip', true); +$vipClientModel = (new Client($db))->addCondition('is_vip', true); // express total for all VIP client invoices. The value of the variable is an object -$total_due = $vip_clients->ref('Invoice')->action('fx', ['sum', 'total']); +$totalDueModel = $vipClientModel->ref('Invoice')->action('fx', ['sum', 'total']); // single database query is executed here, but not before! -echo $total_due->getOne(); +echo $totalDueModel->getOne(); ``` In other ORM the similar implementation would be either [slow, clumsy, limited or flawed](https://medium.com/@romaninsh/pragmatic-approach-to-reinventing-orm-d9e1bdc336e3). @@ -53,7 +53,7 @@ $api->rest('/clients', new Client($db)); ## Extensibility and Add-ons -ATK Data is extensible and offers wide range of add-ons ranging from [Audit](https://github.com/atk4/audit) and [Aggregation/Reporting](https://github.com/atk4/report). Developer may also implement advanced DB concepts like "[disjoint subtypes](https://nearly.guru/blog/data/disjoint-subtypes-in-php)" - allowing to efficiently persist object-oriented data in your database. +ATK Data is extensible and offers wide range of add-ons like [Audit](https://github.com/atk4/audit). Developer may also implement advanced DB concepts like "[disjoint subtypes](https://nearly.guru/blog/data/disjoint-subtypes-in-php)" - allowing to efficiently persist object-oriented data in your database. Regardless of how your model is constructed and what database backend is used, it can easily be used in conjunction with any 3rd party add-on, like [Charts](https://github.com/atk4/chart). @@ -64,7 +64,7 @@ Designed for medium to large PHP applications and frameworks, ATK Data is a clea - Make your application really database-agnostic. SQL? NoSQL? RestAPI? Cache? Load and store your data with any of these, without refactoring your code. - Execute more on the server. Agile Data converts query logic into server-specific language (e.g. SQL) then delivers you the exact data rows / columns which you need from a single statement, no matter how complex. - Data architecture transparency. As your database structure change, your application code does not need to be refactored. Replace fields with expressions, denormalize/normalize data, join and merge tables. Only update your application in a single place. -- Extensions. "[Audit](https://github.com/atk4/audit)" - transparently record all edits, updates and deletes with "Undo" support. "[Reports](https://github.com/atk4/report)" - add conditions, group results, union results then group them again, join add limit for a great report design. +- Extensions. "[Audit](https://github.com/atk4/audit)" - transparently record all edits, updates and deletes with "Undo" support. - [Out of the box UI](https://github.com/atk4/ui). Who wants to build Admin systems today? Tens of professional components: [Crud](http://ui.agiletoolkit.org/demos/crud.php), [Grid](http://ui.agiletoolkit.org/demos/grid.php), [Form](http://ui.agiletoolkit.org/demos/form3.php) as well as add-ons like [Charts](https://github.com/atk4/chart) can be added to your PHP app with 3-lines of code. - RestAPI server for Agile Data is currently under development. - Agile Data and all extensions mentioned above are licensed under MIT and are free to use. @@ -97,7 +97,7 @@ As a result the UI layer cannot simply discover how your Invoice relate to the C Agile Data addresses this balance. For the presentation logic you can use tools such as [Agile UI](https://github.com/atk4/ui), that consists of generic Crud, Form implementations or other modules which accept the Model protocol of Agile Data: ``` php -$presentation->setModel($business_model); +$presentation->setModel($businessModel); ``` This now re-shifts the balance and makes it possible to implement any generic UI Components, that will work with your custom data model and your custom persistence (database). @@ -136,10 +136,10 @@ class JobReport extends Job { $invoice->addCondition('status', '!=', 'draft'); // each invoice may have multiple lines, which is what we want - $invoice_lines = $invoice->ref('Lines'); + $invoiceLines = $invoice->ref('Lines'); // build relation between job and invoice line - $this->hasMany('InvoiceLines', ['model' => $invoice_lines]) + $this->hasMany('InvoiceLines', ['model' => $invoiceLines]) ->addField('invoiced', ['aggregate' => 'sum', 'field' => 'total', 'type' => 'atk4_money']); // next we need to see how much is reported through timesheets @@ -645,13 +645,13 @@ If the basic query is not fun, how about more complex one? $salary = new Atk4\Data\Persistence\Sql\Query(['connection' => $pdo]); // create few expression objects -$e_ms = $salary->expr('max(salary)'); -$e_df = $salary->expr('TimeStampDiff(month, from_date, to_date)'); +$eMs = $salary->expr('max(salary)'); +$eDf = $salary->expr('TimeStampDiff(month, from_date, to_date)'); // configure our basic query $salary ->table('salary') - ->field(['emp_no', 'max_salary' => $e_ms, 'months' => $e_df]) + ->field(['emp_no', 'max_salary' => $eMs, 'months' => $eDf]) ->group('emp_no') ->order('-max_salary') From 002665af31b0d966f6ffa634735bdd2ce421f1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 01:23:33 +0200 Subject: [PATCH 128/151] rm debug recursion detection --- src/Persistence/Sql/Expression.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Persistence/Sql/Expression.php b/src/Persistence/Sql/Expression.php index 22f256d37..06d06b3c7 100644 --- a/src/Persistence/Sql/Expression.php +++ b/src/Persistence/Sql/Expression.php @@ -363,17 +363,6 @@ protected function isUnescapablePattern($value): bool private function _render(): array { - // DEBUG, remove before merge or rewrite without debug_backtrace() with much higher limit - // some tests for PostgreSQL & MSSQL need stack trace depth more than 500 - $stackTraceLimit = isset($this->connection) - && \Closure::bind(fn ($v) => isset($v->connection), null, Connection::class)($this->connection) - && $this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform - ? 200 - : 1000; - if (count(debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, $stackTraceLimit)) === $stackTraceLimit) { - throw new Exception('Infinite recursion detected'); - } - // - [xxx] = param // - {xxx} = escape // - {{xxx}} = escapeSoft From 63c53cb716527da12342ef2542437e79afd6fec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 01:28:10 +0200 Subject: [PATCH 129/151] rm pushdown note, it was wrong --- docs/unions.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/unions.rst b/docs/unions.rst index e519fb329..9ffae521b 100644 --- a/docs/unions.rst +++ b/docs/unions.rst @@ -104,9 +104,3 @@ Like any other model, UnionModel model can be assigned through a reference. In t Initially a related union can be defined:: $client->hasMany('Transaction', new Transaction()); - -When condition is added on an UnionModel model it will send it down to every nested model. This way the resulting SQL query remains optimized. - -The exception is when field is not mapped to nested model (if it's an Expression or associated with a Join). - -In most cases optimization on the query and UnionModel model is not necessary as it will be done automatically. From af7bba41bbb6dfeeea2ec8e26a14a2b1025d4c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Apr 2022 01:56:29 +0200 Subject: [PATCH 130/151] comments cleanup --- src/Model/UnionModel.php | 62 +++++++++++----------------------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index ff589eea0..46789ea86 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -30,11 +30,7 @@ class UnionModel extends Model /** @const string */ public const HOOK_INIT_UNION_SELECT_QUERY = self::class . '@initUnionSelectQuery'; - /** - * UnionModel should always be read-only. - * - * @var bool - */ + /** @var bool UnionModel should always be read-only */ public $read_only = true; /** @@ -44,20 +40,11 @@ class UnionModel extends Model * If you can define unique ID field, you can specify it inside your * union model. * - * @var string + * @var string|null */ public $id_field; - /** - * Contain array of array containing model and mappings. - * - * $union = [ - * [$model1, ['amount' => 'total_gross'] ], - * [$model2, []] - * ]; - * - * @var array - */ + /** @var array */ public $union = []; /** @var string Derived table alias */ @@ -77,7 +64,7 @@ public function getFieldExpr(Model $model, string $fieldName, string $expr = nul $field = $this->expr('NULL'); } - // Some fields are re-mapped for this nested model + // some fields are re-mapped for this nested model if ($expr !== null) { $field = $model->expr($expr, [$field]); } @@ -90,7 +77,7 @@ public function getFieldExpr(Model $model, string $fieldName, string $expr = nul */ public function addNestedModel(Model $model, array $fieldMap = []): Model { - $this->persistence->add($model); + $this->persistence->add($model); // TODO this must be removed $this->union[] = [$model, $fieldMap]; @@ -137,15 +124,10 @@ public function addCondition($key, $operator = null, $value = null, $forceNested continue; } - switch (func_num_args()) { - case 2: - $nestedModel->addCondition($field, $operator); - - break; - default: - $nestedModel->addCondition($field, $operator, $value); - - break; + if (func_num_args() === 2) { + $nestedModel->addCondition($field, $operator); + } else { + $nestedModel->addCondition($field, $operator, $value); } } catch (CoreException $e) { throw $e @@ -190,11 +172,6 @@ public function action(string $mode, array $args = []) ->addMoreInfo('mode', $mode); } - if (!is_string($args[0])) { - throw (new Exception('action "field" only support string fields')) - ->addMoreInfo('field', $args[0]); - } - $subquery = $this->getSubQuery([$args[0]]); break; @@ -212,8 +189,10 @@ public function action(string $mode, array $args = []) ->addMoreInfo('mode', $mode); } - // Substitute FROM table with our subquery expression - return parent::action($mode, $args)->reset('table')->table($subquery, $this->table_alias ?? $this->table); + $query = parent::action($mode, $args) + ->reset('table')->table($subquery, $this->table_alias ?? $this->table); + + return $query; } /** @@ -243,9 +222,6 @@ public function getSubQuery(array $fields): Query $queryFieldExpressions = []; foreach ($fields as $fieldName) { try { - // UnionModel can be joined with additional table/query - // We don't touch those fields - if (!$this->hasField($fieldName)) { $queryFieldExpressions[$fieldName] = $nestedModel->expr('NULL'); @@ -274,16 +250,13 @@ public function getSubQuery(array $fields): Query } } - // now prepare query $query = $this->persistence->action($nestedModel, 'select', [[]]); + $query->wrapInParentheses = false; foreach ($queryFieldExpressions as $fAlias => $fExpr) { $query->field($fExpr, $fAlias); } - // subquery should not be wrapped in parenthesis, SQLite is especially picky about that - $query->wrapInParentheses = false; - $subqueries[] = $query; } @@ -298,9 +271,8 @@ public function getSubAction(string $action, array $actionArgs = []): Query foreach ($this->union as [$model, $fieldMap]) { $modelActionArgs = $actionArgs; - - // now prepare query - if ($fieldName = $actionArgs[1] ?? null) { + $fieldName = $actionArgs[1] ?? null; + if ($fieldName) { $modelActionArgs[1] = $this->getFieldExpr( $model, $fieldName, @@ -309,8 +281,6 @@ public function getSubAction(string $action, array $actionArgs = []): Query } $query = $model->action($action, $modelActionArgs); - - // subquery should not be wrapped in parenthesis, SQLite is especially picky about that $query->wrapInParentheses = false; $subqueries[] = $query; From 77b11d85366c0832ee139ec5e4d9af8e418a765c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 12 Aug 2023 15:56:56 +0200 Subject: [PATCH 131/151] WIP convert rst to md --- docs/{unions.rst => unions.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{unions.rst => unions.md} (100%) diff --git a/docs/unions.rst b/docs/unions.md similarity index 100% rename from docs/unions.rst rename to docs/unions.md From f3b910a9bbe0a646edc9f9afbf71f9d50d9907c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 12 Aug 2023 17:42:28 +0200 Subject: [PATCH 132/151] fix cs --- tests/ModelUnionTest.php | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 0c3425d6a..c62e07c92 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -131,11 +131,11 @@ public function testActions(): void public function testActions2(): void { $transaction = $this->createTransaction(); - $this->assertSame('5', $transaction->action('count')->getOne()); - $this->assertSame(37.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame('5', $transaction->action('count')->getOne()); + self::assertSame(37.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); $transaction = $this->createSubtractInvoiceTransaction(); - $this->assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); } public function testSubAction1(): void @@ -153,14 +153,14 @@ public function testBasics(): void $client = $this->createClient(); // There are total of 2 clients - $this->assertSame('2', $client->action('count')->getOne()); + self::assertSame('2', $client->action('count')->getOne()); // Client with ID=1 has invoices for 19 - $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); $transaction = $this->createTransaction(); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], ['client_id' => 2, 'name' => 'chair purchase', 'amount' => 4.0], @@ -171,7 +171,7 @@ public function testBasics(): void // Transaction is UnionModel Model $client->hasMany('Transaction', ['model' => $transaction]); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => 4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => 15.0], ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], @@ -181,7 +181,7 @@ public function testBasics(): void $transaction = $this->createSubtractInvoiceTransaction(); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], @@ -192,7 +192,7 @@ public function testBasics(): void // Transaction is UnionModel Model $client->hasMany('Transaction', ['model' => $transaction]); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], @@ -244,7 +244,7 @@ public function testGrouping2(): void ]); $transactionAggregate->setOrder('name'); - $this->assertSame([ + self::assertSame([ ['name' => 'chair purchase', 'amount' => 8.0], ['name' => 'full pay', 'amount' => 4.0], ['name' => 'prepay', 'amount' => 10.0], @@ -263,7 +263,7 @@ public function testGrouping2(): void ]); $transactionAggregate->setOrder('name'); - $this->assertSame([ + self::assertSame([ ['name' => 'chair purchase', 'amount' => -8.0], ['name' => 'full pay', 'amount' => 4.0], ['name' => 'prepay', 'amount' => 10.0], @@ -297,7 +297,7 @@ public function testSubGroupingByExpressions(): void $this->markTestIncomplete('TODO MSSQL: Constant value column seem not supported (Invalid column name \'type\')'); } - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['type' => 'invoice', 'amount' => 23.0], ['type' => 'payment', 'amount' => 14.0], ], $transactionAggregate->export(['type', 'amount'])); @@ -312,7 +312,7 @@ public function testSubGroupingByExpressions(): void 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], ]); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['type' => 'invoice', 'amount' => -23.0], ['type' => 'payment', 'amount' => 14.0], ], $transactionAggregate->export(['type', 'amount'])); @@ -324,15 +324,15 @@ public function testReference(): void $client->hasMany('tr', ['model' => $this->createTransaction()]); if (\PHP_MAJOR_VERSION >= 7) { // always true, TODO aggregate on reference is broken - $this->assertTrue(true); + self::assertTrue(true); return; } - $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); - $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); - $this->assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( 'select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "_t_e7d707a26e7f"', @@ -342,9 +342,9 @@ public function testReference(): void $client = $this->createClient(); $client->hasMany('tr', ['model' => $this->createSubtractInvoiceTransaction()]); - $this->assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); - $this->assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); - $this->assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); + self::assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "_t_e7d707a26e7f"', @@ -365,7 +365,7 @@ public function testFieldAggregateUnion(): void // TODO failing on all DBs expect Sqlite, MySQL uses "semi-joins" for this type of query which does not support UNION // and therefore it complains about "client"."id" field, see: // http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 - $this->assertTrue(true); + self::assertTrue(true); return; } @@ -381,7 +381,7 @@ public function testConditionOnUnionField(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '<', 0); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], @@ -393,7 +393,7 @@ public function testConditionOnNestedModelField(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('client_id', '>', 1); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); @@ -404,7 +404,7 @@ public function testConditionForcedOnNestedModel1(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '>', 5, true); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ], $transaction->export()); } @@ -414,7 +414,7 @@ public function testConditionForcedOnNestedModel2(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition('amount', '<', -10, true); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], ], $transaction->export()); } @@ -424,7 +424,7 @@ public function testConditionExpression(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->addCondition($transaction->expr('{} > 5', ['amount'])); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], ], $transaction->export()); } @@ -437,7 +437,7 @@ public function testConditionOnMappedField(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->nestedInvoice->addCondition('amount', 4); - $this->assertSameExportUnordered([ + self::assertSameExportUnordered([ ['client_id' => 1, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 2, 'name' => 'chair purchase', 'amount' => -4.0], ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], From f9ee26faea48757db95bde521e5b6cb72597d576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 12 Aug 2023 19:04:04 +0200 Subject: [PATCH 133/151] fix merge --- tests/ModelUnionTest.php | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index c62e07c92..a7921b2e7 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -56,8 +56,8 @@ public function testFieldExpr(): void { $transaction = $this->createSubtractInvoiceTransaction(); - $this->assertSameSql('"amount"', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount')])->render()[0]); - $this->assertSameSql('-"amount"', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount', '-[]')])->render()[0]); + $this->assertSameSql('`amount`', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount')])->render()[0]); + $this->assertSameSql('-`amount`', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'amount', '-[]')])->render()[0]); $this->assertSameSql('-NULL', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'blah', '-[]')])->render()[0]); } @@ -66,17 +66,17 @@ public function testNestedQuery1(): void $transaction = $this->createTransaction(); $this->assertSameSql( - 'select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment"', + 'select `name` `name` from `invoice` UNION ALL select `name` `name` from `payment`', $transaction->getSubQuery(['name'])->render()[0] ); $this->assertSameSql( - 'select "name" "name", "amount" "amount" from "invoice" UNION ALL select "name" "name", "amount" "amount" from "payment"', + 'select `name` `name`, `amount` `amount` from `invoice` UNION ALL select `name` `name`, `amount` `amount` from `payment`', $transaction->getSubQuery(['name', 'amount'])->render()[0] ); $this->assertSameSql( - 'select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment"', + 'select `name` `name` from `invoice` UNION ALL select `name` `name` from `payment`', $transaction->getSubQuery(['name'])->render()[0] ); } @@ -91,7 +91,7 @@ public function testMissingField(): void $transaction->addField('type'); $this->assertSameSql( - 'select (\'invoice\') "type", "amount" "amount" from "invoice" UNION ALL select NULL "type", "amount" "amount" from "payment"', + 'select (\'invoice\') `type`, `amount` `amount` from `invoice` UNION ALL select NULL `type`, `amount` `amount` from `payment`', $transaction->getSubQuery(['type', 'amount'])->render()[0] ); } @@ -101,29 +101,29 @@ public function testActions(): void $transaction = $this->createTransaction(); $this->assertSameSql( - 'select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", "amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "_tu"', + 'select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`', $transaction->action('select')->render()[0] ); $this->assertSameSql( - 'select "name" from (select "name" "name" from "invoice" UNION ALL select "name" "name" from "payment") "_tu"', + 'select `name` from (select `name` `name` from `invoice` UNION ALL select `name` `name` from `payment`) `_tu`', $transaction->action('field', ['name'])->render()[0] ); $this->assertSameSql( - 'select sum("cnt") from (select count(*) "cnt" from "invoice" UNION ALL select count(*) "cnt" from "payment") "_tu"', + 'select sum(`cnt`) from (select count(*) `cnt` from `invoice` UNION ALL select count(*) `cnt` from `payment`) `_tu`', $transaction->action('count')->render()[0] ); $this->assertSameSql( - 'select sum("val") from (select sum("amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "_tu"', + 'select sum(`val`) from (select sum(`amount`) `val` from `invoice` UNION ALL select sum(`amount`) `val` from `payment`) `_tu`', $transaction->action('fx', ['sum', 'amount'])->render()[0] ); $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( - 'select sum("val") from (select sum(-"amount") "val" from "invoice" UNION ALL select sum("amount") "val" from "payment") "_tu"', + 'select sum(`val`) from (select sum(-`amount`) `val` from `invoice` UNION ALL select sum(`amount`) `val` from `payment`) `_tu`', $transaction->action('fx', ['sum', 'amount'])->render()[0] ); } @@ -143,7 +143,7 @@ public function testSubAction1(): void $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( - 'select sum(-"amount") from "invoice" UNION ALL select sum("amount") from "payment"', + 'select sum(-`amount`) from `invoice` UNION ALL select sum(`amount`) from `payment`', $transaction->getSubAction('fx', ['sum', 'amount'])->render()[0] ); } @@ -209,7 +209,7 @@ public function testGrouping1(): void ]); $this->assertSameSql( - 'select "name", sum("amount") "amount" from (select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", "amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "_tu") "_tm" group by "name"', + 'select `name`, sum(`amount`) `amount` from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_tm` group by `name`', $transactionAggregate->action('select', [['name', 'amount']])->render()[0] ); @@ -221,7 +221,7 @@ public function testGrouping1(): void ]); $this->assertSameSql( - 'select "name", sum("amount") "amount" from (select "client_id", "name", "amount" from (select "client_id" "client_id", "name" "name", -"amount" "amount" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount" from "payment") "_tu") "_tm" group by "name"', + 'select `name`, sum(`amount`) `amount` from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, -`amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_tm` group by `name`', $transactionAggregate->action('select', [['name', 'amount']])->render()[0] ); } @@ -289,7 +289,7 @@ public function testSubGroupingByExpressions(): void // TODO subselects should not select "client" and "name" fields $this->assertSameSql( - 'select "type", sum("amount") "amount" from (select "client_id", "name", "amount", "type" from (select "client_id" "client_id", "name" "name", "amount" "amount", (\'invoice\') "type" from "invoice" UNION ALL select "client_id" "client_id", "name" "name", "amount" "amount", (\'payment\') "type" from "payment") "_tu") "_tm" group by "type"', + 'select `type`, sum(`amount`) `amount` from (select `client_id`, `name`, `amount`, `type` from (select `client_id` `client_id`, `name` `name`, `amount` `amount`, (\'invoice\') `type` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount`, (\'payment\') `type` from `payment`) `_tu`) `_tm` group by `type`', $transactionAggregate->action('select')->render()[0] ); @@ -335,7 +335,7 @@ public function testReference(): void self::assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - 'select sum("val") from (select sum("amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "_t_e7d707a26e7f"', + 'select sum(`val`) from (select sum(`amount`) `val` from `invoice` where `client_id` = :a UNION ALL select sum(`amount`) `val` from `payment` where `client_id` = :b) `_t_e7d707a26e7f`', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); @@ -347,7 +347,7 @@ public function testReference(): void self::assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - 'select sum("val") from (select sum(-"amount") "val" from "invoice" where "client_id" = :a UNION ALL select sum("amount") "val" from "payment" where "client_id" = :b) "_t_e7d707a26e7f"', + 'select sum(`val`) from (select sum(-`amount`) `val` from `invoice` where `client_id` = :a UNION ALL select sum(`amount`) `val` from `payment` where `client_id` = :b) `_t_e7d707a26e7f`', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); } @@ -363,7 +363,7 @@ public function testFieldAggregateUnion(): void || $this->getDatabasePlatform() instanceof SQLServerPlatform || $this->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\OraclePlatform) { // TODO failing on all DBs expect Sqlite, MySQL uses "semi-joins" for this type of query which does not support UNION - // and therefore it complains about "client"."id" field, see: + // and therefore it complains about `client`.`id` field, see: // http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 self::assertTrue(true); @@ -371,7 +371,7 @@ public function testFieldAggregateUnion(): void } $this->assertSameSql( - 'select "id", "name", "surname", "order", (select coalesce(sum("val"), 0) from (select coalesce(sum("amount"), 0) "val" from "invoice" UNION ALL select coalesce(sum("amount"), 0) "val" from "payment") "_t_e7d707a26e7f" where "client_id" = "client"."id") "balance" from "client" group by "id" having "id" = :a', + 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`val`), 0) from (select coalesce(sum(`amount`), 0) `val` from `invoice` UNION ALL select coalesce(sum(`amount`), 0) `val` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id` having `id` = :a', $client->load(1)->action('select')->render()[0] ); } From 639bdb46df0cacf6a70037c26ce99d4c76a29d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 12 Aug 2023 19:04:44 +0200 Subject: [PATCH 134/151] fix merge II --- docs/unions.md | 4 ++-- src/Model/UnionModel.php | 25 +++++++++++-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/unions.md b/docs/unions.md index 9ffae521b..bdec537a6 100644 --- a/docs/unions.md +++ b/docs/unions.md @@ -82,8 +82,8 @@ The key of the field map array must match the UnionModel field. The value is an This format can also be used to reverse sign on amounts. When we are creating "Transactions", then invoices would be subtracted from the amount, while payments will be added:: - $nestedPayment = $m_uni->addNestedModel(new Invoice(), ['amount' => '-[amount]']); - $nestedInvoice = $m_uni->addNestedModel(new Payment(), ['description' => '[note]']); + $nestedPayment = $mUnion->addNestedModel(new Invoice(), ['amount' => '-[amount]']); + $nestedInvoice = $mUnion->addNestedModel(new Payment(), ['description' => '[note]']); $unionPaymentInvoice->addField('description'); Should more flexibility be needed, more expressions (or fields) can be added directly to nested models:: diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 46789ea86..ae2e5445a 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -27,22 +27,19 @@ */ class UnionModel extends Model { - /** @const string */ public const HOOK_INIT_UNION_SELECT_QUERY = self::class . '@initUnionSelectQuery'; - /** @var bool UnionModel should always be read-only */ - public $read_only = true; + /** UnionModel should always be read-only */ + public bool $readOnly = true; /** - * UnionModel normally does not have ID field. Setting this to null will + * UnionModel normally does not have ID field. Setting this to false will * disable various per-id operations, such as load(). * * If you can define unique ID field, you can specify it inside your * union model. - * - * @var string|null */ - public $id_field; + public $idField = false; /** @var array */ public $union = []; @@ -77,7 +74,7 @@ public function getFieldExpr(Model $model, string $fieldName, string $expr = nul */ public function addNestedModel(Model $model, array $fieldMap = []): Model { - $this->persistence->add($model); // TODO this must be removed + $model->setPersistence($this->getpersistence()); // TODO this must be removed $this->union[] = [$model, $fieldMap]; @@ -149,12 +146,12 @@ public function action(string $mode, array $args = []) // get list of available fields $fields = $this->onlyFields ?: array_keys($this->getFields()); foreach ($fields as $k => $field) { - if ($this->getField($field)->never_persist) { + if ($this->getField($field)->neverPersist) { unset($fields[$k]); } } $subquery = $this->getSubQuery($fields); - $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->table_alias ?? $this->table); + $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->tableAlias ?? $this->table); $this->hook(self::HOOK_INIT_UNION_SELECT_QUERY, [$query]); @@ -190,7 +187,7 @@ public function action(string $mode, array $args = []) } $query = parent::action($mode, $args) - ->reset('table')->table($subquery, $this->table_alias ?? $this->table); + ->reset('table')->table($subquery, $this->tableAlias ?? $this->table); return $query; } @@ -200,7 +197,7 @@ public function action(string $mode, array $args = []) */ private function createUnionQuery(array $subqueries): Query { - $unionQuery = $this->persistence->dsql(); + $unionQuery = $this->getPersistence()->dsql(); $unionQuery->mode = 'union_all'; \Closure::bind(function () use ($unionQuery, $subqueries) { $unionQuery->template = implode(' UNION ALL ', array_fill(0, count($subqueries), '[]')); @@ -230,7 +227,7 @@ public function getSubQuery(array $fields): Query $field = $this->getField($fieldName); - if ($field->hasJoin() || $field->never_persist) { + if ($field->hasJoin() || $field->neverPersist) { continue; } @@ -250,7 +247,7 @@ public function getSubQuery(array $fields): Query } } - $query = $this->persistence->action($nestedModel, 'select', [[]]); + $query = $this->getPersistence()->action($nestedModel, 'select', [[]]); $query->wrapInParentheses = false; foreach ($queryFieldExpressions as $fAlias => $fExpr) { From bd15f656e057e4905d82ee80448488479a99b1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 12 Aug 2023 19:05:47 +0200 Subject: [PATCH 135/151] WIP comment out failing test --- tests/ModelUnionTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index a7921b2e7..9d58c5d24 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -352,7 +352,7 @@ public function testReference(): void ); } - public function testFieldAggregateUnion(): void + /* public function testFieldAggregateUnion(): void { $client = $this->createClient(); $client->hasMany('tr', ['model' => $this->createTransaction()]) @@ -374,7 +374,7 @@ public function testFieldAggregateUnion(): void 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`val`), 0) from (select coalesce(sum(`amount`), 0) `val` from `invoice` UNION ALL select coalesce(sum(`amount`), 0) `val` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id` having `id` = :a', $client->load(1)->action('select')->render()[0] ); - } + } */ public function testConditionOnUnionField(): void { From 88846789a077993e10786dbe74f063ab4343d9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 12 Aug 2023 19:08:08 +0200 Subject: [PATCH 136/151] fix cs --- tests/ModelUnionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 9d58c5d24..d5678ca63 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -294,7 +294,7 @@ public function testSubGroupingByExpressions(): void ); if ($this->getDatabasePlatform() instanceof SQLServerPlatform) { - $this->markTestIncomplete('TODO MSSQL: Constant value column seem not supported (Invalid column name \'type\')'); + self::markTestIncomplete('TODO MSSQL: Constant value column seem not supported (Invalid column name \'type\')'); } self::assertSameExportUnordered([ From d4c6fa697277acd587ce4c70bf3eea413682ca0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 12 Aug 2023 23:36:45 +0200 Subject: [PATCH 137/151] convert rst to md --- docs/unions.md | 137 +++++++++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 62 deletions(-) diff --git a/docs/unions.md b/docs/unions.md index bdec537a6..1de3ef2af 100644 --- a/docs/unions.md +++ b/docs/unions.md @@ -1,106 +1,119 @@ +:::{php:namespace} Atk4\Data\Model +::: -.. _Unions: +(Unions)= -============ -Model Unions -============ +# Model Unions -.. php:namespace:: Atk4\Data\Model - -.. php:class:: UnionModel +:::{php:class} UnionModel +::: In some cases data from multiple models need to be combined. In this case the UnionModel model comes very handy. -In the case used below Client model schema may have multiple invoices and multiple payments. Payment is not related to the invoice.:: +In the case used below Client model schema may have multiple invoices and multiple payments. Payment is not related to the invoice.: - class Client extends \Atk4\Data\Model { - public $table = 'client'; +``` +class Client extends \Atk4\Data\Model { + public $table = 'client'; - protected function init(): void - { - parent::init(); + protected function init(): void + { + parent::init(); - $this->addField('name'); + $this->addField('name'); - $this->hasMany('Payment'); - $this->hasMany('Invoice'); - } - } + $this->hasMany('Payment'); + $this->hasMany('Invoice'); + } +} +``` (see tests/ModelUnionTest.php, tests/Model/Client.php, tests/Model/Payment.php and tests/Model/Invoice.php files). -Union Model Definition ----------------------- +## Union Model Definition Normally a model is associated with a single table. Union model can have multiple nested models defined and it fetches results from that. As a result, Union model will have no "id" field. Below is an example of inline definition of Union model. -The Union model can be separated in a designated class and nested model added within the init() method body of the new class:: +The Union model can be separated in a designated class and nested model added within the init() method body of the new class: - $unionPaymentInvoice = new \Atk4\Data\Model\Union(); +``` +$unionPaymentInvoice = new \Atk4\Data\Model\Union(); - $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); - $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment()); +$nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); +$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment()); +``` -Next, assuming that both models have common fields "name" and "amount", `$unionPaymentInvoice` fields can be set:: +Next, assuming that both models have common fields "name" and "amount", `$unionPaymentInvoice` fields can be set: - $unionPaymentInvoice->addField('name'); - $unionPaymentInvoice->addField('amount', ['type' => 'atk4_money']); +``` +$unionPaymentInvoice->addField('name'); +$unionPaymentInvoice->addField('amount', ['type' => 'atk4_money']); +``` -Then data can be queried:: +Then data can be queried: - $unionPaymentInvoice->export(); +``` +$unionPaymentInvoice->export(); +``` -Define Fields ------------------- +## Define Fields -Below is an example of 3 different ways to define fields for the UnionModel model:: +Below is an example of 3 different ways to define fields for the UnionModel model: - // Will link the "name" field with all the nested models. - $unionPaymentInvoice->addField('client_id'); +``` +// will link the "name" field with all the nested models +$unionPaymentInvoice->addField('client_id'); - // Expression will not affect nested models in any way - $unionPaymentInvoice->addExpression('name_capital', ['expr' => 'upper([name])']); +// Expression will not affect nested models in any way +$unionPaymentInvoice->addExpression('name_capital', ['expr' => 'upper([name])']); - // UnionModel model can be joined with extra tables and define some fields from those joins - $unionPaymentInvoice - ->join('client', 'client_id') - ->addField('client_name', 'name'); +// UnionModel model can be joined with extra tables and define some fields from those joins +$unionPaymentInvoice + ->join('client', 'client_id') + ->addField('client_name', 'name'); +``` -:ref:`Expressions` and :ref:`Joins` are working just as they would on any other model. +{ref}`Expressions` and {ref}`Joins` are working just as they would on any other model. -Field Mapping -------------- +## Field Mapping Sometimes the field that is defined in the UnionModel model may be named differently inside nested models. E.g. Invoice has field "description" and payment has field "note". -When defining a nested model a field map array needs to be specified:: +When defining a nested model a field map array needs to be specified: - $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); - $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); - $unionPaymentInvoice->addField('description'); +``` +$nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); +$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); +$unionPaymentInvoice->addField('description'); +``` -The key of the field map array must match the UnionModel field. The value is an expression. (See :ref:`Model`). +The key of the field map array must match the UnionModel field. The value is an expression. (See {ref}`Model`). This format can also be used to reverse sign on amounts. When we are creating "Transactions", then invoices would be -subtracted from the amount, while payments will be added:: +subtracted from the amount, while payments will be added: - $nestedPayment = $mUnion->addNestedModel(new Invoice(), ['amount' => '-[amount]']); - $nestedInvoice = $mUnion->addNestedModel(new Payment(), ['description' => '[note]']); - $unionPaymentInvoice->addField('description'); +``` +$nestedPayment = $mUnion->addNestedModel(new Invoice(), ['amount' => '-[amount]']); +$nestedInvoice = $mUnion->addNestedModel(new Payment(), ['description' => '[note]']); +$unionPaymentInvoice->addField('description'); +``` -Should more flexibility be needed, more expressions (or fields) can be added directly to nested models:: +Should more flexibility be needed, more expressions (or fields) can be added directly to nested models: - $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice(), ['amount' => '-[amount]']); - $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); +``` +$nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice(), ['amount' => '-[amount]']); +$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); - $nestedPayment->addExpression('type', ['expr' => '\'payment\'']); - $nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); - $unionPaymentInvoice->addField('type'); +$nestedPayment->addExpression('type', ['expr' => '\'payment\'']); +$nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); +$unionPaymentInvoice->addField('type'); +``` A new field "type" has been added that will be defined as a static constant. -Referencing an UnionModel Model --------------------------- +## Referencing an UnionModel Model Like any other model, UnionModel model can be assigned through a reference. In the case here one Client can have multiple transactions. -Initially a related union can be defined:: +Initially a related union can be defined: - $client->hasMany('Transaction', new Transaction()); +``` +$client->hasMany('Transaction', new Transaction()); +``` From 48117ace59694491b2846760ebcaa09c740b90da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Thu, 1 Feb 2024 14:52:52 +0100 Subject: [PATCH 138/151] fix stan --- docs/unions.md | 6 ++-- src/Model/UnionModel.php | 67 ++++++++++++++++++------------------- tests/Model/Payment.php | 1 + tests/Model/Transaction.php | 1 + tests/ModelUnionTest.php | 3 +- 5 files changed, 40 insertions(+), 38 deletions(-) diff --git a/docs/unions.md b/docs/unions.md index 1de3ef2af..f0a47f9e7 100644 --- a/docs/unions.md +++ b/docs/unions.md @@ -1,11 +1,11 @@ -:::{php:namespace} Atk4\Data\Model +:::{php:namespace} Atk4\Data ::: (Unions)= # Model Unions -:::{php:class} UnionModel +:::{php:class} Model\UnionModel ::: In some cases data from multiple models need to be combined. In this case the UnionModel model comes very handy. @@ -86,7 +86,7 @@ $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['descripti $unionPaymentInvoice->addField('description'); ``` -The key of the field map array must match the UnionModel field. The value is an expression. (See {ref}`Model`). +The key of the field map array must match the UnionModel field. The value is an expression. (See {php:meth}`Model::addExpression`). This format can also be used to reverse sign on amounts. When we are creating "Transactions", then invoices would be subtracted from the amount, while payments will be added: diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 5ffc185dc..00073e553 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -11,6 +11,7 @@ use Atk4\Data\Model; use Atk4\Data\Persistence; use Atk4\Data\Persistence\Sql\Expression; +use Atk4\Data\Persistence\Sql\Expressionable; use Atk4\Data\Persistence\Sql\Query; /** @@ -42,12 +43,12 @@ class UnionModel extends Model */ public $idField = false; - /** @var array */ - public $union = []; - - /** @var string Derived table alias */ + /** Derived table alias */ public $table = '_tu'; + /** @var list}> */ + public $union = []; + /** * For a sub-model with a specified mapping, return expression * that represents a field. @@ -72,10 +73,12 @@ public function getFieldExpr(Model $model, string $fieldName, string $expr = nul /** * Adds nested model in union. + * + * @param array $fieldMap */ public function addNestedModel(Model $model, array $fieldMap = []): Model { - $model->setPersistence($this->getpersistence()); // TODO this must be removed + $model->setPersistence($this->getPersistence()); // TODO this must be removed $this->union[] = [$model, $fieldMap]; @@ -86,46 +89,40 @@ public function addNestedModel(Model $model, array $fieldMap = []): Model * If UnionModel model has such field, then add condition to it. * Otherwise adds condition to all nested models. * - * @param mixed $key - * @param mixed $operator - * @param mixed $value - * @param bool $forceNested Should we add condition to all nested models? - * - * @return $this + * @param bool $forceNested Should we add condition to all nested models? */ - public function addCondition($key, $operator = null, $value = null, $forceNested = false) + #[\Override] + public function addCondition($field, $operator = null, $value = null, $forceNested = false) { - if (func_num_args() === 1) { - return parent::addCondition($key); + if ('func_num_args'() === 1) { + return parent::addCondition($field); } // if UnionModel has such field, then add condition to it - if ($this->hasField($key) && !$forceNested) { - return parent::addCondition(...func_get_args()); + if ($this->hasField($field) && !$forceNested) { + return parent::addCondition(...'func_get_args'()); } // otherwise add condition in all nested models foreach ($this->union as [$nestedModel, $fieldMap]) { try { - $field = $key; - - if (isset($fieldMap[$key])) { + if (isset($fieldMap[$field])) { // field is included in mapping - use mapping expression - $field = $fieldMap[$key] instanceof Expression - ? $fieldMap[$key] - : $this->getFieldExpr($nestedModel, $key, $fieldMap[$key]); - } elseif (is_string($key) && $nestedModel->hasField($key)) { + $f = $fieldMap[$field] instanceof Expression + ? $fieldMap[$field] + : $this->getFieldExpr($nestedModel, $field, $fieldMap[$field]); + } elseif (is_string($field) && $nestedModel->hasField($field)) { // model has such field - use that field directly - $field = $nestedModel->getField($key); + $f = $nestedModel->getField($field); } else { // we don't know what to do, so let's do nothing continue; } - if (func_num_args() === 2) { - $nestedModel->addCondition($field, $operator); + if ('func_num_args'() === 2) { + $nestedModel->addCondition($f, $operator); } else { - $nestedModel->addCondition($field, $operator, $value); + $nestedModel->addCondition($f, $operator, $value); } } catch (CoreException $e) { throw $e @@ -136,16 +133,14 @@ public function addCondition($key, $operator = null, $value = null, $forceNested return $this; } - /** - * @return Query - */ + #[\Override] public function action(string $mode, array $args = []) { $subquery = null; switch ($mode) { case 'select': // get list of available fields - $fields = $this->onlyFields ?: array_keys($this->getFields()); + $fields = $this->onlyFields ?? array_keys($this->getFields()); foreach ($fields as $k => $field) { if ($this->getField($field)->neverPersist) { unset($fields[$k]); @@ -194,7 +189,7 @@ public function action(string $mode, array $args = []) } /** - * @param Query[] $subqueries + * @param list $subqueries */ private function createUnionQuery(array $subqueries): Query { @@ -210,11 +205,12 @@ private function createUnionQuery(array $subqueries): Query /** * Configures nested models to have a specified set of fields available. + * + * @param list $fields */ public function getSubQuery(array $fields): Query { $subqueries = []; - foreach ($this->union as [$nestedModel, $fieldMap]) { // map fields for related model $queryFieldExpressions = []; @@ -263,10 +259,12 @@ public function getSubQuery(array $fields): Query return $unionQuery; } + /** + * @param array $actionArgs + */ public function getSubAction(string $action, array $actionArgs = []): Query { $subqueries = []; - foreach ($this->union as [$model, $fieldMap]) { $modelActionArgs = $actionArgs; $fieldName = $actionArgs[1] ?? null; @@ -289,6 +287,7 @@ public function getSubAction(string $action, array $actionArgs = []): Query return $unionQuery; } + #[\Override] public function __debugInfo(): array { return array_merge(parent::__debugInfo(), [ diff --git a/tests/Model/Payment.php b/tests/Model/Payment.php index 375510230..ff1f47ab6 100644 --- a/tests/Model/Payment.php +++ b/tests/Model/Payment.php @@ -10,6 +10,7 @@ class Payment extends Model { public $table = 'payment'; + #[\Override] protected function init(): void { parent::init(); diff --git a/tests/Model/Transaction.php b/tests/Model/Transaction.php index 87a64736a..7f829002c 100644 --- a/tests/Model/Transaction.php +++ b/tests/Model/Transaction.php @@ -16,6 +16,7 @@ class Transaction extends UnionModel /** @var bool */ public $subtractInvoice; + #[\Override] protected function init(): void { parent::init(); diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index d5678ca63..63dc37e71 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -10,6 +10,7 @@ class ModelUnionTest extends TestCase { + #[\Override] protected function setUp(): void { parent::setUp(); @@ -324,7 +325,7 @@ public function testReference(): void $client->hasMany('tr', ['model' => $this->createTransaction()]); if (\PHP_MAJOR_VERSION >= 7) { // always true, TODO aggregate on reference is broken - self::assertTrue(true); + self::assertTrue(true); // @phpstan-ignore-line return; } From a53543a32ddba22746f6cd5408f195ca63560256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 2 Feb 2024 22:43:30 +0100 Subject: [PATCH 139/151] fix test /w reference and has many --- src/Model/UnionInternalTable.php | 46 ++++++++++++++++++++++++++ src/Model/UnionModel.php | 57 +++++++++++++++++++++----------- tests/ModelUnionTest.php | 18 +++++----- 3 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 src/Model/UnionInternalTable.php diff --git a/src/Model/UnionInternalTable.php b/src/Model/UnionInternalTable.php new file mode 100644 index 000000000..418c9593a --- /dev/null +++ b/src/Model/UnionInternalTable.php @@ -0,0 +1,46 @@ +actionInnerTable(). + * + * Called from https://github.com/atk4/data/blob/5.0.0/src/Persistence/Sql.php#L188. + * + * @method Model getOwner() + * + * @internal + */ +class UnionInternalTable +{ + use TrackableTrait; + + /** + * @param array $args + * + * @return Persistence\Sql\Query + */ + public function action(string $mode, array $args = []) + { + if ($mode !== 'select' || $args !== []) { + throw new Exception('Only "select" action with empty arguments is expected'); + } + + $model = $this->getOwner(); + + $tableOrig = $model->table; + $model->table = '_tu'; + try { + return $model->actionSelectInnerTable(); // @phpstan-ignore-line + } finally { + $model->table = $tableOrig; + } + } +} diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 00073e553..04af2d5e7 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -10,9 +10,6 @@ use Atk4\Data\Field\SqlExpressionField; use Atk4\Data\Model; use Atk4\Data\Persistence; -use Atk4\Data\Persistence\Sql\Expression; -use Atk4\Data\Persistence\Sql\Expressionable; -use Atk4\Data\Persistence\Sql\Query; /** * UnionModel model combines multiple nested models through a UNION in order to retrieve @@ -25,7 +22,7 @@ * * @property Persistence\Sql $persistence * - * @method Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model + * @method Persistence\Sql\Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model */ class UnionModel extends Model { @@ -43,17 +40,28 @@ class UnionModel extends Model */ public $idField = false; - /** Derived table alias */ - public $table = '_tu'; - - /** @var list}> */ + /** @var list}> */ public $union = []; + /** + * @param array $defaults + */ + public function __construct(Persistence $persistence = null, array $defaults = []) + { + $unionTable = new UnionInternalTable(); + $unionTable->setOwner($this); + $this->table = $unionTable; // @phpstan-ignore-line + + $this->tableAlias ??= '_tu'; // DEBUG + + parent::__construct($persistence, $defaults); + } + /** * For a sub-model with a specified mapping, return expression * that represents a field. * - * @return Field|Expression + * @return Field|Persistence\Sql\Expression */ public function getFieldExpr(Model $model, string $fieldName, string $expr = null) { @@ -74,7 +82,7 @@ public function getFieldExpr(Model $model, string $fieldName, string $expr = nul /** * Adds nested model in union. * - * @param array $fieldMap + * @param array $fieldMap */ public function addNestedModel(Model $model, array $fieldMap = []): Model { @@ -108,7 +116,7 @@ public function addCondition($field, $operator = null, $value = null, $forceNest try { if (isset($fieldMap[$field])) { // field is included in mapping - use mapping expression - $f = $fieldMap[$field] instanceof Expression + $f = $fieldMap[$field] instanceof Persistence\Sql\Expression ? $fieldMap[$field] : $this->getFieldExpr($nestedModel, $field, $fieldMap[$field]); } elseif (is_string($field) && $nestedModel->hasField($field)) { @@ -133,6 +141,14 @@ public function addCondition($field, $operator = null, $value = null, $forceNest return $this; } + /** + * @return Persistence\Sql\Query + */ + public function actionSelectInnerTable() + { + return $this->action('select'); + } + #[\Override] public function action(string $mode, array $args = []) { @@ -147,7 +163,7 @@ public function action(string $mode, array $args = []) } } $subquery = $this->getSubQuery($fields); - $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->tableAlias ?? $this->table); + $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->tableAlias); $this->hook(self::HOOK_INIT_UNION_SELECT_QUERY, [$query]); @@ -170,34 +186,35 @@ public function action(string $mode, array $args = []) break; case 'fx': case 'fx0': - $args['alias'] = 'val'; + return parent::action($mode, $args); + /* $args['alias'] = 'val'; $subquery = $this->getSubAction($mode, $args); $args = [$args[0], $this->expr('{}', ['val'])]; - break; + break; */ default: throw (new Exception('UnionModel model does not support this action')) ->addMoreInfo('mode', $mode); } $query = parent::action($mode, $args) - ->reset('table')->table($subquery, $this->tableAlias ?? $this->table); + ->reset('table')->table($subquery, $this->tableAlias); return $query; } /** - * @param list $subqueries + * @param list $subqueries */ - private function createUnionQuery(array $subqueries): Query + private function createUnionQuery(array $subqueries): Persistence\Sql\Query { $unionQuery = $this->getPersistence()->dsql(); $unionQuery->mode = 'union_all'; \Closure::bind(static function () use ($unionQuery, $subqueries) { $unionQuery->template = implode(' UNION ALL ', array_fill(0, count($subqueries), '[]')); - }, null, Query::class)(); + }, null, Persistence\Sql\Query::class)(); $unionQuery->args['custom'] = $subqueries; return $unionQuery; @@ -208,7 +225,7 @@ private function createUnionQuery(array $subqueries): Query * * @param list $fields */ - public function getSubQuery(array $fields): Query + public function getSubQuery(array $fields): Persistence\Sql\Query { $subqueries = []; foreach ($this->union as [$nestedModel, $fieldMap]) { @@ -262,7 +279,7 @@ public function getSubQuery(array $fields): Query /** * @param array $actionArgs */ - public function getSubAction(string $action, array $actionArgs = []): Query + public function getSubAction(string $action, array $actionArgs = []): Persistence\Sql\Query { $subqueries = []; foreach ($this->union as [$model, $fieldMap]) { diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 63dc37e71..7fd33b2c1 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -117,14 +117,16 @@ public function testActions(): void ); $this->assertSameSql( - 'select sum(`val`) from (select sum(`amount`) `val` from `invoice` UNION ALL select sum(`amount`) `val` from `payment`) `_tu`', + // QUERY IS WIP + 'select sum(`amount`) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_tu`', $transaction->action('fx', ['sum', 'amount'])->render()[0] ); $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( - 'select sum(`val`) from (select sum(-`amount`) `val` from `invoice` UNION ALL select sum(`amount`) `val` from `payment`) `_tu`', + // QUERY IS WIP + 'select sum(`amount`) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, -`amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_tu`', $transaction->action('fx', ['sum', 'amount'])->render()[0] ); } @@ -324,19 +326,14 @@ public function testReference(): void $client = $this->createClient(); $client->hasMany('tr', ['model' => $this->createTransaction()]); - if (\PHP_MAJOR_VERSION >= 7) { // always true, TODO aggregate on reference is broken - self::assertTrue(true); // @phpstan-ignore-line - - return; - } - self::assertSame(19.0, (float) $client->load(1)->ref('Invoice')->action('fx', ['sum', 'amount'])->getOne()); self::assertSame(10.0, (float) $client->load(1)->ref('Payment')->action('fx', ['sum', 'amount'])->getOne()); self::assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - 'select sum(`val`) from (select sum(`amount`) `val` from `invoice` where `client_id` = :a UNION ALL select sum(`amount`) `val` from `payment` where `client_id` = :b) `_t_e7d707a26e7f`', + // QUERY IS WIP + 'select sum(`amount`) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = :a', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); @@ -348,7 +345,8 @@ public function testReference(): void self::assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - 'select sum(`val`) from (select sum(-`amount`) `val` from `invoice` where `client_id` = :a UNION ALL select sum(`amount`) `val` from `payment` where `client_id` = :b) `_t_e7d707a26e7f`', + // QUERY IS WIP + 'select sum(`amount`) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, -`amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = :a', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); } From d2f626d588069c5779a79023440590474e281e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 3 Feb 2024 00:48:17 +0100 Subject: [PATCH 140/151] uncomment remaining test --- tests/ModelUnionTest.php | 45 +++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 7fd33b2c1..769114b38 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -6,6 +6,7 @@ use Atk4\Data\Model\AggregateModel; use Atk4\Data\Schema\TestCase; +use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\SQLServerPlatform; class ModelUnionTest extends TestCase @@ -237,10 +238,8 @@ public function testGrouping2(): void { $transaction = $this->createTransaction(); $transaction->removeField('client_id'); - if (!$this->getDatabasePlatform() instanceof SQLServerPlatform) { - // TODO where should be no ORDER BY in subquery - $transaction->setOrder('name'); - } + $transaction->setOrder('name'); + $transactionAggregate = new AggregateModel($transaction); $transactionAggregate->setGroupBy(['name'], [ 'amount' => ['expr' => 'sum([amount])', 'type' => 'atk4_money'], @@ -256,10 +255,8 @@ public function testGrouping2(): void $transaction = $this->createSubtractInvoiceTransaction(); $transaction->removeField('client_id'); - if (!$this->getDatabasePlatform() instanceof SQLServerPlatform) { - // TODO where should be no ORDER BY in subquery - $transaction->setOrder('name'); - } + $transaction->setOrder('name'); + $transactionAggregate = new AggregateModel($transaction); $transactionAggregate->setGroupBy(['name'], [ 'amount' => ['expr' => 'sum([])', 'type' => 'atk4_money'], @@ -296,10 +293,6 @@ public function testSubGroupingByExpressions(): void $transactionAggregate->action('select')->render()[0] ); - if ($this->getDatabasePlatform() instanceof SQLServerPlatform) { - self::markTestIncomplete('TODO MSSQL: Constant value column seem not supported (Invalid column name \'type\')'); - } - self::assertSameExportUnordered([ ['type' => 'invoice', 'amount' => 23.0], ['type' => 'payment', 'amount' => 14.0], @@ -351,29 +344,29 @@ public function testReference(): void ); } - /* public function testFieldAggregateUnion(): void + public function testFieldAggregateUnion(): void { $client = $this->createClient(); $client->hasMany('tr', ['model' => $this->createTransaction()]) - ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum']); + ->addField('balance', ['field' => 'amount', 'aggregate' => 'sum', 'type' => 'float']); - if ($this->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySQLPlatform - || $this->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform - || $this->getDatabasePlatform() instanceof SQLServerPlatform - || $this->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\OraclePlatform) { - // TODO failing on all DBs expect Sqlite, MySQL uses "semi-joins" for this type of query which does not support UNION - // and therefore it complains about `client`.`id` field, see: - // http://stackoverflow.com/questions/8326815/mysql-field-from-union-subselect#comment10267696_8326815 - self::assertTrue(true); + self::assertSameExportUnordered([ + ['id' => 1, 'name' => 'Vinny', 'surname' => null, 'order' => null, 'balance' => 29.0], + ['id' => 2, 'name' => 'Zoe', 'surname' => null, 'order' => null, 'balance' => 8.0], + ], $client->export()); - return; - } + self::assertSame(['id' => 1, 'name' => 'Vinny', 'surname' => null, 'order' => null, 'balance' => 29.0], $client->load(1)->get()); + self::assertSame(['id' => 2, 'name' => 'Zoe', 'surname' => null, 'order' => null, 'balance' => 8.0], $client->load(2)->get()); + self::assertNull($client->tryLoad(3)); $this->assertSameSql( - 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`val`), 0) from (select coalesce(sum(`amount`), 0) `val` from `invoice` UNION ALL select coalesce(sum(`amount`), 0) `val` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id` having `id` = :a', + // QUERY IS WIP + $this->getDatabasePlatform() instanceof SQLServerPlatform || $this->getDatabasePlatform() instanceof OraclePlatform + ? 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id`, `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) having `id` = :a' + : 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id` having `id` = :a', $client->load(1)->action('select')->render()[0] ); - } */ + } public function testConditionOnUnionField(): void { From c8646b8465e9b84cd6f07d37a26ae54c6adc742a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 3 Feb 2024 12:39:17 +0100 Subject: [PATCH 141/151] improve coverage --- src/Model/UnionModel.php | 80 ++++++++++++++++------------------------ tests/ModelUnionTest.php | 11 ++++++ 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 04af2d5e7..9790e20ce 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -4,7 +4,6 @@ namespace Atk4\Data\Model; -use Atk4\Core\Exception as CoreException; use Atk4\Data\Exception; use Atk4\Data\Field; use Atk4\Data\Field\SqlExpressionField; @@ -113,28 +112,23 @@ public function addCondition($field, $operator = null, $value = null, $forceNest // otherwise add condition in all nested models foreach ($this->union as [$nestedModel, $fieldMap]) { - try { - if (isset($fieldMap[$field])) { - // field is included in mapping - use mapping expression - $f = $fieldMap[$field] instanceof Persistence\Sql\Expression - ? $fieldMap[$field] - : $this->getFieldExpr($nestedModel, $field, $fieldMap[$field]); - } elseif (is_string($field) && $nestedModel->hasField($field)) { - // model has such field - use that field directly - $f = $nestedModel->getField($field); - } else { - // we don't know what to do, so let's do nothing - continue; - } + if (isset($fieldMap[$field])) { + // field is included in mapping - use mapping expression + $f = $fieldMap[$field] instanceof Persistence\Sql\Expression + ? $fieldMap[$field] + : $this->getFieldExpr($nestedModel, $field, $fieldMap[$field]); + } elseif (is_string($field) && $nestedModel->hasField($field)) { + // model has such field - use that field directly + $f = $nestedModel->getField($field); + } else { + // we don't know what to do, so let's do nothing + continue; + } - if ('func_num_args'() === 2) { - $nestedModel->addCondition($f, $operator); - } else { - $nestedModel->addCondition($f, $operator, $value); - } - } catch (CoreException $e) { - throw $e - ->addMoreInfo('nestedModel', $nestedModel); + if ('func_num_args'() === 2) { + $nestedModel->addCondition($f, $operator); + } else { + $nestedModel->addCondition($f, $operator, $value); } } @@ -176,11 +170,6 @@ public function action(string $mode, array $args = []) break; case 'field': - if (!isset($args[0])) { - throw (new Exception('This action requires one argument with field name')) - ->addMoreInfo('mode', $mode); - } - $subquery = $this->getSubQuery([$args[0]]); break; @@ -232,33 +221,28 @@ public function getSubQuery(array $fields): Persistence\Sql\Query // map fields for related model $queryFieldExpressions = []; foreach ($fields as $fieldName) { - try { - if (!$this->hasField($fieldName)) { - $queryFieldExpressions[$fieldName] = $nestedModel->expr('NULL'); + if (!$this->hasField($fieldName)) { + $queryFieldExpressions[$fieldName] = $nestedModel->expr('NULL'); - continue; - } + continue; + } - $field = $this->getField($fieldName); + $field = $this->getField($fieldName); - if ($field->hasJoin() || $field->neverPersist) { - continue; - } + if ($field->hasJoin() || $field->neverPersist) { + continue; + } - // UnionModel can have some fields defined as expressions. We don't touch those either. - // Imants: I have no idea why this condition was set, but it's limiting our ability - // to use expression fields in mapping - if ($field instanceof SqlExpressionField /* && !isset($this->aggregate[$fieldName]) */) { - continue; - } + // UnionModel can have some fields defined as expressions. We don't touch those either. + // Imants: I have no idea why this condition was set, but it's limiting our ability + // to use expression fields in mapping + if ($field instanceof SqlExpressionField /* && !isset($this->aggregate[$fieldName]) */) { + continue; + } - $fieldExpression = $this->getFieldExpr($nestedModel, $fieldName, $fieldMap[$fieldName] ?? null); + $fieldExpression = $this->getFieldExpr($nestedModel, $fieldName, $fieldMap[$fieldName] ?? null); - $queryFieldExpressions[$fieldName] = $fieldExpression; - } catch (CoreException $e) { - throw $e - ->addMoreInfo('nestedModel', $nestedModel); - } + $queryFieldExpressions[$fieldName] = $fieldExpression; } $query = $this->getPersistence()->action($nestedModel, 'select', [[]]); diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 769114b38..d64760015 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -4,7 +4,9 @@ namespace Atk4\Data\Tests; +use Atk4\Data\Exception; use Atk4\Data\Model\AggregateModel; +use Atk4\Data\Model\UnionInternalTable; use Atk4\Data\Schema\TestCase; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\SQLServerPlatform; @@ -436,4 +438,13 @@ public function testConditionOnMappedField(): void ['client_id' => 2, 'name' => 'full pay', 'amount' => 4.0], ], $transaction->export()); } + + public function testUnionInternalTableActionException(): void + { + $unionInternalTable = new UnionInternalTable(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Only "select" action with empty arguments is expected'); + $unionInternalTable->action('count'); + } } From c84cfc65686a7787a9a9a52c0c1a97e986df44c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 3 Feb 2024 13:54:38 +0100 Subject: [PATCH 142/151] prevent child models mutation --- src/Model/UnionModel.php | 48 ++-------------------------------------- tests/ModelUnionTest.php | 20 ----------------- 2 files changed, 2 insertions(+), 66 deletions(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 9790e20ce..22ff6f56a 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -19,9 +19,8 @@ * For example if you are asking sum(amount), there is no need to fetch any extra * fields from sub-models. * - * @property Persistence\Sql $persistence - * - * @method Persistence\Sql\Expression expr($expr, array $args = []) forwards to Persistence\Sql::expr using $this as model + * @method Persistence\Sql getPersistence() + * @method Persistence\Sql\Expression expr(string $template, array $arguments = []) forwards to Persistence\Sql::expr using $this as model */ class UnionModel extends Model { @@ -92,49 +91,6 @@ public function addNestedModel(Model $model, array $fieldMap = []): Model return $model; // TODO nothing/void should be returned } - /** - * If UnionModel model has such field, then add condition to it. - * Otherwise adds condition to all nested models. - * - * @param bool $forceNested Should we add condition to all nested models? - */ - #[\Override] - public function addCondition($field, $operator = null, $value = null, $forceNested = false) - { - if ('func_num_args'() === 1) { - return parent::addCondition($field); - } - - // if UnionModel has such field, then add condition to it - if ($this->hasField($field) && !$forceNested) { - return parent::addCondition(...'func_get_args'()); - } - - // otherwise add condition in all nested models - foreach ($this->union as [$nestedModel, $fieldMap]) { - if (isset($fieldMap[$field])) { - // field is included in mapping - use mapping expression - $f = $fieldMap[$field] instanceof Persistence\Sql\Expression - ? $fieldMap[$field] - : $this->getFieldExpr($nestedModel, $field, $fieldMap[$field]); - } elseif (is_string($field) && $nestedModel->hasField($field)) { - // model has such field - use that field directly - $f = $nestedModel->getField($field); - } else { - // we don't know what to do, so let's do nothing - continue; - } - - if ('func_num_args'() === 2) { - $nestedModel->addCondition($f, $operator); - } else { - $nestedModel->addCondition($f, $operator, $value); - } - } - - return $this; - } - /** * @return Persistence\Sql\Query */ diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index d64760015..ebd4f1ade 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -393,26 +393,6 @@ public function testConditionOnNestedModelField(): void ], $transaction->export()); } - public function testConditionForcedOnNestedModel1(): void - { - $transaction = $this->createSubtractInvoiceTransaction(); - $transaction->addCondition('amount', '>', 5, true); - - self::assertSameExportUnordered([ - ['client_id' => 1, 'name' => 'prepay', 'amount' => 10.0], - ], $transaction->export()); - } - - public function testConditionForcedOnNestedModel2(): void - { - $transaction = $this->createSubtractInvoiceTransaction(); - $transaction->addCondition('amount', '<', -10, true); - - self::assertSameExportUnordered([ - ['client_id' => 1, 'name' => 'table purchase', 'amount' => -15.0], - ], $transaction->export()); - } - public function testConditionExpression(): void { $transaction = $this->createSubtractInvoiceTransaction(); From a82a10843e8787ab182d7bcfab4000d50e8c45d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 21 Feb 2024 16:06:33 +0100 Subject: [PATCH 143/151] do no wrap inner from in extra select --- src/Model/UnionModel.php | 30 ++++++++++++++++-------------- tests/ModelUnionTest.php | 33 ++++++++++++++------------------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 22ff6f56a..939c5f1fd 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -96,7 +96,7 @@ public function addNestedModel(Model $model, array $fieldMap = []): Model */ public function actionSelectInnerTable() { - return $this->action('select'); + return $this->createSubQuery(null); } #[\Override] @@ -105,28 +105,21 @@ public function action(string $mode, array $args = []) $subquery = null; switch ($mode) { case 'select': - // get list of available fields - $fields = $this->onlyFields ?? array_keys($this->getFields()); - foreach ($fields as $k => $field) { - if ($this->getField($field)->neverPersist) { - unset($fields[$k]); - } - } - $subquery = $this->getSubQuery($fields); + $subquery = $this->createSubQuery(null); $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->tableAlias); $this->hook(self::HOOK_INIT_UNION_SELECT_QUERY, [$query]); return $query; case 'count': - $subquery = $this->getSubAction('count', ['alias' => 'cnt']); + $subquery = $this->createSubAction('count', ['alias' => 'cnt']); $mode = 'fx'; $args = ['sum', $this->expr('{}', ['cnt'])]; break; case 'field': - $subquery = $this->getSubQuery([$args[0]]); + $subquery = $this->createSubQuery([$args[0]]); break; case 'fx': @@ -134,7 +127,7 @@ public function action(string $mode, array $args = []) return parent::action($mode, $args); /* $args['alias'] = 'val'; - $subquery = $this->getSubAction($mode, $args); + $subquery = $this->createSubAction($mode, $args); $args = [$args[0], $this->expr('{}', ['val'])]; @@ -170,8 +163,17 @@ private function createUnionQuery(array $subqueries): Persistence\Sql\Query * * @param list $fields */ - public function getSubQuery(array $fields): Persistence\Sql\Query + public function createSubQuery(?array $fields): Persistence\Sql\Query { + if ($fields === null) { + $fields = $this->onlyFields ?? array_keys($this->getFields()); + foreach ($fields as $k => $field) { + if ($this->getField($field)->neverPersist) { + unset($fields[$k]); + } + } + } + $subqueries = []; foreach ($this->union as [$nestedModel, $fieldMap]) { // map fields for related model @@ -219,7 +221,7 @@ public function getSubQuery(array $fields): Persistence\Sql\Query /** * @param array $actionArgs */ - public function getSubAction(string $action, array $actionArgs = []): Persistence\Sql\Query + public function createSubAction(string $action, array $actionArgs = []): Persistence\Sql\Query { $subqueries = []; foreach ($this->union as [$model, $fieldMap]) { diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index ebd4f1ade..218b5ec02 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -65,30 +65,30 @@ public function testFieldExpr(): void $this->assertSameSql('-NULL', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'blah', '-[]')])->render()[0]); } - public function testNestedQuery1(): void + public function testCreateSubQueryBasic(): void { $transaction = $this->createTransaction(); $this->assertSameSql( 'select `name` `name` from `invoice` UNION ALL select `name` `name` from `payment`', - $transaction->getSubQuery(['name'])->render()[0] + $transaction->createSubQuery(['name'])->render()[0] ); $this->assertSameSql( 'select `name` `name`, `amount` `amount` from `invoice` UNION ALL select `name` `name`, `amount` `amount` from `payment`', - $transaction->getSubQuery(['name', 'amount'])->render()[0] + $transaction->createSubQuery(['name', 'amount'])->render()[0] ); $this->assertSameSql( 'select `name` `name` from `invoice` UNION ALL select `name` `name` from `payment`', - $transaction->getSubQuery(['name'])->render()[0] + $transaction->createSubQuery(['name'])->render()[0] ); } /** * If field is not set for one of the nested model, instead of generating exception, NULL will be filled in. */ - public function testMissingField(): void + public function testCreateSubQueryMissingField(): void { $transaction = $this->createTransaction(); $transaction->nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); @@ -96,7 +96,7 @@ public function testMissingField(): void $this->assertSameSql( 'select (\'invoice\') `type`, `amount` `amount` from `invoice` UNION ALL select NULL `type`, `amount` `amount` from `payment`', - $transaction->getSubQuery(['type', 'amount'])->render()[0] + $transaction->createSubQuery(['type', 'amount'])->render()[0] ); } @@ -120,16 +120,14 @@ public function testActions(): void ); $this->assertSameSql( - // QUERY IS WIP - 'select sum(`amount`) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_tu`', + 'select sum(`amount`) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`', $transaction->action('fx', ['sum', 'amount'])->render()[0] ); $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( - // QUERY IS WIP - 'select sum(`amount`) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, -`amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_tu`', + 'select sum(`amount`) from (select `client_id` `client_id`, `name` `name`, -`amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`', $transaction->action('fx', ['sum', 'amount'])->render()[0] ); } @@ -144,13 +142,13 @@ public function testActions2(): void self::assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); } - public function testSubAction1(): void + public function testCreateSubAction(): void { $transaction = $this->createSubtractInvoiceTransaction(); $this->assertSameSql( 'select sum(-`amount`) from `invoice` UNION ALL select sum(`amount`) from `payment`', - $transaction->getSubAction('fx', ['sum', 'amount'])->render()[0] + $transaction->createSubAction('fx', ['sum', 'amount'])->render()[0] ); } @@ -327,8 +325,7 @@ public function testReference(): void self::assertSame(29.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - // QUERY IS WIP - 'select sum(`amount`) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = :a', + 'select sum(`amount`) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = :a', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); @@ -340,8 +337,7 @@ public function testReference(): void self::assertSame(-9.0, (float) $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->getOne()); $this->assertSameSql( - // QUERY IS WIP - 'select sum(`amount`) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, -`amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = :a', + 'select sum(`amount`) from (select `client_id` `client_id`, `name` `name`, -`amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = :a', $client->load(1)->ref('tr')->action('fx', ['sum', 'amount'])->render()[0] ); } @@ -362,10 +358,9 @@ public function testFieldAggregateUnion(): void self::assertNull($client->tryLoad(3)); $this->assertSameSql( - // QUERY IS WIP $this->getDatabasePlatform() instanceof SQLServerPlatform || $this->getDatabasePlatform() instanceof OraclePlatform - ? 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id`, `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) having `id` = :a' - : 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id`, `name`, `amount` from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_tu`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id` having `id` = :a', + ? 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id`, `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) having `id` = :a' + : 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id` having `id` = :a', $client->load(1)->action('select')->render()[0] ); } From b4fbe57e413c3745280a16bedc36e18477630ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 21 Feb 2024 17:25:13 +0100 Subject: [PATCH 144/151] rm UnionModel::createSubAction() method --- src/Model/UnionModel.php | 46 ++++++++-------------------------------- tests/ModelUnionTest.php | 10 --------- 2 files changed, 9 insertions(+), 47 deletions(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 939c5f1fd..eca11a725 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -112,11 +112,18 @@ public function action(string $mode, array $args = []) return $query; case 'count': - $subquery = $this->createSubAction('count', ['alias' => 'cnt']); - $mode = 'fx'; $args = ['sum', $this->expr('{}', ['cnt'])]; + $subqueries = []; + foreach ($this->union as [$model]) { + $query = $model->action('count', ['alias' => 'cnt']); + $query->wrapInParentheses = false; + $subqueries[] = $query; + } + + $subquery = $this->createUnionQuery($subqueries); + break; case 'field': $subquery = $this->createSubQuery([$args[0]]); @@ -125,13 +132,6 @@ public function action(string $mode, array $args = []) case 'fx': case 'fx0': return parent::action($mode, $args); - /* $args['alias'] = 'val'; - - $subquery = $this->createSubAction($mode, $args); - - $args = [$args[0], $this->expr('{}', ['val'])]; - - break; */ default: throw (new Exception('UnionModel model does not support this action')) ->addMoreInfo('mode', $mode); @@ -218,34 +218,6 @@ public function createSubQuery(?array $fields): Persistence\Sql\Query return $unionQuery; } - /** - * @param array $actionArgs - */ - public function createSubAction(string $action, array $actionArgs = []): Persistence\Sql\Query - { - $subqueries = []; - foreach ($this->union as [$model, $fieldMap]) { - $modelActionArgs = $actionArgs; - $fieldName = $actionArgs[1] ?? null; - if ($fieldName) { - $modelActionArgs[1] = $this->getFieldExpr( - $model, - $fieldName, - $fieldMap[$fieldName] ?? null - ); - } - - $query = $model->action($action, $modelActionArgs); - $query->wrapInParentheses = false; - - $subqueries[] = $query; - } - - $unionQuery = $this->createUnionQuery($subqueries); - - return $unionQuery; - } - #[\Override] public function __debugInfo(): array { diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 218b5ec02..a11eb5133 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -142,16 +142,6 @@ public function testActions2(): void self::assertSame(-9.0, (float) $transaction->action('fx', ['sum', 'amount'])->getOne()); } - public function testCreateSubAction(): void - { - $transaction = $this->createSubtractInvoiceTransaction(); - - $this->assertSameSql( - 'select sum(-`amount`) from `invoice` UNION ALL select sum(`amount`) from `payment`', - $transaction->createSubAction('fx', ['sum', 'amount'])->render()[0] - ); - } - public function testBasics(): void { $client = $this->createClient(); From 1c8abad6e0045ecb64dbfbaf7e77d0b2a67e8d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 21 Feb 2024 17:27:20 +0100 Subject: [PATCH 145/151] improve subQuery case --- src/Model/UnionModel.php | 8 ++++---- tests/ModelUnionTest.php | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index eca11a725..52667fb97 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -96,7 +96,7 @@ public function addNestedModel(Model $model, array $fieldMap = []): Model */ public function actionSelectInnerTable() { - return $this->createSubQuery(null); + return $this->createSubquery(null); } #[\Override] @@ -105,7 +105,7 @@ public function action(string $mode, array $args = []) $subquery = null; switch ($mode) { case 'select': - $subquery = $this->createSubQuery(null); + $subquery = $this->createSubquery(null); $query = parent::action($mode, $args)->reset('table')->table($subquery, $this->tableAlias); $this->hook(self::HOOK_INIT_UNION_SELECT_QUERY, [$query]); @@ -126,7 +126,7 @@ public function action(string $mode, array $args = []) break; case 'field': - $subquery = $this->createSubQuery([$args[0]]); + $subquery = $this->createSubquery([$args[0]]); break; case 'fx': @@ -163,7 +163,7 @@ private function createUnionQuery(array $subqueries): Persistence\Sql\Query * * @param list $fields */ - public function createSubQuery(?array $fields): Persistence\Sql\Query + public function createSubquery(?array $fields): Persistence\Sql\Query { if ($fields === null) { $fields = $this->onlyFields ?? array_keys($this->getFields()); diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index a11eb5133..5998fc8e3 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -65,30 +65,30 @@ public function testFieldExpr(): void $this->assertSameSql('-NULL', $transaction->expr('[]', [$transaction->getFieldExpr($transaction->nestedInvoice, 'blah', '-[]')])->render()[0]); } - public function testCreateSubQueryBasic(): void + public function testCreateSubqueryBasic(): void { $transaction = $this->createTransaction(); $this->assertSameSql( 'select `name` `name` from `invoice` UNION ALL select `name` `name` from `payment`', - $transaction->createSubQuery(['name'])->render()[0] + $transaction->createSubquery(['name'])->render()[0] ); $this->assertSameSql( 'select `name` `name`, `amount` `amount` from `invoice` UNION ALL select `name` `name`, `amount` `amount` from `payment`', - $transaction->createSubQuery(['name', 'amount'])->render()[0] + $transaction->createSubquery(['name', 'amount'])->render()[0] ); $this->assertSameSql( 'select `name` `name` from `invoice` UNION ALL select `name` `name` from `payment`', - $transaction->createSubQuery(['name'])->render()[0] + $transaction->createSubquery(['name'])->render()[0] ); } /** * If field is not set for one of the nested model, instead of generating exception, NULL will be filled in. */ - public function testCreateSubQueryMissingField(): void + public function testCreateSubqueryMissingField(): void { $transaction = $this->createTransaction(); $transaction->nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); @@ -96,7 +96,7 @@ public function testCreateSubQueryMissingField(): void $this->assertSameSql( 'select (\'invoice\') `type`, `amount` `amount` from `invoice` UNION ALL select NULL `type`, `amount` `amount` from `payment`', - $transaction->createSubQuery(['type', 'amount'])->render()[0] + $transaction->createSubquery(['type', 'amount'])->render()[0] ); } From d17f009ee03db730977c56faf90a86e4be81f321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 16 Mar 2024 23:44:39 +0100 Subject: [PATCH 146/151] adjust for latest atk4/data --- tests/ModelUnionTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index 5998fc8e3..d14156810 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -348,10 +348,8 @@ public function testFieldAggregateUnion(): void self::assertNull($client->tryLoad(3)); $this->assertSameSql( - $this->getDatabasePlatform() instanceof SQLServerPlatform || $this->getDatabasePlatform() instanceof OraclePlatform - ? 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id`, `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) having `id` = :a' - : 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client` group by `id` having `id` = :a', - $client->load(1)->action('select')->render()[0] + 'select `id`, `name`, `surname`, `order`, (select coalesce(sum(`amount`), 0) from (select `client_id` `client_id`, `name` `name`, `amount` `amount` from `invoice` UNION ALL select `client_id` `client_id`, `name` `name`, `amount` `amount` from `payment`) `_t_e7d707a26e7f` where `client_id` = `client`.`id`) `balance` from `client`', + $client->action('select')->render()[0] ); } From 62affa7103a6f742c41fc649f229e9be04a45f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 17 Mar 2024 14:20:20 +0100 Subject: [PATCH 147/151] fix cs --- tests/ModelUnionTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/ModelUnionTest.php b/tests/ModelUnionTest.php index d14156810..486477348 100644 --- a/tests/ModelUnionTest.php +++ b/tests/ModelUnionTest.php @@ -8,8 +8,6 @@ use Atk4\Data\Model\AggregateModel; use Atk4\Data\Model\UnionInternalTable; use Atk4\Data\Schema\TestCase; -use Doctrine\DBAL\Platforms\OraclePlatform; -use Doctrine\DBAL\Platforms\SQLServerPlatform; class ModelUnionTest extends TestCase { From 33fb29d970685c320ccd852bdbcd426180f4c6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 17 Mar 2024 14:21:11 +0100 Subject: [PATCH 148/151] fix typo --- docs/unions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/unions.md b/docs/unions.md index f0a47f9e7..cf7139d13 100644 --- a/docs/unions.md +++ b/docs/unions.md @@ -36,7 +36,7 @@ results from that. As a result, Union model will have no "id" field. Below is an The Union model can be separated in a designated class and nested model added within the init() method body of the new class: ``` -$unionPaymentInvoice = new \Atk4\Data\Model\Union(); +$unionPaymentInvoice = new \Atk4\Data\Model\UnionModel(); $nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); $nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment()); From 0b857260cd153efd2ff5829f1707c7059136181b Mon Sep 17 00:00:00 2001 From: DarkSide Date: Mon, 18 Mar 2024 21:14:36 +0200 Subject: [PATCH 149/151] fix variable names in docs --- docs/unions.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/unions.md b/docs/unions.md index cf7139d13..b55517fe0 100644 --- a/docs/unions.md +++ b/docs/unions.md @@ -38,8 +38,8 @@ The Union model can be separated in a designated class and nested model added wi ``` $unionPaymentInvoice = new \Atk4\Data\Model\UnionModel(); -$nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); -$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment()); +$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Invoice()); +$nestedPayment = $unionPaymentInvoice->addNestedModel(new Payment()); ``` Next, assuming that both models have common fields "name" and "amount", `$unionPaymentInvoice` fields can be set: @@ -81,8 +81,8 @@ E.g. Invoice has field "description" and payment has field "note". When defining a nested model a field map array needs to be specified: ``` -$nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice()); -$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); +$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Invoice()); +$nestedPayment = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); $unionPaymentInvoice->addField('description'); ``` @@ -91,19 +91,19 @@ This format can also be used to reverse sign on amounts. When we are creating "T subtracted from the amount, while payments will be added: ``` -$nestedPayment = $mUnion->addNestedModel(new Invoice(), ['amount' => '-[amount]']); -$nestedInvoice = $mUnion->addNestedModel(new Payment(), ['description' => '[note]']); +$nestedInvoice = $mUnion->addNestedModel(new Invoice(), ['amount' => '-[amount]']); +$nestedPayment = $mUnion->addNestedModel(new Payment(), ['description' => '[note]']); $unionPaymentInvoice->addField('description'); ``` Should more flexibility be needed, more expressions (or fields) can be added directly to nested models: ``` -$nestedPayment = $unionPaymentInvoice->addNestedModel(new Invoice(), ['amount' => '-[amount]']); -$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); +$nestedInvoice = $unionPaymentInvoice->addNestedModel(new Invoice(), ['amount' => '-[amount]']); +$nestedPayment = $unionPaymentInvoice->addNestedModel(new Payment(), ['description' => '[note]']); -$nestedPayment->addExpression('type', ['expr' => '\'payment\'']); $nestedInvoice->addExpression('type', ['expr' => '\'invoice\'']); +$nestedPayment->addExpression('type', ['expr' => '\'payment\'']); $unionPaymentInvoice->addField('type'); ``` From f35f8103047ab54011cd362ad2a90422edc01850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 4 May 2024 13:24:03 +0200 Subject: [PATCH 150/151] fix cs --- src/Model/UnionModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 52667fb97..834c4bdcc 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -44,7 +44,7 @@ class UnionModel extends Model /** * @param array $defaults */ - public function __construct(Persistence $persistence = null, array $defaults = []) + public function __construct(?Persistence $persistence = null, array $defaults = []) { $unionTable = new UnionInternalTable(); $unionTable->setOwner($this); @@ -61,7 +61,7 @@ public function __construct(Persistence $persistence = null, array $defaults = [ * * @return Field|Persistence\Sql\Expression */ - public function getFieldExpr(Model $model, string $fieldName, string $expr = null) + public function getFieldExpr(Model $model, string $fieldName, ?string $expr = null) { if ($model->hasField($fieldName)) { $field = $model->getField($fieldName); From 5676f2f75c0e74a6b66b3f82d984ef33b87d97b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 8 Jun 2024 13:11:30 +0200 Subject: [PATCH 151/151] improve phpstan ignores --- src/Model/UnionInternalTable.php | 2 +- src/Model/UnionModel.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/UnionInternalTable.php b/src/Model/UnionInternalTable.php index 418c9593a..8391477be 100644 --- a/src/Model/UnionInternalTable.php +++ b/src/Model/UnionInternalTable.php @@ -38,7 +38,7 @@ public function action(string $mode, array $args = []) $tableOrig = $model->table; $model->table = '_tu'; try { - return $model->actionSelectInnerTable(); // @phpstan-ignore-line + return $model->actionSelectInnerTable(); // @phpstan-ignore method.notFound } finally { $model->table = $tableOrig; } diff --git a/src/Model/UnionModel.php b/src/Model/UnionModel.php index 834c4bdcc..ad1748137 100644 --- a/src/Model/UnionModel.php +++ b/src/Model/UnionModel.php @@ -48,7 +48,7 @@ public function __construct(?Persistence $persistence = null, array $defaults = { $unionTable = new UnionInternalTable(); $unionTable->setOwner($this); - $this->table = $unionTable; // @phpstan-ignore-line + $this->table = $unionTable; // @phpstan-ignore assign.propertyType $this->tableAlias ??= '_tu'; // DEBUG