Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.59% covered (success)
98.59%
70 / 71
80.00% covered (success)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Pgsql
98.59% covered (success)
98.59%
70 / 71
80.00% covered (success)
80.00%
4 / 5
23
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 search
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
6
 getMatchingColumnsAsResult
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
5
 getMatchingOrder
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 getMatchingColumns
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3/**
4 * phpMyFAQ PostgreSQL search classes.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2010-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2010-06-06
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Search\Database;
21
22use Exception;
23use Override;
24use phpMyFAQ\Configuration;
25use phpMyFAQ\Search\SearchDatabase;
26
27/**
28 * Class Pgsql
29 *
30 * @package phpMyFAQ\Search\Database
31 * @deprecated Use PDO instead. Will be removed in the v5.0 release.
32 */
33class Pgsql extends SearchDatabase implements DatabaseInterface
34{
35    /**
36     * List of relevance columns that were actually added to the SELECT clause.
37     *
38     * @var string[]
39     */
40    private array $addedRelevanceColumns = [];
41
42    /**
43     * Constructor.
44     */
45    public function __construct(Configuration $configuration)
46    {
47        parent::__construct($configuration);
48        $this->relevanceSupport = true;
49    }
50
51    /**
52     * Prepares the search and executes it.
53     *
54     * @param  string $searchTerm Search term
55     * @throws Exception
56     */
57    #[Override]
58    public function search(string $searchTerm): mixed
59    {
60        if (is_numeric($searchTerm) && (bool) $this->configuration->get(item: 'search.searchForSolutionId')) {
61            return parent::search($searchTerm);
62        }
63
64        $enableRelevance = $this->configuration->get(item: 'search.enableRelevance');
65
66        $columns = $this->getResultColumns();
67        $columns .= $enableRelevance ? $this->getMatchingColumnsAsResult() : '';
68        $orderBy = $enableRelevance ? 'ORDER BY ' . $this->getMatchingOrder() : '';
69
70        $query = sprintf(
71            "
72                SELECT
73                    %s
74                FROM
75                    %s %s %s %s
76                WHERE
77                    (%s) ILIKE ('%%%s%%') ESCAPE '%s'
78                    %s
79                    %s",
80            $columns,
81            $this->getTable(),
82            $this->getJoinedTable(),
83            $this->getJoinedColumns(),
84            $enableRelevance
85                ? ", plainto_tsquery('" . $this->configuration->getDb()->escape($searchTerm) . "') query "
86                : '',
87            $this->getMatchingColumns(),
88            self::escapeLikeWildcards($this->configuration->getDb()->escape($searchTerm)),
89            self::LIKE_ESCAPE_CHARACTER,
90            $this->getConditions(),
91            $orderBy,
92        );
93
94        $this->resultSet = $this->configuration->getDb()->query($query);
95
96        return $this->resultSet;
97    }
98
99    /**
100     * Add the matching columns into the columns for the resultset.
101     */
102    public function getMatchingColumnsAsResult(): string
103    {
104        $resultColumns = '';
105        $config = $this->configuration->get(item: 'search.relevance');
106        $list = explode(',', (string) $config);
107
108        // Set weight
109        $weights = ['A', 'B', 'C', 'D'];
110        $weight = [];
111        foreach ($list as $columnName) {
112            $weight[$columnName] = array_shift($weights);
113        }
114
115        // Reset the list of added columns
116        $this->addedRelevanceColumns = [];
117
118        foreach ($this->matchingColumns as $matchingColumn) {
119            $qualifiedSuffix = strstr($matchingColumn, needle: '.');
120            $columnName = $qualifiedSuffix === false ? $matchingColumn : substr($qualifiedSuffix, offset: 1);
121
122            if (array_key_exists($columnName, $weight)) {
123                $column = sprintf(
124                    "TS_RANK_CD(SETWEIGHT(TO_TSVECTOR(COALESCE(%s, '')), '%s'), query) AS relevance_%s",
125                    $matchingColumn,
126                    $weight[$columnName],
127                    $columnName,
128                );
129
130                $resultColumns .= ', ' . $column;
131                $this->addedRelevanceColumns[] = $columnName;
132            }
133        }
134
135        return $resultColumns;
136    }
137
138    /**
139     * Returns the part of the SQL query with the order by.
140     *
141     * Weight calculates the order depend on the search.relevance order
142     */
143    public function getMatchingOrder(): string
144    {
145        $list = explode(',', (string) $this->configuration->get(item: 'search.relevance'));
146        $order = '';
147
148        foreach ($list as $field) {
149            // Only add to ORDER BY if this relevance column was actually added to SELECT
150            if (!in_array($field, $this->addedRelevanceColumns, strict: true)) {
151                continue;
152            }
153
154            $string = sprintf('relevance_%s DESC', $field);
155            if ($order === '' || $order === '0') {
156                $order .= $string;
157                continue;
158            }
159
160            $order .= ', ' . $string;
161        }
162
163        return $order;
164    }
165
166    /**
167     * Returns the part of the SQL query with the matching columns.
168     */
169    #[Override]
170    public function getMatchingColumns(): string
171    {
172        $enableRelevance = (bool) $this->configuration->get(item: 'search.enableRelevance');
173        $matchColumns = '';
174
175        if ($enableRelevance) {
176            foreach ($this->matchingColumns as $matchingColumn) {
177                $match = sprintf("to_tsvector(coalesce(%s,''))", $matchingColumn);
178                if ($matchColumns === '' || $matchColumns === '0') {
179                    $matchColumns .= '(' . $match;
180                    continue;
181                }
182
183                $matchColumns .= ' || ' . $match;
184            }
185
186            // Add the ILIKE since the FULLTEXT looks for the exact phrase only
187            $matchColumns .= ') @@ query) OR (' . implode(" || ' ' || ", $this->matchingColumns);
188        }
189
190        if (!$enableRelevance) {
191            $matchColumns = implode(" || ' ' || ", $this->matchingColumns);
192        }
193
194        return $matchColumns;
195    }
196}