Passed
Push — visibilities ( 664baa...e779e2 )
by Matias
04:50
created

PersonMapper::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2018-2021, Matias De lellis <[email protected]>
4
 * @copyright Copyright (c) 2018-2019, Branko Kokanovic <[email protected]>
5
 *
6
 * @author Branko Kokanovic <[email protected]>
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 *
23
 */
24
namespace OCA\FaceRecognition\Db;
25
26
use OC\DB\QueryBuilder\Literal;
0 ignored issues
show
Bug introduced by
The type OC\DB\QueryBuilder\Literal was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
28
use OCP\IDBConnection;
29
use OCP\IUser;
30
31
use OCP\AppFramework\Db\QBMapper;
32
use OCP\AppFramework\Db\DoesNotExistException;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
35
class PersonMapper extends QBMapper {
36
37 1
	public function __construct(IDBConnection $db) {
38 1
		parent::__construct($db, 'facerecog_persons', '\OCA\FaceRecognition\Db\Person');
39 1
	}
40
41
	/**
42
	 * @param string $userId ID of the user
43
	 * @param int $personId ID of the person
44
	 *
45
	 * @return Person
46
	 */
47 8
	public function find(string $userId, int $personId): Person {
48 8
		$qb = $this->db->getQueryBuilder();
49 8
		$qb->select('id', 'name')
50 8
			->from($this->getTableName(), 'p')
51 8
			->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)))
52 8
			->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
53 8
		return $this->findEntity($qb);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->findEntity($qb) returns the type OCP\AppFramework\Db\Entity which includes types incompatible with the type-hinted return OCA\FaceRecognition\Db\Person.
Loading history...
54
	}
55
56
	/**
57
	 * @param string $userId ID of the user
58
	 * @param int $modelId ID of the model
59
	 * @param string $personName name of the person to find
60
	 * @return Person[]
61
	 */
62 2
	public function findByName(string $userId, int $modelId, string $personName): array {
63 2
		$sub = $this->db->getQueryBuilder();
64 2
		$sub->select(new Literal('1'))
65 2
			->from('facerecog_faces', 'f')
66 2
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
67 2
			->where($sub->expr()->eq('p.id', 'f.person'))
68 2
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
69 2
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')))
70 2
			->andWhere($sub->expr()->eq('p.name', $sub->createParameter('person_name')));
71
72 2
		$qb = $this->db->getQueryBuilder();
73 2
		$qb->select('id', 'name', 'is_valid')
74 2
			->from($this->getTableName(), 'p')
75 2
			->where('EXISTS (' . $sub->getSQL() . ')')
76 2
			->setParameter('user_id', $userId)
77 2
			->setParameter('model_id', $modelId)
78 2
			->setParameter('person_name', $personName);
79
80 2
		return $this->findEntities($qb);
81
	}
82
83
	/**
84
	 * @param string $userId ID of the user
85
	 * @param int $modelId ID of the model
86
	 * @return Person[]
87
	 */
88
	public function findUnassigned(string $userId, int $modelId): array {
89
		$sub = $this->db->getQueryBuilder();
90
		$sub->select(new Literal('1'))
91
			->from('facerecog_faces', 'f')
92
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
93
			->where($sub->expr()->eq('p.id', 'f.person'))
94
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
95
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
96
97
		$qb = $this->db->getQueryBuilder();
98
		$qb->select('id', 'is_valid')
99
			->from($this->getTableName(), 'p')
100
			->where('EXISTS (' . $sub->getSQL() . ')')
101
			->andWhere($qb->expr()->eq('is_valid', $qb->createParameter('is_valid')))
102
			->andWhere($qb->expr()->eq('is_visible', $qb->createParameter('is_visible')))
103
			->andWhere($qb->expr()->isNull('name'))
104
			->setParameter('user_id', $userId)
105
			->setParameter('model_id', $modelId)
106
			->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
107
			->setParameter('is_visible', true, IQueryBuilder::PARAM_BOOL);
108
109
		return $this->findEntities($qb);
110
	}
111
112
	/**
113
	 * @param string $userId ID of the user
114
	 * @param int $modelId ID of the model
115
	 * @return Person[]
116
	 */
117 13
	public function findAll(string $userId, int $modelId): array {
118 13
		$sub = $this->db->getQueryBuilder();
119 13
		$sub->select(new Literal('1'))
120 13
			->from('facerecog_faces', 'f')
121 13
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
122 13
			->where($sub->expr()->eq('p.id', 'f.person'))
123 13
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
124 13
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
125
126 13
		$qb = $this->db->getQueryBuilder();
127 13
		$qb->select('id', 'name', 'is_valid')
128 13
			->from($this->getTableName(), 'p')
129 13
			->where('EXISTS (' . $sub->getSQL() . ')')
130 13
			->setParameter('user_id', $userId)
131 13
			->setParameter('model_id', $modelId);
132
133 13
		return $this->findEntities($qb);
134
	}
