Skip to content

Commit f5a10f7

Browse files
committed
[ADD] Build a cross-database numeric cast expression for a TV value column.
1 parent 4e7c66d commit f5a10f7

File tree

1 file changed

+76
-53
lines changed

1 file changed

+76
-53
lines changed

src/Models/sLangContent.php

Lines changed: 76 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -112,67 +112,66 @@ public function scopeSearch()
112112
}
113113

114114
/**
115-
* Adds a WHERE clause to the query that filters results based on the given TV and value.
116-
* If value is an array, uses WHERE IN clause, otherwise uses WHERE clause with the specified operator.
115+
* Add a WHERE clause to filter by a Template Variable (TV) value using EXISTS.
117116
*
118-
* @param \Illuminate\Database\Query\Builder $query The database query builder instance
119-
* @param string $name The name of the TV to filter by
120-
* @param mixed $value The value to filter by (can be a single value or an array)
121-
* @param string $operator The comparison operator (=, >, <, >=, <=, !=, <>, like, not like). Default is '='
122-
* @return \Illuminate\Database\Query\Builder The modified query builder instance
117+
* This approach avoids JOIN-driven row multiplication and therefore does not require GROUP BY
118+
* to de-duplicate results. It is compatible with MySQL 8.0+, MariaDB 10.5+, PostgreSQL 10+,
119+
* and SQLite 3.25+.
120+
*
121+
* Supported operators:
122+
* - Equality: =, !=, <>
123+
* - Pattern: like, not like (auto-wraps value with % if no wildcard is present)
124+
* - Numeric: >, <, >=, <= (casts TV value to a numeric type per DB driver)
125+
* - Arrays: value as array -> WHERE IN (operator is ignored)
126+
*
127+
* @param Builder $query The Eloquent query builder instance.
128+
* @param string $name The TV name to filter by.
129+
* @param mixed $value The value to filter by (scalar or array).
130+
* @param string $operator The comparison operator (=, >, <, >=, <=, !=, <>, like, not like).
131+
* @return Builder The modified query builder instance.
123132
*/
124-
public function scopeWhereTv($query, $name, $value, $operator = '=')
133+
public function scopeWhereTv(Builder $query, string $name, $value, string $operator = '='): Builder
125134
{
126-
$tvValuesAlias = 'tv_values_' . $name;
127-
$tvVarsAlias = 'tv_vars_' . $name;
128-
129-
// Check if join already exists to avoid "Not unique table/alias" error
130-
$eloquentQuery = $query->getQuery();
131-
$hasJoin = $this->hasTvJoin($eloquentQuery->joins ?? [], $tvValuesAlias);
132-
133-
if (!$hasJoin) {
134-
$query = $query->leftJoin('site_tmplvar_contentvalues as ' . $tvValuesAlias, function($join) use ($tvValuesAlias) {
135-
$join->on($tvValuesAlias . '.contentid', '=', 's_lang_content.resource');
136-
});
137-
138-
// Update query reference and check again for vars join
139-
$eloquentQuery = $query->getQuery();
140-
$hasVarsJoin = $this->hasTvJoin($eloquentQuery->joins ?? [], $tvVarsAlias);
141-
if (!$hasVarsJoin) {
142-
$query = $query->leftJoin('site_tmplvars as ' . $tvVarsAlias, function($join) use ($name, $tvValuesAlias, $tvVarsAlias) {
143-
$join->on($tvVarsAlias . '.id', '=', $tvValuesAlias . '.tmplvarid')
144-
->where($tvVarsAlias . '.name', '=', $name);
145-
});
135+
$op = strtolower(trim($operator));
136+
137+
return $query->whereExists(function ($sub) use ($name, $value, $op) {
138+
$sub->select(DB::raw('1'))
139+
->from('site_tmplvar_contentvalues as stvc')
140+
->join('site_tmplvars as stv', 'stv.id', '=', 'stvc.tmplvarid')
141+
->whereColumn('stvc.contentid', 's_lang_content.resource')
142+
->where('stv.name', '=', $name);
143+
144+
// Arrays => IN (operator ignored).
145+
if (is_array($value)) {
146+
$sub->whereIn('stvc.value', $value);
147+
return;
146148
}
147-
}
148149

149-
if (is_array($value)) {
150-
// For arrays, use whereIn regardless of operator (array comparison doesn't make sense with >, <, etc.)
151-
return $query->whereIn($tvValuesAlias . '.value', $value);
152-
}
150+
// LIKE operators.
151+
if ($op === 'like' || $op === 'not like') {
152+
$likeValue = (string)$value;
153+
if (strpos($likeValue, '%') === false) {
154+
$likeValue = '%' . $likeValue . '%';
155+
}
156+
$sub->where('stvc.value', $op, $likeValue);
157+
return;
158+
}
153159

154-
// Handle LIKE operators
155-
$operator = strtolower($operator);
156-
if ($operator === 'like' || $operator === 'not like') {
157-
// If value doesn't already contain wildcards, add them for LIKE search
158-
$likeValue = $value;
159-
if (strpos($value, '%') === false) {
160-
$likeValue = '%' . $value . '%';
160+
// Numeric comparisons (cast column to numeric across supported DB drivers).
161+
if (in_array($op, ['>', '<', '>=', '<='], true)) {
162+
$castExpr = $this->tvNumericCastExpression('stvc.value');
163+
$sub->whereRaw($castExpr . ' ' . $op . ' ?', [(float)$value]);
164+
return;
161165
}
162-
return $query->where($tvValuesAlias . '.value', $operator, $likeValue);
163-
}
164166

165-
// For numeric comparisons, cast the value to numeric
166-
if (in_array($operator, ['>', '<', '>=', '<='])) {
167-
// Cast to numeric for comparison operators
168-
$prefix = DB::getTablePrefix();
169-
$fullAlias = $prefix . $tvValuesAlias;
170-
$column = '`' . $fullAlias . '`.`value`';
171-
return $query->whereRaw('CAST(' . $column . ' AS DECIMAL(10,2)) ' . $operator . ' ?', [(float)$value]);
172-
}
167+
// Default: equality / inequality and any other supported operator.
168+
// Normalize '!=' and '<>' both are fine across DBs, keep as provided (normalized).
169+
if ($op === '!=') {
170+
$op = '<>';
171+
}
173172

174-
// For equality operators, use standard where clause
175-
return $query->where($tvValuesAlias . '.value', $operator, $value);
173+
$sub->where('stvc.value', $op, $value);
174+
});
176175
}
177176

178177
/**
@@ -276,7 +275,8 @@ protected function applyContentSelects(Builder $query): Builder
276275
'site_content.description as description_orig',
277276
'site_content.introtext as introtext_orig',
278277
'site_content.content as content_orig',
279-
'site_content.menutitle as menutitle_orig'
278+
'site_content.menutitle as menutitle_orig',
279+
'site_content.pub_date as pub_date_orig'
280280
);
281281
}
282282

@@ -366,6 +366,29 @@ protected static function resolveLocale(?string $locale): string
366366
return strtolower($locale);
367367
}
368368

369+
/**
370+
* Build a cross-database numeric cast expression for a TV value column.
371+
*
372+
* Note:
373+
* - MySQL/MariaDB: DECIMAL is appropriate.
374+
* - PostgreSQL: NUMERIC is appropriate.
375+
* - SQLite: REAL is the pragmatic choice (SQLite uses dynamic typing / affinity).
376+
*
377+
* @param string $column Fully qualified column name (e.g., "stvc.value").
378+
* @return string SQL expression casting the column to a numeric type.
379+
*/
380+
protected function tvNumericCastExpression(string $column): string
381+
{
382+
$driver = DB::connection()->getDriverName();
383+
384+
return match ($driver) {
385+
'pgsql' => "CAST($column AS NUMERIC(10,2))",
386+
'sqlite' => "CAST($column AS REAL)",
387+
'mysql', 'mariadb' => "CAST($column AS DECIMAL(10,2))",
388+
default => "CAST($column AS DECIMAL(10,2))",
389+
};
390+
}
391+
369392
/**
370393
* Register the default language scope for the model.
371394
*/

0 commit comments

Comments
 (0)