Merge branch 'fix'

This commit is contained in:
Yuri Kuznetsov
2025-06-08 13:55:07 +03:00
16 changed files with 161 additions and 16 deletions
+1 -1
View File
@@ -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.
*/
+28
View File
@@ -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];
+31 -3
View File
@@ -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;
+4
View File
@@ -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."
+5
View File
@@ -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."
}
}
}
+5
View File
@@ -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);