135
136
	/**
137
	 * @param string $userId ID of the user
138
	 *
139
	 * @return Person[]
140
	 */
141
	public function findDistinctNames(string $userId, int $modelId): array {
142
		$qb = $this->db->getQueryBuilder();
143
		$qb->selectDistinct('name')
144
			->from($this->getTableName(), 'p')
145
			->innerJoin('p', 'facerecog_faces' , 'f', $qb->expr()->eq('f.person', 'p.id'))
146
			->innerJoin('f', 'facerecog_images' ,'i', $qb->expr()->eq('f.image', 'i.id'))
147
			->where($qb->expr()->eq('i.user', $qb->createParameter('user_id')))
148
			->andWhere($qb->expr()->eq('i.model', $qb->createParameter('model_id')))
149
			->andwhere($qb->expr()->isNotNull('p.name'))
150
			->setParameter('user_id', $userId)
151
			->setParameter('model_id', $modelId);
152
		return $this->findEntities($qb);
153
	}
154
155
	/**
156
	 * Search Person by name
157
	 *
158
	 * @param int|null $offset
159
	 * @param int|null $limit
160
	 */
161
	public function findPersonsLike(string $userId, int $modelId, string $name, ?int $offset = null, ?int $limit = null): array {
162
		$qb = $this->db->getQueryBuilder();
163
		$qb->selectDistinct('p.name')
164
			->from($this->getTableName(), 'p')
165
			->innerJoin('p', 'facerecog_faces', 'f', $qb->expr()->eq('f.person', 'p.id'))
166
			->innerJoin('p', 'facerecog_images', 'i', $qb->expr()->eq('f.image', 'i.id'))
167
			->where($qb->expr()->eq('p.user', $qb->createNamedParameter($userId)))
168
			->andWhere($qb->expr()->eq('model', $qb->createNamedParameter($modelId)))
169
			->andWhere($qb->expr()->eq('is_processed', $qb->createNamedParameter(True)))
170
			->andWhere($qb->expr()->like($qb->func()->lower('p.name'), $qb->createParameter('query')));
171
172
		$query = '%' . $this->db->escapeLikeParameter(strtolower($name)) . '%';
173
		$qb->setParameter('query', $query);
174
175
		$qb->setFirstResult($offset);
176
		$qb->setMaxResults($limit);
177
178
		return $this->findEntities($qb);
179
	}
180
181
	/**
182
	 * Returns count of persons (clusters) found for a given user.
183
	 *
184
	 * @param string $userId ID of the user
185
	 * @param int $modelId ID of the model
186
	 * @param bool $onlyInvalid True if client wants count of invalid persons only,
187
	 *  false if client want count of all persons
188
	 * @return int Count of persons
189
	 */
190 15
	public function countPersons(string $userId, int $modelId, bool $onlyInvalid=false): int {
191 15
		$sub = $this->db->getQueryBuilder();
192 15
		$sub->select(new Literal('1'))
193 15
			->from('facerecog_faces', 'f')
194 15
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
195 15
			->where($sub->expr()->eq('p.id', 'f.person'))
196 15
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
197 15
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
198
199 15
		$qb = $this->db->getQueryBuilder();
200 15
		$qb->select($qb->createFunction('COUNT(' . $qb->getColumnName('id') . ')'))
201 15
			->from($this->getTableName(), 'p')
202 15
			->where('EXISTS (' . $sub->getSQL() . ')');
203
204 15
		if ($onlyInvalid) {
205
			$qb = $qb
206
				->andWhere($qb->expr()->eq('is_valid', $qb->createParameter('is_valid')))
207
				->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL);
208
		}
209
210
		$qb = $qb
211 15
			->setParameter('user_id', $userId)
212 15
			->setParameter('model_id', $modelId);
213
214 15
		$resultStatement = $qb->execute();
215 15
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
216 15
		$resultStatement->closeCursor();
217
218 15
		return (int)$data[0];
219
	}
220
221
	/**
222
	 * Based on a given image, takes all faces that belong to that image
223
	 * and invalidates all person that those faces belongs to.
224
	 *
225
	 * @param int $imageId ID of image for which to invalidate persons for
226
	 *
227
	 * @return void
228
	 */
