first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
{
"sections": [
{
"id": 1,
"name": "Has children",
"lead_text": "text"
},
{
"id": 2,
"name": "Child",
"lead_text": "text"
},
{
"id": 3,
"name": "No children",
"lead_text": "text"
}
],
"sections_relation": [
{
"id_section": 1,
"id_topsection": null,
"position": 1
},
{
"id_section": 2,
"id_topsection": 1,
"position": 2
},
{
"id_section": 3,
"id_topsection": null,
"position": 3
}
],
"producers": [
{
"id": 1,
"name": "Producer 1",
"photo": "x.jpg",
"web": "http://www.example.com"
},
{
"id": 2,
"name": "Producer 2",
"photo": "x.jpg",
"web": "http://www.example.com"
},
{
"id": 5,
"name": "Producer 5",
"photo": "x.jpg",
"web": "http://www.example.com"
}
],
"products": [
{
"id": 1,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": 1,
"vat": 1,
"campaign": "N",
"price": 0,
"discount": 0,
"figure": "Y",
"price_for_discount": null
},
{
"id": 2,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": 2,
"vat": 1,
"campaign": "N",
"price": 10,
"discount": 0,
"figure": "Y"
},
{
"id": 3,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": 1,
"vat": 1,
"campaign": "N",
"price": 20,
"discount": 0,
"figure": "Y"
},
{
"id": 4,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": 1,
"vat": 1,
"campaign": "N",
"price": 30,
"discount": 0,
"figure": "Y"
},
{
"id": 5,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": 1,
"vat": 1,
"campaign": "N",
"price": 40,
"discount": 0,
"figure": "Y"
},
{
"id": 6,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": 5,
"vat": 1,
"campaign": "N",
"price": 120,
"discount": 10,
"figure": "Y"
},
{
"id": 7,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": null,
"vat": 1,
"campaign": "N",
"price": 120,
"discount": 20,
"figure": "Y"
},
{
"id": 8,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": null,
"vat": 1,
"campaign": "N",
"price": 0,
"discount": 0,
"figure": "N"
},
{
"id": 9,
"short_descr": "short",
"long_descr": "long",
"parameters": "params",
"producer": null,
"vat": 1,
"campaign": "N",
"price": 120,
"discount": 20,
"figure": "Y",
"price_for_discount": 200
}
],
"products_in_sections": [
{
"id_section": 1,
"id_product": 1
},
{
"id_section": 2,
"id_product": 2
},
{
"id_section": 2,
"id_product": 3
},
{
"id_section": 2,
"id_product": 4
},
{
"id_section": 3,
"id_product": 5
}
],
"parameters": [
{
"id": 1,
"name": "Size",
"value_type": "int"
},
{
"id": 2,
"name": "Color",
"value_type": "list"
}
],
"parameters_list": [
{
"id": 1,
"id_parameter": 2,
"value": "black"
},
{
"id": 2,
"id_parameter": 2,
"value": "white"
}
],
"parameters_products": [
{
"id": 1,
"id_product": 3,
"id_parameter": 1,
"value_list": null,
"value_char": null,
"value_float": 5
},
{
"id": 2,
"id_product": 3,
"id_parameter": 2,
"value_list": 1,
"value_char": null,
"value_float": null
},
{
"id": 3,
"id_product": 4,
"id_parameter": 2,
"value_list": 1,
"value_char": null,
"value_float": null
}
],
"products_variations": [
{
"id": 1,
"id_product": 4,
"title": "title 1",
"price": null,
"figure": "Y"
},
{
"id": 3,
"id_product": 4,
"title": "title 2",
"price": 1,
"figure": "N"
},
{
"id": 2,
"id_product": 5,
"title": "title 2",
"price": 50,
"figure": "Y"
}
],
"pricelists": [
{
"id": 1,
"currency": "CZK",
"name": "Test 1"
}
],
"pricelists_products": [
{
"id": 1,
"id_pricelist": 1,
"id_product": 1,
"id_variation": null,
"price": 44.3782,
"price_for_discount": null,
"discount": 25,
"integrity_unique": "1-1"
},
{
"id": 2,
"id_pricelist": 1,
"id_product": 2,
"id_variation": null,
"price": 41.3223,
"price_for_discount": 49.5868,
"discount": null,
"integrity_unique": "1-2"
},
{
"id": 3,
"id_pricelist": 1,
"id_product": 3,
"id_variation": null,
"price": 41.3223,
"price_for_discount": 61.9835,
"discount": 10,
"integrity_unique": "1-2"
}
]
}

