Skip to content

Commit eda0973

Browse files
thomas-topway-italistair3149translatewiki
authored
Datatables searchpanes structured queries (#986)
* use structured queries * fix phpcs * fix phpcs * remove commented line * Use structured queries for datatables (#961) * Fix various PHPCS errors * use QuerySegmentListProcessor * Create QuerySegmentListProcessor.php * remove comment * remove author * Datatables searchpanes structured queries (#969) * use structured queries * fix phpcs * fix phpcs * remove commented line * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * use QuerySegmentListProcessor * Create QuerySegmentListProcessor.php * remove comment * remove author --------- Co-authored-by: translatewiki.net <[email protected]> * fix phpcs * fix phpcs * Datatables searchpanes structured queries (#970) * use structured queries * fix phpcs * fix phpcs * remove commented line * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * use QuerySegmentListProcessor * Create QuerySegmentListProcessor.php * remove comment * remove author * fix phpcs * fix phpcs --------- Co-authored-by: translatewiki.net <[email protected]> * fix phpcs * fix phpcs * fix phpcs * fix phpcs * fix phpcs * add use SMWDataItem * add remove quotes from $smwtable * add use ISQLPlatform * update subobjects query * sort use statements --------- Co-authored-by: alistair3149 <[email protected]> Co-authored-by: translatewiki.net <[email protected]>
1 parent 493b060 commit eda0973

File tree

2 files changed

+546
-55
lines changed

2 files changed

+546
-55
lines changed
Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
<?php
2+
/**
3+
* adds $this->joinConditions and $this->fromTables
4+
* to the original QuerySegmentListProcessor for the
5+
* use with SearchPanes
6+
*/
7+
namespace SRF\DataTables;
8+
9+
use RuntimeException;
10+
use SMW\MediaWiki\Database;
11+
use SMW\SQLStore\QueryEngine\QuerySegment;
12+
use SMW\SQLStore\TableBuilder\TemporaryTableBuilder;
13+
use SMWQuery as Query;
14+
use Wikimedia\Rdbms\Platform\ISQLPlatform;
15+
16+
class QuerySegmentListProcessor {
17+
/* @var array */
18+
public $joinConditions = [];
19+
20+
/* @var array */
21+
public $fromTables = [];
22+
23+
/**
24+
* @var Database
25+
*/
26+
private $connection;
27+
28+
/**
29+
* @var TemporaryTableBuilder
30+
*/
31+
private $temporaryTableBuilder;
32+
33+
/**
34+
* @var HierarchyTempTableBuilder
35+
*/
36+
private $hierarchyTempTableBuilder;
37+
38+
/**
39+
* Array of arrays of executed queries, indexed by the temporary table names
40+
* results were fed into.
41+
*
42+
* @var array
43+
*/
44+
private $executedQueries = [];
45+
46+
/**
47+
* Query mode copied from given query. Some submethods act differently when
48+
* in Query::MODE_DEBUG.
49+
*
50+
* @var int
51+
*/
52+
private $queryMode;
53+
54+
/**
55+
* @var array
56+
*/
57+
private $querySegmentList = [];
58+
59+
/**
60+
* @param Database $connection
61+
* @param TemporaryTableBuilder $temporaryTableBuilder
62+
* @param HierarchyTempTableBuilder $hierarchyTempTableBuilder
63+
*/
64+
public function __construct( $connection, TemporaryTableBuilder $temporaryTableBuilder, $hierarchyTempTableBuilder ) {
65+
$this->connection = $connection;
66+
$this->temporaryTableBuilder = $temporaryTableBuilder;
67+
$this->hierarchyTempTableBuilder = $hierarchyTempTableBuilder;
68+
}
69+
70+
/**
71+
* @since 2.2
72+
*
73+
* @return array
74+
*/
75+
public function getExecutedQueries() {
76+
return $this->executedQueries;
77+
}
78+
79+
/**
80+
* @since 2.2
81+
*
82+
* @param &$querySegmentList
83+
*/
84+
public function setQuerySegmentList( &$querySegmentList ) {
85+
$this->querySegmentList =& $querySegmentList;
86+
}
87+
88+
/**
89+
* @since 2.2
90+
*
91+
* @param int $queryMode
92+
*/
93+
public function setQueryMode( $queryMode ) {
94+
$this->queryMode = $queryMode;
95+
}
96+
97+
/**
98+
* Process stored queries and change store accordingly. The query obj is modified
99+
* so that it contains non-recursive description of a select to execute for getting
100+
* the actual result.
101+
*
102+
* @param int $id
103+
*
104+
* @throws RuntimeException
105+
*/
106+
public function process( $id ) {
107+
$this->hierarchyTempTableBuilder->emptyHierarchyCache();
108+
$this->executedQueries = [];
109+
110+
// Should never happen
111+
if ( !isset( $this->querySegmentList[$id] ) ) {
112+
throw new RuntimeException( "$id doesn't exist" );
113+
}
114+
115+
$this->segment( $this->querySegmentList[$id] );
116+
}
117+
118+
private function segment( QuerySegment &$query ) {
119+
switch ( $query->type ) {
120+
case QuerySegment::Q_TABLE: // .
121+
$this->table( $query );
122+
break;
123+
case QuerySegment::Q_CONJUNCTION:
124+
$this->conjunction( $query );
125+
break;
126+
case QuerySegment::Q_DISJUNCTION:
127+
$this->disjunction( $query );
128+
break;
129+
case QuerySegment::Q_PROP_HIERARCHY:
130+
case QuerySegment::Q_CLASS_HIERARCHY: // make a saturated hierarchy
131+
$this->hierarchy( $query );
132+
break;
133+
case QuerySegment::Q_VALUE:
134+
break; // nothing to do
135+
}
136+
}
137+
138+
/**
139+
* Resolves normal queries with possible conjunctive subconditions
140+
*/
141+
private function table( QuerySegment &$query ) {
142+
foreach ( $query->components as $qid => $joinField ) {
143+
$subQuery = $this->querySegmentList[$qid];
144+
$this->segment( $subQuery );
145+
$alias = $subQuery->alias;
146+
147+
if ( $subQuery->joinTable !== '' ) { // Join with jointable.joinfield
148+
$op = $subQuery->not ? '!' : '';
149+
150+
$joinType = $subQuery->joinType ? $subQuery->joinType : 'INNER';
151+
$t = $this->connection->tableName( $subQuery->joinTable ) . " AS $subQuery->alias";
152+
// If the alias is the same as the table name and if there is a prefix, MediaWiki does not declare the unprefixed alias
153+
$joinTable = $subQuery->joinTable === $subQuery->alias ? $this->connection->tableName( $subQuery->joinTable ) : $subQuery->joinTable;
154+
155+
if ( $subQuery->from ) {
156+
$t = "($t $subQuery->from)";
157+
$alias = 'nested' . $subQuery->alias;
158+
$query->fromTables[$alias] = array_merge( (array)$subQuery->fromTables, [ $subQuery->alias => $joinTable ] );
159+
$query->joinConditions = array_merge( (array)$query->joinConditions, (array)$subQuery->joinConditions );
160+
161+
} else {
162+
$query->fromTables[$alias] = $joinTable;
163+
}
164+
165+
$query->joinConditions[$alias] = [ $joinType . ' JOIN', "$joinField$op=" . $subQuery->joinfield ];
166+
167+
$this->fromTables[$subQuery->alias] = $joinTable;
168+
169+
ksort( $this->fromTables );
170+
$this->joinConditions[$subQuery->alias] = [ $joinType . ' JOIN', "$joinField$op=" . $subQuery->joinfield ];
171+
172+
$query->from .= " $joinType JOIN $t ON $joinField$op=" . $subQuery->joinfield;
173+
if ( $joinType === 'LEFT' ) {
174+
$query->where .= ( ( $query->where === '' ) ? '' : ' AND ' ) . '(' . $subQuery->joinfield . ' IS NULL)';
175+
}
176+
177+
} elseif ( $subQuery->joinfield !== '' ) { // Require joinfield as "value" via WHERE.
178+
$condition = '';
179+
180+
if ( $subQuery->null === true ) {
181+
$condition .= ( $condition ? ' OR ' : '' ) . "$joinField IS NULL";
182+
} else {
183+
foreach ( $subQuery->joinfield as $value ) {
184+
$op = $subQuery->not ? '!' : '';
185+
$condition .= ( $condition ? ' OR ' : '' ) . "$joinField$op=" . $this->connection->addQuotes( $value );
186+
}
187+
}
188+
189+
if ( count( $subQuery->joinfield ) > 1 ) {
190+
$condition = "($condition)";
191+
}
192+
193+
$query->where .= ( ( $query->where === '' || $subQuery->where === null ) ? '' : ' AND ' ) . $condition;
194+
$query->from .= $subQuery->from;
195+
$query->fromTables = array_merge( (array)$query->fromTables, (array)$subQuery->fromTables );
196+
$query->joinConditions = array_merge( (array)$query->joinConditions, (array)$subQuery->joinConditions );
197+
} else { // interpret empty joinfields as impossible condition (empty result)
198+
$query->joinfield = ''; // make whole query false
199+
$query->joinTable = '';
200+
$query->where = '';
201+
$query->from = '';
202+
$query->fromTables = [];
203+
$query->joinConditions = [];
204+
break;
205+
}
206+
207+
if ( $subQuery->where !== '' && $subQuery->where !== null ) {
208+
if ( $subQuery->joinType === 'LEFT' || $subQuery->joinType == 'LEFT OUTER' ) {
209+
$query->from .= ' AND (' . $subQuery->where . ')';
210+
$query->joinConditions[$alias][1] .= ' AND (' . $subQuery->where . ')';
211+
} else {
212+
$query->where .= ( ( $query->where === '' ) ? '' : ' AND ' ) . '(' . $subQuery->where . ')';
213+
}
214+
}
215+
}
216+
217+
$query->components = [];
218+
}
219+
220+
private function conjunction( QuerySegment &$query ) {
221+
reset( $query->components );
222+
$key = false;
223+
224+
// Pick one subquery as anchor point ...
225+
foreach ( $query->components as $qkey => $qid ) {
226+
$key = $qkey;
227+
228+
if ( $this->querySegmentList[$qkey]->joinTable !== '' ) {
229+
break;
230+
}
231+
}
232+
233+
$result = $this->querySegmentList[$key];
234+
unset( $query->components[$key] );
235+
236+
// Execute it first (may change jointable and joinfield, e.g. when
237+
// making temporary tables)
238+
$this->segment( $result );
239+
240+
// ... and append to this query the remaining queries.
241+
foreach ( $query->components as $qid => $joinfield ) {
242+
$result->components[$qid] = $result->joinfield;
243+
}
244+
245+
// Second execute, now incorporating remaining conditions.
246+
$this->segment( $result );
247+
248+
$query = $result;
249+
}
250+
251+
private function disjunction( QuerySegment &$query ) {
252+
if ( $this->queryMode !== Query::MODE_NONE ) {
253+
$this->temporaryTableBuilder->create( $this->connection->tableName( $query->alias ) );
254+
}
255+
256+
$this->executedQueries[$query->alias] = [];
257+
258+
foreach ( $query->components as $qid => $joinField ) {
259+
$subQuery = $this->querySegmentList[$qid];
260+
$this->segment( $subQuery );
261+
$sql = '';
262+
263+
if ( $subQuery->joinTable !== '' ) {
264+
$sql = 'INSERT ' . 'IGNORE ' . 'INTO ' .
265+
$this->connection->tableName( $query->alias ) .
266+
" SELECT DISTINCT $subQuery->joinfield FROM " . $this->connection->tableName( $subQuery->joinTable ) .
267+
" AS $subQuery->alias $subQuery->from" . ( $subQuery->where ? " WHERE $subQuery->where" : '' );
268+
} elseif ( $subQuery->joinfield !== '' ) {
269+
// NOTE: this works only for single "unconditional" values without further
270+
// WHERE or FROM. The execution must take care of not creating any others.
271+
$values = '';
272+
273+
// This produces an error on postgres with
274+
// pg_query(): Query failed: ERROR: duplicate key value violates
275+
// unique constraint "sunittest_t3_pkey" DETAIL: Key (id)=(274) already exists.
276+
277+
foreach ( $subQuery->joinfield as $value ) {
278+
$values .= ( $values ? ',' : '' ) . '(' . $this->connection->addQuotes( $value ) . ')';
279+
}
280+
281+
$sql = 'INSERT ' . 'IGNORE ' . 'INTO ' . $this->connection->tableName( $query->alias ) . " (id) VALUES $values";
282+
} // else: // interpret empty joinfields as impossible condition (empty result), ignore
283+
284+
if ( $sql ) {
285+
$this->executedQueries[$query->alias][] = $sql;
286+
287+
if ( $this->queryMode !== Query::MODE_NONE ) {
288+
$this->connection->query(
289+
$sql,
290+
__METHOD__,
291+
ISQLPlatform::QUERY_CHANGE_ROWS
292+
);
293+
}
294+
}
295+
}
296+
297+
$query->type = QuerySegment::Q_TABLE;
298+
$query->where = '';
299+
$query->components = [];
300+
301+
$query->joinTable = $query->alias;
302+
$query->joinfield = "$query->alias.id";
303+
$query->sortfields = []; // Make sure we got no sortfields.
304+
305+
// TODO: currently this eliminates sortkeys, possibly keep them (needs
306+
// different temp table format though, maybe not such a good thing to do)
307+
}
308+
309+
/**
310+
* Find subproperties or subcategories. This may require iterative computation,
311+
* and temporary tables are used in many cases.
312+
*
313+
* @param QuerySegment &$query
314+
*/
315+
private function hierarchy( QuerySegment &$query ) {
316+
switch ( $query->type ) {
317+
case QuerySegment::Q_PROP_HIERARCHY:
318+
$type = 'property';
319+
break;
320+
case QuerySegment::Q_CLASS_HIERARCHY:
321+
$type = 'class';
322+
break;
323+
}
324+
325+
[ $smwtable, $depth ] = $this->hierarchyTempTableBuilder->getTableDefinitionByType(
326+
$type
327+
);
328+
329+
// An individual depth was annotated as part of the query
330+
if ( $query->depth !== null ) {
331+
$depth = $query->depth;
332+
}
333+
334+
if ( $depth <= 0 ) { // treat as value, no recursion
335+
$query->type = QuerySegment::Q_VALUE;
336+
return;
337+
}
338+
339+
$values = '';
340+
$valuecond = '';
341+
342+
foreach ( $query->joinfield as $value ) {
343+
$values .= ( $values ? ',' : '' ) . '(' . $this->connection->addQuotes( $value ) . ')';
344+
$valuecond .= ( $valuecond ? ' OR ' : '' ) . 'o_id=' . $this->connection->addQuotes( $value );
345+
}
346+
347+
// Try to save time (SELECT is cheaper than creating/dropping 3 temp tables):
348+
$res = $this->connection->query( "SELECT s_id FROM $smwtable WHERE $valuecond LIMIT 1" );
349+
350+
if ( !$res->fetchObject() ) { // no subobjects, we are done!
351+
$res->free();
352+
$query->type = QuerySegment::Q_VALUE;
353+
return;
354+
}
355+
356+
$res->free();
357+
$tablename = $this->connection->tableName( $query->alias );
358+
$this->executedQueries[$query->alias] = [
359+
"Recursively computed hierarchy for element(s) $values.",
360+
"SELECT s_id FROM $smwtable WHERE $valuecond LIMIT 1"
361+
];
362+
363+
$query->joinTable = $query->alias;
364+
$query->joinfield = "$query->alias.id";
365+
366+
$this->hierarchyTempTableBuilder->fillTempTable(
367+
$type,
368+
$tablename,
369+
$values,
370+
$depth
371+
);
372+
}
373+
374+
/**
375+
* After querying, make sure no temporary database tables are left.
376+
*
377+
* @todo I might be better to keep the tables and possibly reuse them later
378+
* on. Being temporary, the tables will vanish with the session anyway.
379+
*/
380+
public function cleanUp() {
381+
foreach ( $this->executedQueries as $table => $log ) {
382+
$this->temporaryTableBuilder->drop( $this->connection->tableName( $table ) );
383+
}
384+
}
385+
}

0 commit comments

Comments
 (0)