229 12
	public function invalidatePersons(int $imageId): void {
230 12
		$sub = $this->db->getQueryBuilder();
231 12
		$tableNameWithPrefixWithoutQuotes = trim($sub->getTableName($this->getTableName()), '`');
232 12
		$sub->select(new Literal('1'));
233 12
		$sub->from('facerecog_images', 'i')
234 12
			->innerJoin('i', 'facerecog_faces' ,'f', $sub->expr()->eq('i.id', 'f.image'))
235 12
			->where($sub->expr()->eq($tableNameWithPrefixWithoutQuotes . '.id', 'f.person'))
236 12
			->andWhere($sub->expr()->eq('i.id', $sub->createParameter('image_id')));
237
238 12
		$qb = $this->db->getQueryBuilder();
239 12
		$qb->update($this->getTableName())
240 12
			->set("is_valid", $qb->createParameter('is_valid'))
241 12
			->where('EXISTS (' . $sub->getSQL() . ')')
242 12
			->setParameter('image_id', $imageId)
243 12
			->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL)
244 12
			->execute();
245 12
	}
246
247
	/**
248
	 * Based on current clusters and new clusters, do database reconciliation.
249
	 * It tries to do that in minimal number of SQL queries. Operation is atomic.
250
	 *
251
	 * Clusters are array, where keys are ID of persons, and values are indexed arrays
252
	 * with values that are ID of the faces for those persons.
253
	 *
254
	 * @param string $userId ID of the user that clusters belong to
255
	 * @param array $currentClusters Current clusters
256
	 * @param array $newClusters New clusters
257
	 *
258
	 * @return void
259
	 */
260 14
	public function mergeClusterToDatabase(string $userId, $currentClusters, $newClusters): void {
261 14
		$this->db->beginTransaction();
262 14
		$currentDateTime = new \DateTime();
263
264
		try {
265
			// Delete clusters that do not exist anymore
266 14
			foreach($currentClusters as $oldPerson => $oldFaces) {
267 11
				if (array_key_exists($oldPerson, $newClusters)) {
268 6
					continue;
269
				}
270
271
				// OK, we bumped into cluster that existed and now it does not exist.
272
				// We need to remove all references to it and to delete it.
273 7
				foreach ($oldFaces as $oldFace) {
274 7
					$this->updateFace($oldFace, null);
275
				}
276
277
				// todo: this is not very cool. What if user had associated linked user to this. And all lost?
278 7
				$qb = $this->db->getQueryBuilder();
279
				// todo: for extra safety, we should probably add here additional condition, where (user=$userId)
280
				$qb
281 7
					->delete($this->getTableName())
282 7
					->where($qb->expr()->eq('id', $qb->createNamedParameter($oldPerson)))
283 7
					->execute();
284
			}
285
286
			// Modify existing clusters
287 14
			foreach($newClusters as $newPerson=>$newFaces) {
288 12
				if (!array_key_exists($newPerson, $currentClusters)) {
289
					// This cluster didn't exist, there is nothing to modify
290
					// It will be processed during cluster adding operation
291 9
					continue;
292
				}
293
294 6
				$oldFaces = $currentClusters[$newPerson];
295 6
				if ($newFaces === $oldFaces) {
296
					// Set cluster as valid now
297 2
					$qb = $this->db->getQueryBuilder();
298
					$qb
299 2
						->update($this->getTableName())
300 2
						->set("is_valid", $qb->createParameter('is_valid'))
301 2
						->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
302 2
						->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
303 2
						->execute();
304 2
					continue;
305
				}
306
307
				// OK, set of faces do differ. Now, we could potentially go into finer grain details
308
				// and add/remove each individual face, but this seems too detailed. Enough is to
309
				// reset all existing faces to null and to add new faces to new person. That should
310
				// take care of both faces that are removed from cluster, as well as for newly added
311
				// faces to this cluster.
312
313
				// First remove all old faces from any cluster (reset them to null)
314 5
				foreach ($oldFaces as $oldFace) {
315
					// Reset face to null only if it wasn't moved to other cluster!
316
					// (if face is just moved to other cluster, do not reset to null, as some other
317
					// pass for some other cluster will eventually update it to proper cluster)
318 5
					if ($this->isFaceInClusters($oldFace, $newClusters) === false) {
319 1
						$this->updateFace($oldFace, null);
320
					}
321
				}
322
323
				// Then set all new faces to belong to this cluster
324 5
				foreach ($newFaces as $newFace) {
325 5
					$this->updateFace($newFace, $newPerson);
326
				}
327
328
				// Set cluster as valid now
329 5
				$qb = $this->db->getQueryBuilder();
330
				$qb
331 5
					->update($this->getTableName())
332 5
					->set("is_valid", $qb->createParameter('is_valid'))
333 5
					->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
334 5
					->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
335 5
					->execute();
336
			}
337
338
			// Add new clusters
339 14
			foreach($newClusters as $newPerson=>$newFaces) {
340 12
				if (array_key_exists($newPerson, $currentClusters)) {
341
					// This cluster already existed, nothing to add
342
					// It was already processed during modify cluster operation
343 6
					continue;
344
				}
345
346
				// Create new cluster and add all faces to it
347 9
				$qb = $this->db->getQueryBuilder();
348
				$qb
349 9
					->insert($this->getTableName())
350 9
					->values([
351 9
						'user' => $qb->createNamedParameter($userId),
352 9
						'is_valid' => $qb->createNamedParameter(true),
353 9
						'last_generation_time' => $qb->createNamedParameter($currentDateTime, IQueryBuilder::PARAM_DATE),
354 9
						'linked_user' => $qb->createNamedParameter(null)])
355 9
					->execute();
356 9
				$insertedPersonId = $qb->getLastInsertId();
357 9
				foreach ($newFaces as $newFace) {
358 9
					$this->updateFace($newFace, $insertedPersonId);
359
				}
360
			}
361
362 14
			$this->db->commit();
363
		} catch (\Exception $e) {
364
			$this->db->rollBack();
365
			throw $e;
366
		}
367 14
	}