View File

@@ -0,0 +1,234 @@
<?php
/**
* Created by PhpStorm.
* User: filip
* Date: 5/17/16
* Time: 9:31 AM.
*/
namespace QueryTest;
use KupShop\CatalogBundle\ProductList\FilterParams;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\PriceType\PriceForDiscountPriceType;
use KupShop\KupShopBundle\PriceType\PriceIfOnlyDiscountPriceType;
use KupShop\KupShopBundle\PriceType\PriceTypeInterface;
use KupShop\KupShopBundle\PriceType\PriceWithoutDiscountPriceType;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\PricelistBundle\Context\PricelistContext;
use Query\Filter;
use Query\Operator;
use Query\Product;
class FilterTest extends \DatabaseTestCase
{
/** @dataProvider data_testVisibilityFilter */
public function testVisibilityFilter(int $expectedCount, callable $figureSpec, string $type = FilterParams::ENTITY_PRODUCT): void
{
$qb = sqlQueryBuilder()
->select('p.id')
->joinVariationsOnProducts()
->from('products', 'p')
->where($figureSpec);
if ($type === FilterParams::ENTITY_VARIATION) {
$qb->groupBy('p.id, pv.id');
} else {
$qb->groupBy('p.id');
}
$count = $qb->execute()->rowCount();
$this->assertEquals($expectedCount, $count);
}
public function data_testVisibilityFilter(): iterable
{
yield 'Test load visible products' => [8, Filter::isVisible()];
yield 'Test load hidden products' => [2, Operator::not(Filter::isVisible())];
yield 'Test load sold out products' => [0, Filter::byFigure('O')];
yield 'Test load visible and hidden products' => [9, Operator::orX(Filter::isVisible(), Operator::not(Filter::isVisible()))];
// Variations list - 1 visible variation + 6 visible products = 7
yield 'Test load visible variation product list' => [8, Filter::isVisible(), FilterParams::ENTITY_VARIATION];
// Variation list - 1 hidden variation and 1 hidden product
yield 'Test load hidden variation product list' => [2, Operator::not(Filter::isVisible()), FilterParams::ENTITY_VARIATION];
// Variation list - 6 products + 3 variation products
yield 'Test load visible and hidden variation product list' => [10, Operator::orX(Filter::isVisible(), Operator::not(Filter::isVisible())), FilterParams::ENTITY_VARIATION];
}
/** @dataProvider dataSet_testByDiscountRange */
public function testByDiscountRange(\Range $range, ?int $priceListId, ?PriceTypeInterface $originalPriceType, array $expectedIDs): void
{
if ($priceListId) {
Contexts::get(PricelistContext::class)->activate($priceListId);
}
if ($originalPriceType) {
Contexts::clear();
$this->withMockedUserContext($originalPriceType);
}
$IDs = sqlQueryBuilder()
->select('p.id')
->from('products', 'p')
->where(Filter::byDiscountRange($range))
->groupBy('p.id')
->execute()
->fetchFirstColumn();
$this->assertEquals($expectedIDs, $IDs);
}
public function dataSet_testByDiscountRange(): iterable
{
yield 'Filter discount range 1-5' => [new \Range(1, 5), null, null, []];
yield 'Filter discount range 5-12' => [new \Range(5, 12), null, null, [6]];
yield 'Filter discount range 10-15' => [new \Range(10, 15), null, null, [6]];
yield 'Filter discount range 10-20' => [new \Range(10, 20), null, null, [6, 7]];
yield 'Filter discount range 20-35' => [new \Range(20, 35), null, null, [7]];
yield 'Filter with price list range 1-10' => [new \Range(1, 10), 1, null, []];
yield 'Filter with price list range 20-30' => [new \Range(20, 30), 1, null, [1]];
yield 'Filter with price list range 15-20 - discount calculated using price_for_discount' => [new \Range(15, 20), 1, null, [2]];
yield 'Filter with price list range 15-20 - with explicit discount field' => [new \Range(15, 20), 1, new PriceIfOnlyDiscountPriceType(new PriceForDiscountPriceType()), []];
yield 'Filter with price list range 30-40 - with explicit discount field' => [new \Range(30, 40), 1, new PriceIfOnlyDiscountPriceType(new PriceForDiscountPriceType()), [3]];
yield 'Filter not discounted products' => [new \Range(null, 0), null, null, [1, 2, 3, 4, 5, 8]];
yield 'Filter not discounted products for price list' => [new \Range(null, 0), 1, null, [4, 5, 6, 7, 8]];
yield 'Filter not discounted products for price list - without price history' => [new \Range(null, 0), 1, new PriceWithoutDiscountPriceType(), [2, 4, 5, 6, 7, 8, 9]];
yield 'Filter not discounted products for price list - with explicit discount field' => [new \Range(null, 0), 1, new PriceIfOnlyDiscountPriceType(new PriceForDiscountPriceType()), [2, 4, 5, 6, 7, 8, 9]];
}
/** @dataProvider dataSet_testDiscountFieldDefinition */
public function testDiscountFieldDefinition(int $productId, ?int $priceListId, ?PriceTypeInterface $originalPriceType, float $expectedDiscount): void
{
if ($priceListId) {
Contexts::get(PricelistContext::class)->activate($priceListId);
}
if ($originalPriceType) {
$this->withMockedUserContext($originalPriceType);
}
$discountFieldDefinition = $this->get(UserContext::class)->getOriginalPriceType()->getDiscountFieldDefinition();
$discount = sqlQueryBuilder()->select($discountFieldDefinition->getField())
->fromProducts()
->joinVariationsOnProducts()
->where(Operator::equals(['p.id' => $productId]))
->andWhere($discountFieldDefinition->getSpec())
->execute()
->fetchOne();
$this->assertEqualsWithDelta($expectedDiscount, $discount, 0.1);
}
public function dataSet_testDiscountFieldDefinition(): iterable
{
yield 'Product with zero price' => [1, null, null, 0];
yield 'Product with zero discount' => [2, null, null, 0];
yield 'Product with 10% discount' => [6, null, null, 10];
yield 'Product with price for discount' => [9, null, null, 100 - (((120 * 0.8) * 100) / 200)];
yield 'Product with discount from pricelist' => [1, 1, null, 25];
yield 'Product with price for discount from pricelist' => [2, 1, null, 100 - ((41.3223 * 100) / 49.5868)];
}
/**
* @dataProvider dataSet_testByProducers
*/
public function testByProducers(array $producerIds, int $expectedCount, bool $negative): void
{
$this->assertSame($expectedCount, sqlQueryBuilder()
->select('p.id')
->from('products', 'p')
->andWhere($negative
? Operator::not(Filter::byProducers($producerIds))
: Filter::byProducers($producerIds)
)
->execute()
->rowCount()
);
}
public function dataSet_testByProducers(): array
{
return [
[[1], 4, false],
[[2], 1, false],
[[3], 0, false],
[[1, 2, 3], 5, false],
[[1337], 9, true],
[[1337], 0, false],
[[1, 2, 3], 4, true],
];
}
/**
* @dataProvider dataSet_testByPriceRange
*/
public function testByPriceRange(\Range $range, $expectedCount)
{
$count = sqlQueryBuilder()
->select('p.id')
->from('products', 'p')
->where(Filter::byPriceRange($range))
->execute()
->rowCount();
$this->assertSame($expectedCount, $count);
}
public function dataSet_testByPriceRange()
{
return [
[new \Range(5 * 1.21, 5 * 1.21), 0],
[new \Range(10 * 1.21, 10 * 1.21), 1],
[new \Range(10 * 1.21, 30 * 1.21), 3],
];
}
/**
* @dataProvider dataSet_testByProductPriceRange
*/
public function testByProductPriceRange(\Range $range, $expectedCount)
{
$qb = sqlQueryBuilder()
->select('p.id')
->from('products', 'p')
->where(Filter::byProductPriceRange($range))
->groupBy('p.id')
->execute();
$count = $qb->rowCount();
$this->assertSame($expectedCount, $count);
}
public function dataSet_testByProductPriceRange()
{
return [
[new \Range(5 * 1.21, 5 * 1.21), 0],
[new \Range(30 * 1.21, 30 * 1.21), 1],
[new \Range(30 * 1.21, 40 * 1.21), 2],
];
}
public function getDataSet()
{
return $this->getJsonDataSetFromFile();
}
private function withMockedUserContext(PriceTypeInterface $originalPriceType): void
{
$mockedUserContext = $this->getMockBuilder(UserContext::class)
->onlyMethods(['loadOriginalPriceType'])
->getMock();
$mockedUserContext->expects(self::once())
->method('loadOriginalPriceType')
->willReturn($originalPriceType);
$this->set(UserContext::class, $mockedUserContext);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace QueryTest;
use Doctrine\DBAL\Query\QueryBuilder;
use Query\Operator;
use Query\Operator as Op;
class OperatorTest extends \DatabaseTestCase
{
public function testAndX()
{
$noName = function (QueryBuilder $qb) {
return $this->expr()->isNull('alias.name');
};
$hasPrice = function (QueryBuilder $qb) {
return $this->expr()->isNotNull('alias.price');
};
$andExpr = Op::andX($noName, $hasPrice);
$where = $andExpr(sqlQueryBuilder());
$this->assertSame('(alias.name IS NULL) AND (alias.price IS NOT NULL)', (string) $where);
}
/**
* @dataProvider dataSet_testBetween
*/
public function testBetween($min, $max, $expected)
{
$between = Op::between('field', new \Range($min, $max));
$where = sqlQueryBuilder()->evaluateClosures([$between])[0];
$this->assertRegExp("@{$expected}@", (string) $where);
}
public function dataSet_testBetween()
{
return [
[1, 2, '\(field >= :between_min_\d+\) AND \(field <= :between_max_\d+\)'],
[1, null, 'field >= :between_min_\d+'],
[null, 2, 'field <= :between_max_\d+'],
[null, null, ''],
];
}
/**
* @expectedException \InvalidArgumentException
*/
public function testFindInSetThrows()
{
Operator::findInSet([1, 2, 3], 'column', 'x');
}
public function testFindInSetAnd()
{
$expr = Operator::findInSet([1, 2], 'column', 'AND');
$this->assertRegExp(
'@\(FIND_IN_SET\(:needle_\d+, `column`\)\) AND \(FIND_IN_SET\(:needle_\d+, `column`\)\)@',
(string) sqlQueryBuilder()->evaluateClosures([$expr])[0]
);
}
public function testFindInSetOr()
{
$expr = Operator::findInSet([1, 2], 'column', 'OR');
$this->assertRegExp(
'@\(FIND_IN_SET\(:needle_\d+, `column`\)\) OR \(FIND_IN_SET\(:needle_\d+, `column`\)\)@',
(string) sqlQueryBuilder()->evaluateClosures([$expr])[0]
);
}
public function testNot()
{
$isNull = function (QueryBuilder $qb) {
return $this->expr()->isNull('alias.name');
};
$expr = Operator::not($isNull);
$this->assertSame(
'NOT (alias.name IS NULL)',
(string) sqlQueryBuilder()->evaluateClosures([$expr])[0]
);
}
private function expr()
{
return sqlQueryBuilder()->expr();
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace QueryTest;
use Query\Filter;
use Query\Product;
use Query\QueryBuilder;
class ProductTest extends \DatabaseTestCase
{
/** @var QueryBuilder */
private $qb;
public function setUp(): void
{
parent::setUp();
$this->qb = sqlQueryBuilder();
}
public function testIsVisible()
{
$this->removeModule(\Modules::TRANSLATIONS, null);
$isVisible = Product::isVisible();
$expression = $isVisible($this->qb);
$this->assertRegExp('/p.figure = :visible\d+/', $expression);
preg_match('/p.figure = :(visible\d+)/', $expression, $matches);
$this->assertSame('Y', $this->qb->getParameter($matches[1]));
}
public function testIsVisibleTranslations()
{
$this->switchLanguage();
$isVisible = Product::isVisible();
$expression = $isVisible($this->qb);
$join = $this->qb->getQueryPart('join');
$this->assertEquals([
'joinType' => 'left',
'joinTable' => 'products_translations',
'joinAlias' => 'p_sk',
'joinCondition' => 'p_sk.id_product = p.id AND p_sk.id_language = :translation_language_sk',
], $join['p'][0]);
$this->assertRegExp('/COALESCE\(p_sk.figure, p.figure\) = :visible\d+/', $expression);
}
public function testInSection()
{
$result = $this->qb->select('*')->from('products', 'p')->where(Product::inSection(3))->execute()->fetchAll();
$this->assertCount(3, $result);
}
/**
* @param int $expectedCount
*
* @dataProvider productsInSections
*/
public function testInSections(array $sectionIds, $expectedCount)
{
$count = $this->qb->select('*')->from('products', 'p')->where(Product::inSections($sectionIds))->execute()->rowCount();
$this->assertSame($expectedCount, $count);
}
public function productsInSections()
{
return [
[[3], 3],
[[2, 3], 5],
];
}
/**
* @dataProvider dataSet_testInStore
*
* @param bool $useVariations
* @param int $expectedCount
*/
public function testInStore($useVariations, $expectedCount)
{
$count = $this->qb->select('*')->from('products', 'p')->where(Product::inStore($useVariations))->execute()->rowCount();
$this->assertSame($expectedCount, $count);
}
public function dataSet_testInStore()
{
$dataSet = [];
$dataSet['no variations'] = [false, 7];
if (findModule('products_variations')) {
$dataSet['variations'] = [true, 13];
}
return $dataSet;
}
/**
* @todo add yml data to products_of_suppliers and refactor this test
*
* @dataProvider dataSet_testInStore
*/
public function testInStoreSupplier($useVariations, $expectedCount)
{
$count = $this->qb->select('*')->from('products', 'p')
->andWhere(Filter::byInStore(\Filter::IN_STORE_SUPPLIER, $useVariations))
->execute()->rowCount();
$this->assertSame($expectedCount, $count);
}
}

View File

@@ -0,0 +1,369 @@
<?php
/**
* Created by PhpStorm.
* User: filip
* Date: 5/13/16
* Time: 10:30 AM.
*/
namespace QueryTest;
use Doctrine\DBAL\Query\QueryBuilder;
use Query\Operator;
class QueryBuilderTest extends \DatabaseTestCase
{
public function testGlobalFactoryFunction()
{
$this->assertInstanceOf('Query\QueryBuilder', sqlQueryBuilder());
$this->assertNotSame(sqlQueryBuilder(), sqlQueryBuilder());
}
/**
* @depends testGlobalFactoryFunction
*/
public function testWhereUsesAnd()
{
$specStub = function ($field, $value) {
return function (QueryBuilder $qb) use ($field, $value) {
return $qb->expr()->eq($field, $value);
};
};
$rowCount = sqlQueryBuilder()->select('*')->from('users')->where(
$specStub('city', "'Vrchlabí'"),
$specStub('figure', "'Y'")
)->execute()->rowCount();
$this->assertSame(2, $rowCount);
}
public function testOrderBySql()
{
$qb = sqlQueryBuilder();
$qb->orderBySql('expression < 0 Asc');
$this->assertSame(['expression < 0 Asc'], $qb->getQueryPart('orderBy'));
}
public function testMultiDirectValuesSql()
{
$qb = sqlQueryBuilder()
->insert('languages')
->multiDirectValues(
[
'id' => 've',
'name' => 'veverky',
'locale' => 've_VE',
'translate' => 0,
]
)
->multiDirectValues(
[
'id' => 'li',
'name' => 'lišky',
'locale' => 'li_LI',
'translate' => 0,
])
->multiDirectValues(
[
'id' => 'ko',
'name' => 'kočky',
'locale' => 'ko_KO',
'translate' => 0,
]);
$this->assertSame('INSERT INTO languages (`id`,`name`,`locale`,`translate`) VALUES (:multi0_id, :multi0_name, :multi0_locale, :multi0_translate),(:multi1_id, :multi1_name, :multi1_locale, :multi1_translate),(:multi2_id, :multi2_name, :multi2_locale, :multi2_translate) ', $qb->getSQL());
}
public function testMultiValuesSql()
{
$qb = sqlQueryBuilder()
->insert('languages')
->multiValues(
[
[
'id' => 've',
'name' => 'veverky',
'locale' => 've_VE',
'translate' => 0,
],
[
'id' => 'li',
'name' => 'lišky',
'locale' => 'li_LI',
'translate' => 0,
],
[
'id' => 'ko',
'name' => 'kočky',
'locale' => 'ko_KO',
'translate' => 0,
],
]
);
$this->assertSame('INSERT INTO languages (id,name,locale,translate) VALUES (ve, veverky, ve_VE, 0),(li, lišky, li_LI, 0),(ko, kočky, ko_KO, 0) ', $qb->getSQL());
}
public function testDirectValues(): void
{
$params = [
'foo' => 'fooFoo',
'bar' => 10,
'baz' => '1970-01-01 00:00:00',
];
$queryBuilder = sqlQueryBuilder()
->insert('users')
->directValues($params);
$this->assertSame('INSERT INTO users (`foo`,`bar`,`baz`) VALUES (:foo, :bar, :baz) ', $queryBuilder->getSQL());
$this->assertSame($params, $queryBuilder->getParameters());
$this->assertSame(
<<<EOT
INSERT INTO users (`foo`, `bar`, `baz`)
VALUES
(
'fooFoo', 10, '1970-01-01 00:00:00'
)
EOT,
$queryBuilder->getRunnableSQL()
);
}
public function testForUpdate(): void
{
$queryBuilder = sqlQueryBuilder()
->select('count(*)', 'foo', 'baz')
->where('a', 'b.c')
->forUpdate();
$this->assertSame('SELECT count(*), foo, baz WHERE (a) AND (b.c) FOR UPDATE', $queryBuilder->getSQL());
}
public function testCalcRows(): void
{
$queryBuilder = sqlQueryBuilder()
->select('*', 'foo')
->from('t')
->where('id >= :id')
->setParameter('id', 1)
->addCalcRows();
$this->assertSame('SELECT SQL_CALC_FOUND_ROWS *, foo FROM t WHERE id >= :id', $queryBuilder->getSQL());
$this->assertSame(['id' => 1], $queryBuilder->getParameters());
}
public function testOnDuplicateKeyUpdate(): void
{
$queryBuilder = sqlQueryBuilder()
->insert('foo')
->values(['username' => 'jarin'])
->onDuplicateKeyUpdate(['foo', 'bar']);
$this->assertSame('INSERT INTO foo (username) VALUES (jarin) ON DUPLICATE KEY UPDATE foo=VALUES(foo), bar=VALUES(bar) ', $queryBuilder->getSQL());
}
public function testOnDuplicateKeyUpdateException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Call of "onDuplicateKeyUpdate" is allowed only with insert query');
sqlQueryBuilder()
->select('foo')
->values(['username' => 'jarin'])
->onDuplicateKeyUpdate(['foo', 'bar'])
->getSQL();
}
public function testSendMaster(): void
{
$queryBuilder = sqlQueryBuilder()
->select('foo')
->from('t')
->sendToMaster();
$this->assertSame('SELECT foo FROM t -- maxscale route to master', $queryBuilder->getSQL());
}
public function testAddQueryBuilderParameters(): void
{
$queryBuilderForParameters = sqlQueryBuilder()
->setParameter('foo', 1)
->setParameter('bar', 'baz');
$queryBuilder = sqlQueryBuilder()
->select('*')
->from('t')
->where('foo != :foo')
->orWhere('bar = :bar')
->addQueryBuilderParameters($queryBuilderForParameters);
$this->assertSame('SELECT * FROM t WHERE (foo != :foo) OR (bar = :bar)', $queryBuilder->getSQL());
$this->assertSame(
<<<EOT
SELECT
*
FROM
t
WHERE
(foo != 1)
OR (bar = 'baz')
EOT,
$queryBuilder->getRunnableSQL()
);
$this->assertSame($queryBuilderForParameters->getParameters(), $queryBuilder->getParameters());
}
public function testSetForceIndexForJoin(): void
{
$queryBuilder = sqlQueryBuilder()
->select('id')
->from('t')
->setForceIndexForJoin('a', 'a_table');
// ignore when using non-existing join alias
$this->assertSame('SELECT id FROM t', $queryBuilder->getSQL());
$queryBuilder = sqlQueryBuilder()
->select('id', 'MAX(max_1, max_2) as j')
->from('t', 'a')
->leftJoin('a', 'products', 'p', 'p.id = a.id_product')
->setForceIndexForJoin('j', 'pricelist_product')
->setForceIndexForJoin('a', 'blbost');
// ignore when using force index for from clause alias or select clause alias
$this->assertSame('SELECT id, MAX(max_1, max_2) as j FROM t a LEFT JOIN products p ON p.id = a.id_product', $queryBuilder->getSQL());
$queryBuilder = sqlQueryBuilder()
->select('id', 'MAX(max_1, max_2) as j')
->from('t', 'a')
->leftJoin('a', 'products', 'p', 'p.id = a.id_product')
->setForceIndexForJoin('p', 'PRIMARY');
$this->assertSame('SELECT id, MAX(max_1, max_2) as j FROM t a LEFT JOIN products p FORCE INDEX (PRIMARY) ON p.id = a.id_product', $queryBuilder->getSQL());
}
public function testLimitDeleteThrowsMultitable01()
{
$qb = sqlQueryBuilder()->delete('orders', 'o')
->setMaxResults(100);
$this->expectException(\Doctrine\DBAL\Exception::class);
$qb->execute();
}
public function testLimitDeleteThrowsMultitable02()
{
$qb = sqlQueryBuilder()->delete('orders')
->join('orders', 'orders_history', 'oh', 'oh.id_order = orders.id')
->setMaxResults(100);
$this->expectException(\Doctrine\DBAL\Exception::class);
$qb->execute();
}
public function testLimitDelete()
{
$qb = sqlQueryBuilder()->delete('orders')
->setMaxResults(1000);
$qb->execute(); // Should not throw
$this->assertSame('DELETE FROM orders LIMIT 1000', $qb->getSQL());
$qb = sqlQueryBuilder()->delete('orders_history')
->andWhere(Operator::exists(
sqlQueryBuilder()->select('*')
->from('orders', 'o')
->where('o.id = id_order')
->andWhere('o.date_updated < (NOW() - INTERVAL 5 YEAR)')
))
->andWhere('custom_data IS NOT NULL')
->andWhere("JSON_EXISTS(custom_data, '$.email_type')")
->andWhere("JSON_EXTRACT(custom_data, '$.email_type') <> 'ORDER_CREATE'")
->orderBy('date', 'DESC')
->addOrderBy('id', 'ASC')
->setMaxResults(1000);
$qb->execute(); // Should not throw
$this->assertSame(
"DELETE FROM orders_history WHERE (EXISTS (SELECT * FROM orders o WHERE (o.id = id_order) AND (o.date_updated < (NOW() - INTERVAL 5 YEAR)))) AND (custom_data IS NOT NULL) AND (JSON_EXISTS(custom_data, '$.email_type')) AND (JSON_EXTRACT(custom_data, '$.email_type') <> 'ORDER_CREATE') ORDER BY date DESC, id ASC LIMIT 1000",
$qb->getSQL(),
);
// Test normal delete still functional
$qb = sqlQueryBuilder()->delete()
->from('orders_history', 'oh')
->andWhere(Operator::exists(
sqlQueryBuilder()->select('*')
->from('orders', 'o')
->where('o.id = id_order')
->andWhere('o.date_updated < (NOW() - INTERVAL 5 YEAR)')
))
->andWhere('custom_data IS NOT NULL')
->andWhere("JSON_EXISTS(custom_data, '$.email_type')")
->andWhere("JSON_EXTRACT(custom_data, '$.email_type') <> 'ORDER_CREATE'");
$qb->execute(); // Should not throw
$this->assertSame(
"DELETE oh FROM orders_history oh WHERE (EXISTS (SELECT * FROM orders o WHERE (o.id = id_order) AND (o.date_updated < (NOW() - INTERVAL 5 YEAR)))) AND (custom_data IS NOT NULL) AND (JSON_EXISTS(custom_data, '$.email_type')) AND (JSON_EXTRACT(custom_data, '$.email_type') <> 'ORDER_CREATE')",
$qb->getSQL(),
);
$qb = sqlQueryBuilder()->delete('orders_history', 'oh')
->andWhere(Operator::exists(
sqlQueryBuilder()->select('*')
->from('orders', 'o')
->where('o.id = id_order')
->andWhere('o.date_updated < (NOW() - INTERVAL 5 YEAR)')
))
->andWhere('custom_data IS NOT NULL')
->andWhere("JSON_EXISTS(custom_data, '$.email_type')")
->andWhere("JSON_EXTRACT(custom_data, '$.email_type') <> 'ORDER_CREATE'");
$qb->execute(); // Should not throw
$this->assertSame(
"DELETE oh FROM orders_history oh WHERE (EXISTS (SELECT * FROM orders o WHERE (o.id = id_order) AND (o.date_updated < (NOW() - INTERVAL 5 YEAR)))) AND (custom_data IS NOT NULL) AND (JSON_EXISTS(custom_data, '$.email_type')) AND (JSON_EXTRACT(custom_data, '$.email_type') <> 'ORDER_CREATE')",
$qb->getSQL(),
);
}
public function testLimitUpdate()
{
$qb = sqlQueryBuilder()
->update('orders_history', 'oh')
->set('oh.comment', 'NULL')
->orderBy('oh.date')
->setMaxResults(10000);
$qb->execute();
$this->assertSame('UPDATE orders_history oh SET oh.comment = NULL ORDER BY oh.date ASC LIMIT 10000', $qb->getSQL());
$qb = sqlQueryBuilder()
->update('orders_history', 'oh')
->set('oh.comment', 'NULL');
$qb->execute();
$this->assertSame('UPDATE orders_history oh SET oh.comment = NULL', $qb->getSQL());
$qb = sqlQueryBuilder()
->update('orders_history', 'oh')
->join('oh', 'orders', 'o', 'oh.id_order = o.id')
->set('oh.comment', 'NULL');
$qb->execute();
$this->assertSame('UPDATE orders_history oh INNER JOIN orders o ON oh.id_order = o.id SET oh.comment = NULL', $qb->getSQL());
$qb = sqlQueryBuilder()
->update('orders_history', 'oh')
->join('oh', 'orders', 'o', 'oh.id_order = o.id')
->set('oh.comment', 'NULL')
->orderBy('oh.date', 'DESC')
->addOrderBy('o.date_updated', 'ASC')
->setMaxResults(10000);
// https://mariadb.com/kb/en/update/#description
// "Until MariaDB 10.3.2, for the multiple-table syntax, UPDATE updates rows in each table named in table_references that satisfy the conditions.
// In this case, ORDER BY and LIMIT cannot be used. This restriction was lifted in MariaDB 10.3.2 and both clauses can be used with multiple-table updates."
$qb->execute(); // Should not throw
$this->assertSame('UPDATE orders_history oh INNER JOIN orders o ON oh.id_order = o.id SET oh.comment = NULL ORDER BY oh.date DESC, o.date_updated ASC LIMIT 10000', $qb->getSQL());
}
}