From cc6048d4ee5acedfe0132b7fba2371a2cf1d2f05 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Thu, 15 May 2025 18:05:23 +0000 Subject: [PATCH] - WIP on migration and some bug fixes linked to withJoin() select problems --- src/Adapter/DefaultAdapterTrait.php | 2 +- src/Adapter/MsSQL.php | 17 +++++-- src/Adapter/MsSQLFieldMapper.php | 29 +++++++++++ src/Adapter/MySQL.php | 3 +- src/Adapter/MySQLFieldMapper.php | 4 -- src/Adapter/SQLite.php | 4 +- src/Adapter/SqlAdapterTrait.php | 38 --------------- src/Adapter/SqlFieldMapper.php | 4 +- src/Attribute/Attribute.php | 6 +++ src/Attribute/Property/Virtual.php | 1 + src/Common/EntityResolver.php | 7 ++- src/Entity/DatasetHandler.php | 5 +- src/Entity/InformationSchema/Column.php | 17 ------- src/Entity/Mysql/Column.php | 21 ++++++++ src/Migration/Sql/SqliteMigration.php | 2 +- src/Migration/SqlMigrationTrait.php | 57 ++++++++++++++++++++++ src/Query/Select.php | 2 + src/Repository.php | 64 +++++++++++++++++-------- src/Repository/RelationBuilder.php | 4 +- src/Repository/WithOptionEnum.php | 2 +- 20 files changed, 196 insertions(+), 93 deletions(-) create mode 100644 src/Adapter/MsSQLFieldMapper.php create mode 100644 src/Migration/SqlMigrationTrait.php diff --git a/src/Adapter/DefaultAdapterTrait.php b/src/Adapter/DefaultAdapterTrait.php index 4d0d90f..68b1cde 100644 --- a/src/Adapter/DefaultAdapterTrait.php +++ b/src/Adapter/DefaultAdapterTrait.php @@ -4,7 +4,7 @@ namespace Ulmus\Adapter; use Ulmus\{ConnectionAdapter, Entity\InformationSchema\Table, Migration\FieldDefinition, Repository, QueryBuilder\Sql\MysqlQueryBuilder, Entity}; -trait DefaultAdapterTrait +UNUSED? trait DefaultAdapterTrait { public function repositoryClass() : string { diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index 9bceb75..fc4b505 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -9,10 +9,14 @@ use Ulmus\Exception\AdapterConfigurationException; use Ulmus\Ulmus; use Ulmus\Migration\FieldDefinition; -use Ulmus\{Entity\InformationSchema\Table, Migration\MigrateInterface, Repository, QueryBuilder}; +use Ulmus\{Entity\InformationSchema\Table, + Migration\MigrateInterface, + Migration\SqlMigrationTrait, + Repository, + QueryBuilder}; class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { - use SqlAdapterTrait; + use SqlAdapterTrait, SqlMigrationTrait; const ALLOWED_ATTRIBUTES = [ 'default', 'primary_key', 'auto_increment', @@ -205,7 +209,14 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { $this->traceOn = $configuration['trace_on']; } } - + + public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string + { + $mapper = new MsSQLFieldMapper($field); + + return $typeOnly ? $mapper->type : $mapper->render(); + } + public static function escapeIdentifier(string $segment, int $type) : string { switch($type) { diff --git a/src/Adapter/MsSQLFieldMapper.php b/src/Adapter/MsSQLFieldMapper.php new file mode 100644 index 0000000..f79c433 --- /dev/null +++ b/src/Adapter/MsSQLFieldMapper.php @@ -0,0 +1,29 @@ +type, [ 'CHAR', 'VARCHAR', 'TEXT', ])) { + $this->type = "N" . $this->type; + }; + } + + /* @TODO ! + public function postProcess() : void + { + if ( + in_array($this->type, [ 'BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB', 'JSON', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'GEOMETRY' ]) && + ! is_object($this->field->default ?? false) # Could be a functional default, which would now be valid + ) { + unset($this->field->default); + } + } + * */ +} \ No newline at end of file diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index c543ac2..cabd2c9 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -6,6 +6,7 @@ use Ulmus\ConnectionAdapter; use Ulmus\Entity\Mysql\Table; use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\MigrateInterface; +use Ulmus\Migration\SqlMigrationTrait; use Ulmus\QueryBuilder\Sql; use Ulmus\Common\PdoObject; @@ -13,7 +14,7 @@ use Ulmus\Exception\AdapterConfigurationException; use Ulmus\Repository; class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { - use SqlAdapterTrait; + use SqlAdapterTrait, SqlMigrationTrait; const ALLOWED_ATTRIBUTES = [ 'default', 'primary_key', 'auto_increment', 'update', diff --git a/src/Adapter/MySQLFieldMapper.php b/src/Adapter/MySQLFieldMapper.php index fb9e867..8512c0b 100644 --- a/src/Adapter/MySQLFieldMapper.php +++ b/src/Adapter/MySQLFieldMapper.php @@ -6,10 +6,6 @@ use Ulmus\Migration\FieldDefinition; class MySQLFieldMapper extends SqlFieldMapper { - public readonly string $type; - - public readonly string $length; - public function map() : void { $type = $this->field->type; diff --git a/src/Adapter/SQLite.php b/src/Adapter/SQLite.php index b8ce40f..6ccff2e 100644 --- a/src/Adapter/SQLite.php +++ b/src/Adapter/SQLite.php @@ -7,10 +7,10 @@ use Ulmus\Common\PdoObject; use Ulmus\ConnectionAdapter; use Ulmus\Entity; use Ulmus\Migration\FieldDefinition; -use Ulmus\{Migration\MigrateInterface, Repository, QueryBuilder}; +use Ulmus\{Migration\MigrateInterface, Migration\SqlMigrationTrait, Repository, QueryBuilder}; class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface { - use SqlAdapterTrait; + use SqlAdapterTrait, SqlMigrationTrait; const ALLOWED_ATTRIBUTES = [ 'default', 'primary_key', 'auto_increment', 'collate nocase', 'collate binary', 'collate rtrim', diff --git a/src/Adapter/SqlAdapterTrait.php b/src/Adapter/SqlAdapterTrait.php index 2e8b896..e3184c6 100644 --- a/src/Adapter/SqlAdapterTrait.php +++ b/src/Adapter/SqlAdapterTrait.php @@ -36,44 +36,11 @@ trait SqlAdapterTrait return $this->database; } - public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object - { - return Table::repository(Repository::DEFAULT_ALIAS, $adapter) - ->select(\Ulmus\Common\Sql::raw('this.*')) - ->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName) - ->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName); - } - - public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string - { - $mapper = new SqlFieldMapper($field); - - return $typeOnly ? $mapper->type : $mapper->render(); - } - public function whitelistAttributes(array &$parameters) : void { $parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES)); } - public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable - { - if (! empty($field['previous']) ) { - $position = sprintf('AFTER %s', $this->escapeIdentifier($field['previous']->field, AdapterInterface::IDENTIFIER_FIELD)); - } - else { - $position = "FIRST"; - } - - return implode(" ", [ - strtoupper($field['action']), - $this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD), - $definition->getSqlType(), - $definition->getSqlParams(), - $position, - ]); - } - public function writableValue(mixed $value) : mixed { switch (true) { @@ -92,9 +59,4 @@ trait SqlAdapterTrait return $value; } - - public function splitAlterQuery() : bool - { - return false; - } } diff --git a/src/Adapter/SqlFieldMapper.php b/src/Adapter/SqlFieldMapper.php index 39d0a3d..b1e45cd 100644 --- a/src/Adapter/SqlFieldMapper.php +++ b/src/Adapter/SqlFieldMapper.php @@ -7,9 +7,9 @@ use Ulmus\Entity; class SqlFieldMapper { - public readonly string $type; + public string $type; - public readonly string $length; + public string $length; public function __construct( public FieldDefinition $field, diff --git a/src/Attribute/Attribute.php b/src/Attribute/Attribute.php index 7072de4..5a77994 100644 --- a/src/Attribute/Attribute.php +++ b/src/Attribute/Attribute.php @@ -10,6 +10,12 @@ class Attribute public static function handleArrayField(null|\Stringable|string|array $field, null|string|bool $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed { if ( is_array($field) ) { + if (count($field) < 2) { + throw new \RuntimeException( + sprintf("Array field must be formed of at least two things, a class name, and a property. (received %s)", json_encode($field)) + ); + } + $class = array_shift($field); $field[1] ??= $alias; diff --git a/src/Attribute/Property/Virtual.php b/src/Attribute/Property/Virtual.php index 297280b..f5d5d34 100644 --- a/src/Attribute/Property/Virtual.php +++ b/src/Attribute/Property/Virtual.php @@ -10,6 +10,7 @@ class Virtual extends Field implements ResettablePropertyInterface { public function __construct( public null|string|array $method = null, public ? \Closure $closure = null, + public null|string $name = null, ) { $this->method ??= [ static::class, 'noop' ]; } diff --git a/src/Common/EntityResolver.php b/src/Common/EntityResolver.php index 2decb16..51aa73d 100644 --- a/src/Common/EntityResolver.php +++ b/src/Common/EntityResolver.php @@ -114,7 +114,12 @@ class EntityResolver { throw new \InvalidArgumentException("Can't find entity relation's column named `$name` from entity {$this->entityClass}"); } } - + + public function getPropertyEntityType(string $name) : false|string + { + return $this->reflectedClass->getProperties(true)[$name]->getTypes()[0]->type ?? false; + } + public function searchFieldAnnotation(string $field, array|object|string $annotationType, bool $caseSensitive = true) : ? object { return $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive)[0] ?? null; diff --git a/src/Entity/DatasetHandler.php b/src/Entity/DatasetHandler.php index f51f9c2..1cde76a 100644 --- a/src/Entity/DatasetHandler.php +++ b/src/Entity/DatasetHandler.php @@ -56,7 +56,10 @@ class DatasetHandler } elseif ( $field->expectType('array') ) { if ( is_string($value)) { - if (substr($value, 0, 1) === "a") { + if (empty($value)) { + yield $field->name => []; + } + elseif (substr($value, 0, 1) === "a") { yield $field->name => unserialize($value); } diff --git a/src/Entity/InformationSchema/Column.php b/src/Entity/InformationSchema/Column.php index 734c8d4..fc427b4 100644 --- a/src/Entity/InformationSchema/Column.php +++ b/src/Entity/InformationSchema/Column.php @@ -13,9 +13,6 @@ class Column { use \Ulmus\EntityTrait; - #[Field\Id] - public ? id $srs_id; - #[Field(name: "TABLE_CATALOG", length: 512)] public string $tableCatalog; @@ -64,24 +61,10 @@ class Column # #[Field(name: "COLLATION_TYPE", type: "longtext")] # public string $type; - #[Field(name: "COLUMN_KEY", length: 3)] - public string $key; - - #[Field(name: "EXTRA", length: 30)] - public string $extra; - - #[Field(name: "PRIVILEGES", length: 80)] - public string $privileges; - - #[Field(name: "COLUMN_COMMENT", length: 1024)] - public string $comment; # #[Field(name: "IS_GENERATED", length: 6)] # public string $generated; - #[Field(name: "GENERATION_EXPRESSION", type: "longtext")] - public ? string $generationExpression; - public function matchFieldDefinition(ReflectedProperty $definition) : bool { $nullable = $this->nullable === 'YES'; diff --git a/src/Entity/Mysql/Column.php b/src/Entity/Mysql/Column.php index 23db000..ca9d072 100644 --- a/src/Entity/Mysql/Column.php +++ b/src/Entity/Mysql/Column.php @@ -2,8 +2,28 @@ namespace Ulmus\Entity\Mysql; +use Ulmus\Attribute\Property\Field; + class Column extends \Ulmus\Entity\InformationSchema\Column { + #[Field\Id] + public ? id $srs_id; + + #[Field(name: "COLUMN_KEY", length: 3)] + public string $key; + + #[Field(name: "EXTRA", length: 30)] + public string $extra; + + #[Field(name: "PRIVILEGES", length: 80)] + public string $privileges; + + #[Field(name: "COLUMN_COMMENT", length: 1024)] + public string $comment; + + #[Field(name: "GENERATION_EXPRESSION", type: "longtext")] + public ? string $generationExpression; + # TODO ! Handle FUNCTIONAL default value protected function canHaveDefaultValue(): bool { @@ -12,4 +32,5 @@ class Column extends \Ulmus\Entity\InformationSchema\Column ]); } + } \ No newline at end of file diff --git a/src/Migration/Sql/SqliteMigration.php b/src/Migration/Sql/SqliteMigration.php index f8c0abc..34366d3 100644 --- a/src/Migration/Sql/SqliteMigration.php +++ b/src/Migration/Sql/SqliteMigration.php @@ -5,4 +5,4 @@ namespace Ulmus\Migration\Sql; class SqliteMigration { -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Migration/SqlMigrationTrait.php b/src/Migration/SqlMigrationTrait.php new file mode 100644 index 0000000..23db204 --- /dev/null +++ b/src/Migration/SqlMigrationTrait.php @@ -0,0 +1,57 @@ + "alter", + ]; + + public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object + { + return \Ulmus\Entity\InformationSchema\Table::repository(Repository::DEFAULT_ALIAS, $adapter) + ->select(\Ulmus\Common\Sql::raw('this.*')) + ->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName) + ->or($this->escapeIdentifier('table_catalog', AdapterInterface::IDENTIFIER_FIELD), $databaseName) + ->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName); + } + + public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string + { + $mapper = new SqlFieldMapper($field); + + return $typeOnly ? $mapper->type : $mapper->render(); + } + + public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable + { + if ($field['action'] === 'add') { + if (! empty($field['previous'])) { + $position = sprintf('AFTER %s', $this->escapeIdentifier($field['previous']->field, AdapterInterface::IDENTIFIER_FIELD)); + } + else { + $position = "FIRST"; + } + } + + return implode(" ", array_filter([ + strtoupper($field['action']), + $this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD), + $definition->getSqlType(), + $definition->getSqlParams(), + $position ?? null, + ])); + } + + public function splitAlterQuery() : bool + { + return false; + } +} \ No newline at end of file diff --git a/src/Query/Select.php b/src/Query/Select.php index a4efe9f..7b34fe7 100644 --- a/src/Query/Select.php +++ b/src/Query/Select.php @@ -12,6 +12,8 @@ class Select extends Fragment { public bool $union = false; + public bool $isInternalSelect = false; + public ? int $top = null; protected array $fields = []; diff --git a/src/Repository.php b/src/Repository.php index 8355046..d5c3a72 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -416,7 +416,8 @@ class Repository implements RepositoryInterface public function withJoin(string|array $fields, array $options = []) : self { - $canSelect = null === $this->queryBuilder->getFragment(Query\Select::class); + $selectObj = $this->queryBuilder->getFragment(Query\Select::class); + $canSelect = empty($selectObj) || $selectObj->isInternalSelect === true; if ( $canSelect ) { $select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true); @@ -424,18 +425,13 @@ class Repository implements RepositoryInterface } # @TODO Apply FILTER annotation to this too ! - foreach(array_filter((array) $fields) as $item) { - if ( isset($this->joined[$item]) ) { - continue; - } - else { - $this->joined[$item] = true; - } + foreach(array_filter((array) $fields, fn($e) => ! isset($this->joined[$e])) as $item) { + $this->joined[$item] = true; $attribute = $this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?: $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]); - $isRelation = ( $attribute instanceof Relation ) || ($attribute instanceof Relation); + $isRelation = $attribute instanceof Relation; if ($isRelation && ( $attribute->isManyToMany() )) { throw new \Exception("Many-to-many relation can not be preloaded within joins."); @@ -444,7 +440,7 @@ class Repository implements RepositoryInterface if ( $attribute ) { $alias = $attribute->alias ?? $item; - $entity = $attribute->entity ?? $this->entityResolver->reflectedClass->getProperties(true)[$item]->getTypes()[0]->type; + $entity = $attribute->entity ?? $this->entityResolver->getPropertyEntityType($item); foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) { @@ -462,12 +458,12 @@ class Repository implements RepositoryInterface $this->open(); if ( ! in_array(WithOptionEnum::SkipWhere, $options)) { - foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) { - if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) { - $this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) { + if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) { + $this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); + } } } - } if ( ! in_array(WithOptionEnum::SkipHaving, $options)) { foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) { @@ -485,9 +481,9 @@ class Repository implements RepositoryInterface $key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key; - $foreignKey = is_string($attribute->foreignKey) ? $entity::field($attribute->foreignKey, $alias) : $attribute->foreignKey; + $foreignKey = $this->evalClosure($attribute->foreignKey, $entity, $alias); - $this->join("LEFT", $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $options) { + $this->join($isRelation ? "LEFT" : $attribute->type, $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $options) { if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) { foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) { if ( ! is_object($condition->field) ) { @@ -518,6 +514,12 @@ class Repository implements RepositoryInterface } } + if ($canSelect) { + if ( $selectObj ??= $this->queryBuilder->getFragment(Query\Select::class) ) { + $selectObj->isInternalSelect = true; + } + } + return $this; } @@ -549,7 +551,7 @@ class Repository implements RepositoryInterface ->selectJsonEntity($relationRelation->entity, $alias)->open(); } else { - $entity = $relation->entity ?? $this->entityResolver->properties[$item]['type']; + $entity = $relation->entity ?? $this->entityResolver->getPropertyEntityType($name); $repository = $entity::repository()->selectJsonEntity($entity, $alias)->open(); } @@ -590,7 +592,7 @@ class Repository implements RepositoryInterface $order = $this->entityResolver->searchFieldAnnotationList($name, [ OrderBy::class ]); $where = $this->entityResolver->searchFieldAnnotationList($name, [ Where::class ]); - $baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type']; + $baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->getPropertyEntityType($name); $baseEntityResolver = $baseEntity::resolveEntity(); $property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name']; @@ -644,6 +646,22 @@ class Repository implements RepositoryInterface } } + ## AWAITING THE Closures in constant expression from PHP 8.5 ! + protected function evalClosure(string $content, string $entityClass, mixed $alias = self::DEFAULT_ALIAS) : mixed + { + if (is_string($content)) { + if ( str_starts_with($content, 'fn(') ) { + $closure = eval("return $content;"); + + return $closure($this); + } + + return $entityClass::field($content, $alias); + } + + return $content; + } + public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self { if ($count) { @@ -748,7 +766,15 @@ class Repository implements RepositoryInterface public function instanciateEntity(? string $entityClass = null) : object { - $entity = ( new \ReflectionClass($entityClass ?? $this->entityClass) )->newInstanceWithoutConstructor(); + $entityClass ??= $this->entityClass; + + try { + $entity = new $entityClass(); + } + catch (\Throwable $ex) { + $entity = ( new \ReflectionClass($entityClass) )->newInstanceWithoutConstructor(); + } + $entity->initializeEntity(); return $entity; diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php index 48f6f98..1463920 100644 --- a/src/Repository/RelationBuilder.php +++ b/src/Repository/RelationBuilder.php @@ -57,7 +57,7 @@ class RelationBuilder } else { - if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) { + if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class, Join::class ] ) ) { return $this->instanciateEmptyObject($name, $relation); } elseif ($virtual = $this->resolveVirtual($name)) { @@ -91,7 +91,7 @@ class RelationBuilder protected function resolveRelation(string $name) : mixed { - if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) ) { + if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class, /*Join::class*/ ] ) ) ) { $this->orders = $this->resolver->searchFieldAnnotationList($name, [ OrderBy::class ] ); $this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Where::class ] ); $this->filters = $this->resolver->searchFieldAnnotationList($name, [ Filter::class ] ); diff --git a/src/Repository/WithOptionEnum.php b/src/Repository/WithOptionEnum.php index 83a75bc..da3222f 100644 --- a/src/Repository/WithOptionEnum.php +++ b/src/Repository/WithOptionEnum.php @@ -5,7 +5,7 @@ namespace Ulmus\Repository; enum WithOptionEnum { case SkipWhere; - case SkipHaving; + case SkipHaving; case SkipFilter; case SkipOrderBy; case SkipJoinWhere;