@@ -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