Descrizione
La ricerca globale (barra di ricerca in alto → ajax_search.php) restituisce un solo risultato per un modulo anche quando i record che combaciano sono molti, se il modulo ha un segmento (o condizione aggiuntiva) con filtro non vuoto in posizione WHERE. Con la configurazione di default il problema si presenta sempre, perché il segmento predefinito "Tutti" ha filtro 1=1.
Comportamento atteso
La ricerca globale restituisce tutti i record che combaciano con il termine cercato.
Comportamento osservato
Restituisce esattamente 1 record, qualunque sia il termine. Con un termine che combacia con un solo articolo, quello viene trovato correttamente; con un termine che combacia con più articoli, ne esce sempre e solo il primo secondo l'ordinamento. La risposta AJAX contiene un solo oggetto.
Esempio: cercando bluet (molti articoli con "Bluetooth" in descrizione) esce un solo articolo; cercando maxi2 (un solo articolo) esce quell'articolo — che però contiene anch'esso "Bluetooth" e quindi doveva comparire anche nella ricerca bluet.
Causa
In modules/articoli/ajax/search.php la query viene chiusa così (righe ~53-55, identiche in 2.9 → 2.11):
$query .= ') GROUP BY `mg_articoli`.`id`';
$query .= Modules::getAdditionalsQuery(Module::where('name', 'Articoli')->first()->id);
getAdditionalsQuery() invocata senza parametro $type fonde le condizioni WHR e HVN in un'unica stringa AND <condizioni>, che viene concatenata dopo il GROUP BY. È un errore di posizionamento:
- È un baco per le condizioni in posizione
WHERE. Una condizione WHERE deve stare prima del GROUP BY; concatenandola dopo, entra a far parte dell'espressione di raggruppamento. GROUP BY \mg_articoli`.`id` AND 1=1viene interpretato da MySQL comeGROUP BY (`id` AND 1=1): poiché idè sempre ≠ 0, l'espressione vale sempre1, tutte le righe finiscono in un unico gruppo e con SELECT * viene restituita **una sola riga**. Nota che basta anche una condizione no-op (1=1`, il filtro del segmento "Tutti") a innescare il collasso — per questo il problema si manifesta praticamente su ogni installazione con un segmento WHERE non vuoto.
- Per le condizioni in posizione
HAVING il "dopo il GROUP BY" sarebbe la regione corretta, quindi la rottura da WHERE mal posizionata non si presenterebbe; resta però che il codice concatena AND cond senza la keyword HAVING, quindi anche una condizione HVN finirebbe erroneamente dentro l'espressione del GROUP BY.
In pratica il search.php non usa il meccanismo di instradamento per posizione che OSM applica già altrove tramite Modules::replaceAdditionals() (placeholder 1=1 per la WHERE, 2=2 per la HAVING).
Perché il problema è poco evidente
La query mostrata come "Query risultante" nell'editor dei segmenti non è quella eseguita dalla ricerca globale. Sono percorsi diversi:
- Editor segmenti (
modules/segmenti/edit.php) e lista/DataTables (src/Util/Query.php, src/API/Controllers/DataTablesController.php) applicano il segmento tramite Modules::replaceAdditionals(), che inserisce le condizioni nei placeholder 1=1 (WHERE) e 2=2 (HAVING) della query principale del modulo → SQL corretto. Per questo l'anteprima sembra a posto e il filtro della lista funziona.
- La ricerca globale (
modules/*/ajax/search.php) non usa replaceAdditionals: costruisce una query separata e concatena getAdditionalsQuery() dopo il GROUP BY. È questa la query realmente eseguita, ed è quella malformata.
Anteprima corretta, lista funzionante e ricerca globale rotta convivono perché sono tre consumatori con due meccanismi diversi.
Come riprodurre
- Modulo Articoli con il segmento predefinito "Tutti" (filtro
1=1, posizione WHERE) — configurazione di default.
- Avere più articoli che combaciano con un termine (es. più articoli con "Bluetooth" in descrizione).
- Cercare quel termine dalla barra di ricerca globale.
- Risultato: un solo articolo invece di tutti quelli che combaciano.
Verifica lato DB — questa query restituisce correttamente tutti i match:
SELECT a.id, a.codice, l.title
FROM mg_articoli a
LEFT JOIN mg_articoli_lang l
ON a.id = l.id_record AND l.id_lang = (SELECT id FROM zz_langs WHERE predefined = 1)
WHERE a.deleted_at IS NULL
AND (1=0 OR l.title LIKE '%bluet%' OR a.codice LIKE '%bluet%')
GROUP BY a.id;
Aggiungendo AND 1=1 dopo GROUP BY a.id il risultato collassa a 1 riga: è la dimostrazione del problema.
Moduli coinvolti
Stesso pattern (getAdditionalsQuery() concatenata dopo il GROUP BY):
modules/articoli/ajax/search.php
modules/contratti/ajax/search.php
modules/interventi/ajax/search.php
modules/preventivi/ajax/search.php
Proposta di correzione
Instradare le condizioni per posizione, riusando lo stesso meccanismo della query principale. Soluzione allineata a replaceAdditionals() (gestisce correttamente sia WHERE che HAVING):
$query .= ') AND 1=1 GROUP BY `mg_articoli`.`id` HAVING 2=2';
$query = Modules::replaceAdditionals(Module::where('name', 'Articoli')->first()->id, $query);
In alternativa, come fix minimale che copre il solo caso WHERE (quello che causa la regressione), spostare solo le condizioni WHR prima del GROUP BY usando il tipo esplicito:
$query .= ')';
$query .= Modules::getAdditionalsQuery(Module::where('name', 'Articoli')->first()->id, 'WHR');
$query .= ' GROUP BY `mg_articoli`.`id`';
Da applicare la stessa correzione agli altri moduli con lo stesso pattern.
Ambiente
Descrizione
La ricerca globale (barra di ricerca in alto →
ajax_search.php) restituisce un solo risultato per un modulo anche quando i record che combaciano sono molti, se il modulo ha un segmento (o condizione aggiuntiva) con filtro non vuoto in posizioneWHERE. Con la configurazione di default il problema si presenta sempre, perché il segmento predefinito "Tutti" ha filtro1=1.Comportamento atteso
La ricerca globale restituisce tutti i record che combaciano con il termine cercato.
Comportamento osservato
Restituisce esattamente 1 record, qualunque sia il termine. Con un termine che combacia con un solo articolo, quello viene trovato correttamente; con un termine che combacia con più articoli, ne esce sempre e solo il primo secondo l'ordinamento. La risposta AJAX contiene un solo oggetto.
Esempio: cercando
bluet(molti articoli con "Bluetooth" in descrizione) esce un solo articolo; cercandomaxi2(un solo articolo) esce quell'articolo — che però contiene anch'esso "Bluetooth" e quindi doveva comparire anche nella ricercabluet.Causa
In
modules/articoli/ajax/search.phpla query viene chiusa così (righe ~53-55, identiche in 2.9 → 2.11):getAdditionalsQuery()invocata senza parametro$typefonde le condizioniWHReHVNin un'unica stringaAND <condizioni>, che viene concatenata dopo ilGROUP BY. È un errore di posizionamento:WHERE. Una condizione WHERE deve stare prima delGROUP BY; concatenandola dopo, entra a far parte dell'espressione di raggruppamento.GROUP BY \mg_articoli`.`id` AND 1=1viene interpretato da MySQL comeGROUP BY (`id` AND 1=1): poichéidè sempre ≠ 0, l'espressione vale sempre1, tutte le righe finiscono in un unico gruppo e conSELECT *viene restituita **una sola riga**. Nota che basta anche una condizione no-op (1=1`, il filtro del segmento "Tutti") a innescare il collasso — per questo il problema si manifesta praticamente su ogni installazione con un segmento WHERE non vuoto.HAVINGil "dopo ilGROUP BY" sarebbe la regione corretta, quindi la rottura da WHERE mal posizionata non si presenterebbe; resta però che il codice concatenaAND condsenza la keywordHAVING, quindi anche una condizione HVN finirebbe erroneamente dentro l'espressione delGROUP BY.In pratica il
search.phpnon usa il meccanismo di instradamento per posizione che OSM applica già altrove tramiteModules::replaceAdditionals()(placeholder1=1per la WHERE,2=2per la HAVING).Perché il problema è poco evidente
La query mostrata come "Query risultante" nell'editor dei segmenti non è quella eseguita dalla ricerca globale. Sono percorsi diversi:
modules/segmenti/edit.php) e lista/DataTables (src/Util/Query.php,src/API/Controllers/DataTablesController.php) applicano il segmento tramiteModules::replaceAdditionals(), che inserisce le condizioni nei placeholder1=1(WHERE) e2=2(HAVING) della query principale del modulo → SQL corretto. Per questo l'anteprima sembra a posto e il filtro della lista funziona.modules/*/ajax/search.php) non usareplaceAdditionals: costruisce una query separata e concatenagetAdditionalsQuery()dopo ilGROUP BY. È questa la query realmente eseguita, ed è quella malformata.Anteprima corretta, lista funzionante e ricerca globale rotta convivono perché sono tre consumatori con due meccanismi diversi.
Come riprodurre
1=1, posizione WHERE) — configurazione di default.Verifica lato DB — questa query restituisce correttamente tutti i match:
Aggiungendo
AND 1=1dopoGROUP BY a.idil risultato collassa a 1 riga: è la dimostrazione del problema.Moduli coinvolti
Stesso pattern (
getAdditionalsQuery()concatenata dopo ilGROUP BY):modules/articoli/ajax/search.phpmodules/contratti/ajax/search.phpmodules/interventi/ajax/search.phpmodules/preventivi/ajax/search.phpProposta di correzione
Instradare le condizioni per posizione, riusando lo stesso meccanismo della query principale. Soluzione allineata a
replaceAdditionals()(gestisce correttamente sia WHERE che HAVING):In alternativa, come fix minimale che copre il solo caso
WHERE(quello che causa la regressione), spostare solo le condizioni WHR prima delGROUP BYusando il tipo esplicito:Da applicare la stessa correzione agli altri moduli con lo stesso pattern.
Ambiente
2.11-beta