Compare commits

...

8 Commits

Author SHA1 Message Date
Grégoire Paris
a64f315dfe Merge pull request #10642 from yobrx/patch-1 2023-04-20 11:46:32 +02:00
Yoann B
6ca319a6f4 fix array association on partial index 2023-04-20 10:59:39 +02:00
Grégoire Paris
fceb279947 Merge pull request #10630 from monadial/fix/fqcn-type-in-xml-mapping
Fixed xsd schema for support FQCN type
2023-04-16 10:26:37 +02:00
Alexander M. Turek
2977933119 Run tests on SQLite with foreign keys enabled (#10632) 2023-04-15 10:54:31 +02:00
Tomas Mihalicka
5ac6fadf29 Fixed xsd schema for support FQCN type
After update to orm 2.14.2 invalid xsd schema error is occured, when in field,id or attribute-override have type is FQCN
2023-04-14 18:16:35 +02:00
Grégoire Paris
e59ed88251 Merge pull request #10620 from ecourtial/fix-doc-typo
fix typo in HydrationCompleteHandler doc
2023-04-12 21:21:42 +02:00
Eric COURTIAL
a16aeaeac8 fix typo in HydrationCompleteHandler doc 2023-04-12 21:02:36 +02:00
Mathieu
fca1ef78a7 Handle null comparisons in ManyToManyPersister (#10587)
* Add test case for https://github.com/doctrine/orm/issues/7717

* Do not hide null equality checks in `SqlValueVisitor::walkComparison`

* Annotate `GH7717Parent::$children` type
2023-04-12 17:31:38 +02:00
17 changed files with 277 additions and 27 deletions

View File

@@ -575,7 +575,7 @@ Example with partial indexes:
#[Index(name: "search_idx", columns: ["category"],
options: [
"where": "((category IS NOT NULL))"
"where" => "((category IS NOT NULL))"
]
)]
class ECommerceProduct

View File

@@ -302,7 +302,7 @@
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="xs:NMTOKEN" default="string" />
<xs:attribute name="type" type="orm:type" default="string" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="unique" type="xs:boolean" default="false" />
@@ -414,7 +414,7 @@
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="xs:NMTOKEN" />
<xs:attribute name="type" type="orm:type" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="association-key" type="xs:boolean" default="false" />
@@ -446,6 +446,13 @@
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="type" id="type">
<xs:restriction base="xs:token">
<xs:pattern value="([a-zA-Z_u01-uff][a-zA-Z0-9_u01-uff]+)|(\c+)" id="type.class.pattern">
</xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="inverse-join-columns">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="join-column" type="orm:join-column" minOccurs="1" maxOccurs="unbounded" />
@@ -630,7 +637,7 @@
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="type" type="xs:NMTOKEN" default="string" />
<xs:attribute name="type" type="orm:type" default="string" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="unique" type="xs:boolean" default="false" />

View File

@@ -51,7 +51,7 @@ final class HydrationCompleteHandler
}
/**
* This method should me called after any hydration cycle completed.
* This method should be called after any hydration cycle completed.
*
* Method fires all deferred invocations of postLoad events
*/

View File

@@ -6,6 +6,7 @@ namespace Doctrine\ORM\Persisters\Collection;
use BadMethodCallException;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
@@ -246,10 +247,15 @@ class ManyToManyPersister extends AbstractCollectionPersister
foreach ($parameters as $parameter) {
[$name, $value, $operator] = $parameter;
$field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform);
$whereClauses[] = sprintf('te.%s %s ?', $field, $operator);
$params[] = $value;
$paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0];
$field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform);
if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
$whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT');
} else {
$whereClauses[] = sprintf('te.%s %s ?', $field, $operator);
$params[] = $value;
$paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0];
}
}
$tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);

View File

@@ -890,15 +890,17 @@ class BasicEntityPersister implements EntityPersister
$valueVisitor->dispatch($expression);
[$params, $types] = $valueVisitor->getParamsAndTypes();
foreach ($params as $param) {
$sqlParams = array_merge($sqlParams, $this->getValues($param));
}
[, $types] = $valueVisitor->getParamsAndTypes();
foreach ($types as $type) {
[$field, $value] = $type;
$sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
[$field, $value, $operator] = $type;
if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
continue;
}
$sqlParams = array_merge($sqlParams, $this->getValues($value));
$sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
}
return [$sqlParams, $sqlTypes];

View File

