Merge branch 'fix'
This commit is contained in:
@@ -165,7 +165,7 @@ class Htmlizer
|
||||
if (!$skipInlineAttachmentHandling) {
|
||||
/** @var string $html */
|
||||
$html = preg_replace_callback(
|
||||
'/\?entryPoint=attachment&id=([A-Za-z0-9]*)/',
|
||||
'/\?entryPoint=attachment&id=([A-Za-z0-9\-]*)/',
|
||||
function ($matches) {
|
||||
$id = $matches[1];
|
||||
|
||||
|
||||
@@ -298,7 +298,10 @@ class Service
|
||||
'assignedUserId' => $userId,
|
||||
];
|
||||
|
||||
if ($seed->hasRelation('users')) {
|
||||
// @todo Use a metadata parameter scope > usersLink. Populate in an upgrade script.
|
||||
$hasUsers = $seed->hasRelation('users') && $seed->getRelationType('users') === Entity::MANY_MANY;
|
||||
|
||||
if ($hasUsers) {
|
||||
$orGroup['usersMiddle.userId'] = $userId;
|
||||
}
|
||||
|
||||
@@ -343,13 +346,15 @@ class Service
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
if ($seed->hasRelation('users')) {
|
||||
if ($hasUsers) {
|
||||
// @todo Use sub-query.
|
||||
$queryBuilder
|
||||
->distinct()
|
||||
->leftJoin('users');
|
||||
}
|
||||
|
||||
if ($seed->hasRelation(Field::ASSIGNED_USERS)) {
|
||||
// @todo Use sub-query.
|
||||
$queryBuilder
|
||||
->distinct()
|
||||
->leftJoin(Field::ASSIGNED_USERS);
|
||||
|
||||
@@ -763,6 +763,16 @@ class Expression implements WhereItem
|
||||
return self::composeFunction('LEAST', ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 'ANY_VALUE' function.
|
||||
*
|
||||
* @since 9.1.6
|
||||
*/
|
||||
public function anyValue(Expression $expression): self
|
||||
{
|
||||
return self::composeFunction('ANY_VALUE', $expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* 'AND' operator. Returns TRUE if all arguments are TRUE.
|
||||
*/
|
||||
|
||||
@@ -47,6 +47,7 @@ class Join
|
||||
|
||||
private ?WhereItem $conditions = null;
|
||||
private bool $onlyMiddle = false;
|
||||
private bool $isLateral = false;
|
||||
|
||||
private function __construct(
|
||||
private string|Select $target,
|
||||
@@ -132,6 +133,16 @@ class Join
|
||||
return $this->onlyMiddle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is LATERAL.
|
||||
*
|
||||
* @since 9.1.6
|
||||
*/
|
||||
public function isLateral(): bool
|
||||
{
|
||||
return $this->isLateral;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create.
|
||||
*
|
||||
@@ -216,4 +227,21 @@ class Join
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* With LATERAL. Only for a sub-query join.
|
||||
*
|
||||
* @since 9.1.6
|
||||
*/
|
||||
public function withLateral(bool $isLateral = true): self
|
||||
{
|
||||
if (!$this->isSubQuery()) {
|
||||
throw new LogicException("Lateral can be used only with sub-query joins.");
|
||||
}
|
||||
|
||||
$obj = clone $this;
|
||||
$obj->isLateral = $isLateral;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@ trait SelectingBuilderTrait
|
||||
): self {
|
||||
|
||||
$onlyMiddle = false;
|
||||
$isLateral = false;
|
||||
|
||||
/** @var string|Join|array<int, mixed> $target */
|
||||
|
||||
@@ -208,6 +209,8 @@ trait SelectingBuilderTrait
|
||||
$alias = $alias ?? $target->getAlias();
|
||||
$conditions = $conditions ?? $target->getConditions();
|
||||
$onlyMiddle = $target->isOnlyMiddle();
|
||||
$isLateral = $target->isLateral();
|
||||
|
||||
$target = $target->getTarget();
|
||||
}
|
||||
|
||||
@@ -256,6 +259,10 @@ trait SelectingBuilderTrait
|
||||
$params['onlyMiddle'] = true;
|
||||
}
|
||||
|
||||
if ($isLateral) {
|
||||
$params['isLateral'] = true;
|
||||
}
|
||||
|
||||
if ($params !== []) {
|
||||
$this->params[$type][] = [$target, $alias, $conditions, $params];
|
||||
|
||||
|
||||
@@ -3140,6 +3140,18 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
|
||||
$value = $right;
|
||||
|
||||
if ($isNotValue) {
|
||||
if (is_null($value)) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$rightPart = $this->convertComplexExpression($entity, $value, false, $params);
|
||||
|
||||
$sql .= " " . $operator . " " . $rightPart;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
if (is_null($value)) {
|
||||
if ($operator === '=') {
|
||||
$sql .= " IS NULL";
|
||||
@@ -3150,14 +3162,6 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
return $sql;
|
||||
}
|
||||
|
||||
if ($isNotValue) {
|
||||
$rightPart = $this->convertComplexExpression($entity, $value, false, $params);
|
||||
|
||||
$sql .= " " . $operator . " " . $rightPart;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$sql .= " " . $operator . " " . $this->quote($value);
|
||||
|
||||
return $sql;
|
||||
@@ -3206,7 +3210,13 @@ abstract class BaseQueryComposer implements QueryComposer
|
||||
|
||||
$aliasPart = $this->quoteIdentifier($alias);
|
||||
|
||||
$sql = $prefixPart . "JOIN $targetPart AS $aliasPart";
|
||||
$sql = $prefixPart . "JOIN ";
|
||||
|
||||
if (!empty($joinParams['isLateral'])) {
|
||||
$sql .= "LATERAL ";
|
||||
}
|
||||
|
||||
$sql .= "$targetPart AS $aliasPart";
|
||||
|
||||
if ($conditions === []) {
|
||||
return $sql;
|
||||
|
||||
@@ -141,6 +141,7 @@ class Functions
|
||||
'POSITION_IN_LIST',
|
||||
'MATCH_BOOLEAN',
|
||||
'MATCH_NATURAL_LANGUAGE',
|
||||
'ANY_VALUE',
|
||||
];
|
||||
|
||||
public const COMPARISON_FUNCTION_LIST = [
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"status": {
|
||||
"type": "enum",
|
||||
"options": ["Active", "Inactive"],
|
||||
"style": {
|
||||
"Inactive": "info"
|
||||
},
|
||||
"default": "Active"
|
||||
},
|
||||
"host": {
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"status": {
|
||||
"type": "enum",
|
||||
"options": ["Active", "Inactive"],
|
||||
"style": {
|
||||
"Inactive": "info"
|
||||
},
|
||||
"default": "Active"
|
||||
},
|
||||
"host": {
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"type": "enum",
|
||||
"options": ["Active", "Inactive"],
|
||||
"default": "Active",
|
||||
"style": {
|
||||
"Inactive": "info"
|
||||
},
|
||||
"audited": true
|
||||
},
|
||||
"scheduling": {
|
||||
|
||||
@@ -196,7 +196,7 @@ class HtmlComposer
|
||||
) ?? '';
|
||||
|
||||
return preg_replace_callback(
|
||||
"/src=\"\?entryPoint=attachment&id=([A-Za-z0-9]*)\"/",
|
||||
"/src=\"\?entryPoint=attachment&id=([A-Za-z0-9\-]*)\"/",
|
||||
function ($matches) {
|
||||
$id = $matches[1];
|
||||
|
||||
|
||||
@@ -390,6 +390,10 @@ class RelationshipPanelView extends BottomPanelView {
|
||||
|
||||
this.once('show', () => collection.fetch());
|
||||
});
|
||||
|
||||
if (this.defs.syncBackWithModel) {
|
||||
this.listenTo(view, 'after:save after:delete', () => this.processSyncBack());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -750,8 +754,11 @@ class RelationshipPanelView extends BottomPanelView {
|
||||
model: model,
|
||||
})
|
||||
.then(view => {
|
||||
// @todo Move to afterSave?
|
||||
this.listenTo(view, 'after:save', () => {
|
||||
this.collection.fetch();
|
||||
|
||||
this.processSyncBack();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -773,6 +780,8 @@ class RelationshipPanelView extends BottomPanelView {
|
||||
id: id,
|
||||
afterSave: () => {
|
||||
this.collection.fetch();
|
||||
|
||||
this.processSyncBack();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -801,6 +810,8 @@ class RelationshipPanelView extends BottomPanelView {
|
||||
|
||||
this.model.trigger('after:unrelate');
|
||||
this.model.trigger('after:unrelate:' + this.link);
|
||||
|
||||
this.processSyncBack();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -831,6 +842,8 @@ class RelationshipPanelView extends BottomPanelView {
|
||||
|
||||
this.model.trigger('after:unrelate');
|
||||
this.model.trigger('after:unrelate:' + this.link);
|
||||
|
||||
this.processSyncBack();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -857,6 +870,8 @@ class RelationshipPanelView extends BottomPanelView {
|
||||
|
||||
this.model.trigger('after:unrelate');
|
||||
this.model.trigger('after:unrelate:' + this.link);
|
||||
|
||||
this.processSyncBack();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -869,7 +884,22 @@ class RelationshipPanelView extends BottomPanelView {
|
||||
actionCreateRelated() {
|
||||
const helper = new CreateRelatedHelper(this);
|
||||
|
||||
helper.process(this.model, this.link);
|
||||
helper.process(this.model, this.link, {
|
||||
afterSave: () => {
|
||||
this.processSyncBack();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
processSyncBack() {
|
||||
if (!this.defs.syncBackWithModel || this.getHelper().webSocketManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.fetch({highlight: true});
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
@@ -912,8 +942,6 @@ class RelationshipPanelView extends BottomPanelView {
|
||||
|
||||
this.defs.create = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default RelationshipPanelView;
|
||||
|
||||
@@ -507,6 +507,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Re-fetch records when the parent model is saved or refreshed by WebSocket."
|
||||
},
|
||||
"syncBackWithModel": {
|
||||
"type": "boolean",
|
||||
"description": "Re-fetch the parent model when the relationship is updated. Ignored if WebSocked is enabled."
|
||||
},
|
||||
"massSelect": {
|
||||
"type": "boolean",
|
||||
"description": "Allow mass select."
|
||||
|
||||
@@ -1184,6 +1184,11 @@
|
||||
"description": "Apply DISTINCT to the query."
|
||||
}
|
||||
}
|
||||
},
|
||||
"massUpdateActionList": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Actions for mass update. Overrides the value from fields metadata."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,11 @@
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Sanitizers. Classes should implement Espo\\Core\\FieldSanitize\\Sanitizer. As of v8.2."
|
||||
},
|
||||
"massUpdateActionList": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Actions for mass update."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,6 +1057,39 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testJoinSubQueryLateral(): void
|
||||
{
|
||||
$sql =
|
||||
"SELECT post.id AS `id` FROM `post` " .
|
||||
"JOIN LATERAL (SELECT post.id AS `id` FROM `post` LIMIT 0, 1) AS `a` ON TRUE";
|
||||
|
||||
$select = SelectBuilder::create()
|
||||
->select('id')
|
||||
->from('Post')
|
||||
->join(
|
||||
Join::createWithSubQuery(
|
||||
SelectBuilder::create()
|
||||
->select('id')
|
||||
->from('Post')
|
||||
->limit(0, 1)
|
||||
->withDeleted()
|
||||
->build(),
|
||||
'a',
|
||||
)
|
||||
->withLateral()
|
||||
->withConditions(
|
||||
Expression::value(true)
|
||||
)
|
||||
)
|
||||
->withDeleted()
|
||||
->build();
|
||||
|
||||
$this->assertEquals(
|
||||
$sql,
|
||||
$this->query->composeSelect($select)
|
||||
);
|
||||
}
|
||||
|
||||
public function testJoinSubQueryException1(): void
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
Reference in New Issue
Block a user