368
369
	/**
370
	 * Deletes all persons from that user.
371
	 *
372
	 * @param string $userId User to drop persons from a table.
373
	 *
374
	 * @return void
375
	 */
376 28
	public function deleteUserPersons(string $userId): void {
377 28
		$qb = $this->db->getQueryBuilder();
378 28
		$qb->delete($this->getTableName())
379 28
			->where($qb->expr()->eq('user', $qb->createNamedParameter($userId)))
380 28
			->execute();
381 28
	}
382
383
	/**
384
	 * Deletes all persons from that user and model
385
	 *
386
	 * @param string $userId ID of user for drop from table
387
	 * @param int $modelId
388
	 *
389
	 * @return void
390
	 */
391
	public function deleteUserModel(string $userId, int $modelId): void {
392
		//TODO: Make it atomic
393
		$qb = $this->db->getQueryBuilder();
394
		$qb->delete($this->getTableName())
395
			->where($qb->expr()->eq('id', $qb->createParameter('person')));
396
397
		$persons = $this->findAll($userId, $modelId);
398
		foreach ($persons as $person) {
399
			$qb->setParameter('person', $person->getId())->execute();
400
		}
401
	}
402
403
	/**
404
	 * Deletes person if it is empty (have no faces associated to it)
405
	 *
406
	 * @param int $personId Person to check if it should be deleted
407
	 *
408
	 * @return void
409
	 */
410
	public function removeIfEmpty(int $personId): void {
411
		$sub = $this->db->getQueryBuilder();
412
		$sub->select(new Literal('1'));
413
		$sub->from('facerecog_faces', 'f')
414
			->where($sub->expr()->eq('f.person', $sub->createParameter('person')));
415
416
		$qb = $this->db->getQueryBuilder();
417
		$qb->delete($this->getTableName())
418
			->where($qb->expr()->eq('id', $qb->createParameter('person')))
419
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
420
			->setParameter('person', $personId)
421
			->execute();
422
	}
423
424
	/**
425
	 * Deletes all persons that have no faces associated to them
426
	 *
427
	 * @param string $userId ID of user for which we are deleting orphaned persons
428
	 */
429 1
	public function deleteOrphaned(string $userId): int {
430 1
		$sub = $this->db->getQueryBuilder();
431 1
		$sub->select(new Literal('1'));
432 1
		$sub->from('facerecog_faces', 'f')
433 1
			->where($sub->expr()->eq('f.person', 'p.id'));
434
435 1
		$qb = $this->db->getQueryBuilder();
436 1
		$qb->select('p.id')
437 1
			->from($this->getTableName(), 'p')
438 1
			->where($qb->expr()->eq('p.user', $qb->createParameter('user')))
439 1
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
440 1
			->setParameter('user', $userId);
441 1
		$orphanedPersons = $this->findEntities($qb);
442
443 1
		$orphaned = 0;
444 1
		foreach ($orphanedPersons as $person) {
445
			$qb = $this->db->getQueryBuilder();
446
			$orphaned += $qb->delete($this->getTableName())
447
				->where($qb->expr()->eq('id', $qb->createNamedParameter($person->id)))
448
				->execute();
449
		}
450 1
		return $orphaned;
451
	}