@@ -27,18 +27,10 @@ class SqlValueVisitor extends ExpressionVisitor
*/
public function walkComparison(Comparison $comparison)
{
$value = $this->getValueFromComparison($comparison);
$field = $comparison->getField();
$operator = $comparison->getOperator();
if (($operator === Comparison::EQ || $operator === Comparison::IS) && $value === null) {
return null;
} elseif ($operator === Comparison::NEQ && $value === null) {
return null;
}
$value = $this->getValueFromComparison($comparison);
$this->values[] = $value;
$this->types[] = [$field, $value, $operator];
$this->types[] = [$comparison->getField(), $value, $comparison->getOperator()];
return null;
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH7717;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="gh7717_children")
*/
class GH7717Child
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
public ?int $id = null;
/**
* @ORM\Column(type="string", nullable=true)
*/
public ?string $nullableProperty = null;
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\GH7717;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="gh7717_parents")
*/
class GH7717Parent
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
public ?int $id = null;
/**
* @ORM\ManyToMany(targetEntity="GH7717Child", cascade={"persist"})
*
* @var Selectable<int, GH7717Child>
*/
public Selectable $children;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Project;
class Project
{
/**
* @var string
*/
private $id;
/**
* @var string
*/
private $name;
public function __construct(string $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Project;
class ProjectId
{
/**
* @var string
*/
private $id;
public function __construct(string $id)
{
$this->id = $id;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Project;
class ProjectInvalidMapping
{
/**
* @var string
*/
private $id;
/**
* @var string
*/
private $name;
public function __construct(string $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Project;
final class ProjectName
{
/**
* @var string
*/
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Tests\Models\GH7717\GH7717Child;
use Doctrine\Tests\Models\GH7717\GH7717Parent;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @requires PHP 7.4
*/
final class GH7717Test extends OrmFunctionalTestCase
{
public function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
GH7717Parent::class,
GH7717Child::class
);
}
public function testManyToManyPersisterIsNullComparison(): void
{
$childWithNullProperty = new GH7717Child();
$childWithoutNullProperty = new GH7717Child();
$childWithoutNullProperty->nullableProperty = 'nope';
$parent = new GH7717Parent();
$parent->children = new ArrayCollection([$childWithNullProperty, $childWithoutNullProperty]);
$this->_em->persist($parent);
$this->_em->flush();
$this->_em->clear();
$parent = $this->_em->find(GH7717Parent::class, 1);
$this->assertCount(1, $parent->children->matching(new Criteria(Criteria::expr()->isNull('nullableProperty'))));
}
}

View File

@@ -22,6 +22,10 @@ use Doctrine\Tests\Models\DDC889\DDC889SuperClass;
use Doctrine\Tests\Models\Generic\BooleanModel;
use Doctrine\Tests\Models\GH7141\GH7141Article;
use Doctrine\Tests\Models\GH7316\GH7316Article;
use Doctrine\Tests\Models\Project\Project;
use Doctrine\Tests\Models\Project\ProjectId;
use Doctrine\Tests\Models\Project\ProjectInvalidMapping;
use Doctrine\Tests\Models\Project\ProjectName;
use Doctrine\Tests\Models\ValueObjects\Name;
use Doctrine\Tests\Models\ValueObjects\Person;
@@ -239,6 +243,10 @@ class XmlMappingDriverTest extends MappingDriverTestCase
UserMissingAttributes::class,
['The attribute \'name\' is required but missing' => 1],
],
[
ProjectInvalidMapping::class,
['attribute \'type\': [facet \'pattern\'] The value' => 2],
],
];
}
@@ -279,6 +287,23 @@ class XmlMappingDriverTest extends MappingDriverTestCase
$this->createClassMetadata(DDC889Class::class);
}
public function testClassNameInFieldOrId(): void
{
$class = new ClassMetadata(Project::class);
$class->initializeReflection(new RuntimeReflectionService());
$driver = $this->loadDriver();
$driver->loadMetadataForClass(Project::class, $class);
/** @var array{type: string} $id */
$id = $class->getFieldMapping('id');
/** @var array{type: string} $name */
$name = $class->getFieldMapping('name');
self::assertEquals(ProjectId::class, $id['type']);
self::assertEquals(ProjectName::class, $name['type']);
}
}
class CTI

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\Models\Project\Project" table="project">
<id name="id" type="Doctrine\Tests\Models\Project\ProjectId" column="id">
<generator strategy="NONE"/>
</id>
<field name="name" type="Doctrine\Tests\Models\Project\ProjectName" column="name"/>
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\Models\Project\ProjectInvalidMapping" table="project">
<id name="id" type="Doctrine/Tests/Models/Project/Project/ProjectId" column="id">
<generator strategy="NONE"/>
</id>
<field name="name" type="Doctrine/Tests/Models/Project/Project/ProjectName" column="name"/>
</entity>
</doctrine-mapping>

View File

@@ -6,6 +6,7 @@ namespace Doctrine\Tests;
use Doctrine\Common\EventSubscriber;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\AbstractSQLiteDriver\Middleware\EnableForeignKeys;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Platforms\SqlitePlatform;
@@ -16,10 +17,12 @@ use Symfony\Component\VarExporter\LazyGhostTrait;
use UnexpectedValueException;
use function assert;
use function class_exists;
use function explode;
use function fwrite;
use function get_debug_type;
use function getenv;
use function in_array;
use function method_exists;
use function sprintf;
use function str_starts_with;
@@ -63,7 +66,13 @@ class TestUtil
self::$initialized = true;
}
$connection = DriverManager::getConnection(self::getTestConnectionParameters());
$connectionParameters = self::getTestConnectionParameters();
$configuration = new Configuration();
if (in_array($connectionParameters['driver'], ['pdo_sqlite', 'sqlite3'], true) && class_exists(EnableForeignKeys::class)) {
$configuration->setMiddlewares([new EnableForeignKeys()]);
}
$connection = DriverManager::getConnection($connectionParameters, $configuration);
assert($connection instanceof DbalExtensions\Connection);
self::addDbEventSubscribers($connection);
@@ -212,6 +221,7 @@ class TestUtil
'port',
'server',
'memory',
'path',
'ssl_key',
'ssl_cert',
'ssl_ca',