452
453
	/*
454
	 * Mark the cluster as hidden or visible to user.
455
	 *
456
	 * @param int $personId ID of the person
457
	 * @param bool $visible visibility of the person
458
	 *
459
	 * @return void
460
	 */
461
	public function setVisibility (int $personId, bool $visible): void {
462
		$qb = $this->db->getQueryBuilder();
463
		if ($visible) {
464
			$qb->update($this->getTableName())
465
				->set('is_visible', $qb->createNamedParameter(1))
466
				->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)))
467
				->execute();
468
		} else {
469
			$qb->update($this->getTableName())
470
				->set('is_visible', $qb->createNamedParameter(0))
471
				->set('name', $qb->createNamedParameter(null))
472
				->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)))
473
				->execute();
474
		}
475
	}
476
477
	/*
478
	 * Mark the cluster as hidden or visible to user.
479
	 *
480
	 * @param int $personId ID of the person
481
	 * @param int $faceId visibility of the person
482
	 *
483
	 * @return void
484
	 */
485
	public function detachFace(int $personId, int $faceId, $name = null): void {
486
		// Mark the face as non groupable.
487
		$qb = $this->db->getQueryBuilder();
488
		$qb->update('facerecog_faces')
489
			->set('is_groupable', $qb->createParameter('is_groupable'))
490
			->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
491
			->setParameter('is_groupable', false, IQueryBuilder::PARAM_BOOL)
492
			->execute();
493
494
		if ($this->countClusterFaces($personId) === 1) {
495
			// If cluster is an single face just rename it.
496
			$qb = $this->db->getQueryBuilder();
497
			$qb->update($this->getTableName())
498
				->set('name', $qb->createNamedParameter($name))
499
				->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)))
500
				->execute();
501
		} else {
502
			// If there are other faces, must create a new perso for that face.
503
			$qb = $this->db->getQueryBuilder();
504
			$qb->select('user')
505
				->from($this->getTableName())
506
				->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)));
507
			$oldPerson = $this->findEntity($qb);
508
509
			$qb = $this->db->getQueryBuilder();
510
			$qb->insert($this->getTableName())->values([
511
				'user' => $qb->createNamedParameter($oldPerson->getUser()),
512
				'name' => $qb->createNamedParameter($name),
513
				'is_valid' => $qb->createNamedParameter(true),
514
				'last_generation_time' => $qb->createNamedParameter(new \DateTime(), IQueryBuilder::PARAM_DATE),
515
				'linked_user' => $qb->createNamedParameter(null),
516
				'is_visible' => $qb->createNamedParameter(true)
517
			])->execute();;
518
			$newPersonId = $qb->getLastInsertId();
519
520
			$qb = $this->db->getQueryBuilder();
521
			$qb->update('facerecog_faces')
522
				->set('person', $qb->createParameter('person'))
523
				->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
524
				->setParameter('person', $newPersonId)
525
				->execute();
526
		}
527
	}
528
529
	public function countClusterFaces(int $personId): int {
530
		$qb = $this->db->getQueryBuilder();
531
		$query = $qb
532
			->select($qb->createFunction('COUNT(' . $qb->getColumnName('id') . ')'))
533
			->from('facerecog_faces')
534
			->where($qb->expr()->eq('person', $qb->createParameter('person')))
535
			->setParameter('person', $personId);
536
		$resultStatement = $query->execute();
537
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
538
		$resultStatement->closeCursor();
539
540
		return (int)$data[0];
541
	}
542
543
	/**
544
	 * Updates one face with $faceId to database to person ID $personId.
545
	 *
546
	 * @param int $faceId ID of the face
547
	 * @param int|null $personId ID of the person
548
	 *
549
	 * @return void
550
	 */
551 12
	private function updateFace(int $faceId, $personId): void {
552 12
		$qb = $this->db->getQueryBuilder();
553 12
		$qb->update('facerecog_faces')
554 12
			->set("person", $qb->createNamedParameter($personId))
555 12
			->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
556 12
			->execute();
557 12
	}
558
559
	/**
560
	 * Checks if face with a given ID is in any cluster.
561
	 *
562
	 * @param int $faceId ID of the face to check
563
	 * @param array $cluster All clusters to check into
564
	 *
565
	 * @return bool True if face is found in any cluster, false otherwise.
566
	 */
567 5
	private function isFaceInClusters(int $faceId, array $clusters): bool {
568 5
		foreach ($clusters as $_=>$faces) {
569 5
			if (in_array($faceId, $faces)) {
570 5
				return true;
571
			}
572
		}
573 1
		return false;
574
	}
575
}
576