Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5e8c964

Browse files
authored
Stable IDs during SQL import (#7988)
* Stable IDs during SQL import Follow-up of #7949 Make sure that the original category IDs, feed IDs, and label IDs are kept identical during an SQL import. Avoid breaking everything referring to categories, feeds, labels by their IDs such as searches and third-party extensions. * Fix export of default category
1 parent fdbdd11 commit 5e8c964

12 files changed

+158
-55
lines changed

app/Models/CategoryDAO.php

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
55

66
public const DEFAULTCATEGORYID = 1;
77

8+
public function sqlResetSequence(): bool {
9+
return true; // Nothing to do for MySQL
10+
}
11+
812
public function resetDefaultCategoryName(): bool {
913
//FreshRSS 1.15.1
1014
$stm = $this->pdo->prepare('UPDATE `_category` SET name = :name WHERE id = :id');
@@ -101,32 +105,49 @@ protected function autoUpdateDb(array $errorInfo): bool {
101105
}
102106

103107
/**
104-
* @param array{name:string,id?:int,kind?:int,lastUpdate?:int,error?:int|bool,attributes?:string|array<string,mixed>} $valuesTmp
108+
* @param array{id?:int,name:string,kind?:int,lastUpdate?:int,error?:int|bool,attributes?:string|array<string,mixed>} $valuesTmp
105109
*/
106110
public function addCategory(array $valuesTmp): int|false {
107-
// TRIM() to provide a type hint as text
111+
if (empty($valuesTmp['id'])) { // Auto-generated ID
112+
$sql = <<<'SQL'
113+
INSERT INTO `_category`(name, kind, attributes)
114+
SELECT * FROM (SELECT :name1 AS name, 1*:kind AS kind, :attributes AS attributes) c2
115+
SQL;
116+
} else {
117+
$sql = <<<'SQL'
118+
INSERT INTO `_category`(id, name, kind, attributes)
119+
SELECT * FROM (SELECT 1*:id AS id, :name1 AS name, 1*:kind AS kind, :attributes AS attributes) c2
120+
SQL;
121+
}
108122
// No tag of the same name
109-
$sql = <<<'SQL'
110-
INSERT INTO `_category`(kind, name, attributes)
111-
SELECT * FROM (SELECT ABS(?) AS kind, TRIM(?) AS name, TRIM(?) AS attributes) c2
112-
WHERE NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = TRIM(?))
123+
$sql .= "\n" . <<<'SQL'
124+
WHERE NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = :name2)
113125
SQL;
114126
$stm = $this->pdo->prepare($sql);
115127

116128
$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
117129
if (!isset($valuesTmp['attributes'])) {
118130
$valuesTmp['attributes'] = [];
119131
}
120-
$values = [
121-
$valuesTmp['kind'] ?? FreshRSS_Category::KIND_NORMAL,
122-
$valuesTmp['name'],
123-
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
124-
$valuesTmp['name'],
125-
];
126-
127-
if ($stm !== false && $stm->execute($values) && $stm->rowCount() > 0) {
128-
$catId = $this->pdo->lastInsertId('`_category_id_seq`');
129-
return $catId === false ? false : (int)$catId;
132+
if ($stm !== false) {
133+
if (!empty($valuesTmp['id'])) {
134+
$stm->bindValue(':id', $valuesTmp['id'], PDO::PARAM_INT);
135+
}
136+
$stm->bindValue(':name1', $valuesTmp['name'], PDO::PARAM_STR);
137+
$stm->bindValue(':kind', $valuesTmp['kind'] ?? FreshRSS_Category::KIND_NORMAL, PDO::PARAM_INT);
138+
$attributes = is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] :
139+
json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
140+
$stm->bindValue(':attributes', $attributes, PDO::PARAM_STR);
141+
$stm->bindValue(':name2', $valuesTmp['name'], PDO::PARAM_STR);
142+
}
143+
if ($stm !== false && $stm->execute() && $stm->rowCount() > 0) {
144+
if (empty($valuesTmp['id'])) {
145+
// Auto-generated ID
146+
$catId = $this->pdo->lastInsertId('`_category_id_seq`');
147+
return $catId === false ? false : (int)$catId;
148+
}
149+
$this->sqlResetSequence();
150+
return $valuesTmp['id'];
130151
} else {
131152
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
132153
/** @var array{0:string,1:int,2:string} $info */
@@ -207,9 +228,6 @@ public function updateLastUpdate(int $id, bool $inError = false, int $mtime = 0)
207228
}
208229

209230
public function deleteCategory(int $id): int|false {
210-
if ($id <= self::DEFAULTCATEGORYID) {
211-
return false;
212-
}
213231
$sql = 'DELETE FROM `_category` WHERE id=:id';
214232
$stm = $this->pdo->prepare($sql);
215233
if ($stm !== false && $stm->bindParam(':id', $id, PDO::PARAM_INT) && $stm->execute()) {
@@ -355,10 +373,6 @@ public function checkDefault(): int|bool {
355373
$cat = new FreshRSS_Category(_t('gen.short.default_category'), self::DEFAULTCATEGORYID);
356374

357375
$sql = 'INSERT INTO `_category`(id, name) VALUES(?, ?)';
358-
if ($this->pdo->dbType() === 'pgsql') {
359-
//Force call to nextval()
360-
$sql .= " RETURNING nextval('`_category_id_seq`');";
361-
}
362376
$stm = $this->pdo->prepare($sql);
363377

364378
$values = [
@@ -368,6 +382,7 @@ public function checkDefault(): int|bool {
368382

369383
if ($stm !== false && $stm->execute($values)) {
370384
$catId = $this->pdo->lastInsertId('`_category_id_seq`');
385+
$this->sqlResetSequence();
371386
return $catId === false ? false : (int)$catId;
372387
} else {
373388
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();

app/Models/CategoryDAOPGSQL.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
final class FreshRSS_CategoryDAOPGSQL extends FreshRSS_CategoryDAO {
5+
6+
#[\Override]
7+
public function sqlResetSequence(): bool {
8+
$sql = <<<'SQL'
9+
SELECT setval('`_category_id_seq`', COALESCE(MAX(id), 0) + 1, false) FROM `_category`
10+
SQL;
11+
return $this->pdo->exec($sql) !== false;
12+
}
13+
}

app/Models/CategoryDAOSQLite.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO {
55

6+
#[\Override]
7+
public function sqlResetSequence(): bool {
8+
return true; // Nothing to do for SQLite
9+
}
10+
611
/** @param array{0:string,1:int,2:string} $errorInfo */
712
#[\Override]
813
protected function autoUpdateDb(array $errorInfo): bool {

app/Models/DatabaseDAO.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -409,19 +409,17 @@ public function dbCopy(string $filename, int $mode, bool $clearFirst = false, bo
409409
$userTo->createUser();
410410

411411
$catTo->beginTransaction();
412+
$catTo->deleteCategory(FreshRSS_CategoryDAO::DEFAULTCATEGORYID);
413+
$catTo->sqlResetSequence();
412414
foreach ($catFrom->selectAll() as $category) {
413-
$cat = $catTo->searchByName($category['name']); //Useful for the default category
414-
if ($cat != null) {
415-
$catId = $cat->id();
416-
} else {
417-
$catId = $catTo->addCategory($category);
418-
if ($catId == false) {
419-
$error = 'Error during SQLite copy of categories!';
420-
return self::stdError($error);
421-
}
415+
$catId = $catTo->addCategory($category);
416+
if ($catId === false) {
417+
$error = 'Error during SQLite copy of categories!';
418+
return self::stdError($error);
422419
}
423420
$idMaps['c' . $category['id']] = $catId;
424421
}
422+
$catTo->sqlResetSequence();
425423
foreach ($feedFrom->selectAll() as $feed) {
426424
$feed['category'] = empty($idMaps['c' . $feed['category']]) ? FreshRSS_CategoryDAO::DEFAULTCATEGORYID : $idMaps['c' . $feed['category']];
427425
$feedId = $feedTo->addFeed($feed);
@@ -431,6 +429,7 @@ public function dbCopy(string $filename, int $mode, bool $clearFirst = false, bo
431429
}
432430
$idMaps['f' . $feed['id']] = $feedId;
433431
}
432+
$feedTo->sqlResetSequence();
434433
$catTo->commit();
435434

436435
$nbEntries = $entryFrom->count();
@@ -483,6 +482,7 @@ public function dbCopy(string $filename, int $mode, bool $clearFirst = false, bo
483482
}
484483
}
485484
}
485+
$tagTo->sqlResetSequence();
486486
$tagTo->commit();
487487

488488
return true;

app/Models/Factory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public static function createUserDao(?string $username = null): FreshRSS_UserDAO
1616
public static function createCategoryDao(?string $username = null): FreshRSS_CategoryDAO {
1717
return match (FreshRSS_Context::systemConf()->db['type'] ?? '') {
1818
'sqlite' => new FreshRSS_CategoryDAOSQLite($username),
19+
'pgsql' => new FreshRSS_CategoryDAOPGSQL($username),
1920
default => new FreshRSS_CategoryDAO($username),
2021
};
2122
}
@@ -26,6 +27,7 @@ public static function createCategoryDao(?string $username = null): FreshRSS_Cat
2627
public static function createFeedDao(?string $username = null): FreshRSS_FeedDAO {
2728
return match (FreshRSS_Context::systemConf()->db['type'] ?? '') {
2829
'sqlite' => new FreshRSS_FeedDAOSQLite($username),
30+
'pgsql' => new FreshRSS_FeedDAOPGSQL($username),
2931
default => new FreshRSS_FeedDAO($username),
3032
};
3133
}

app/Models/FeedDAO.php

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
class FreshRSS_FeedDAO extends Minz_ModelPdo {
55

6+
public function sqlResetSequence(): bool {
7+
return true; // Nothing to do for MySQL
8+
}
9+
610
protected function addColumn(string $name): bool {
711
if ($this->pdo->inTransaction()) {
812
$this->pdo->commit();
@@ -34,12 +38,21 @@ protected function autoUpdateDb(array $errorInfo): bool {
3438
}
3539

3640
/**
37-
* @param array{url:string,kind:int,category:int,name:string,website:string,description:string,lastUpdate:int,priority?:int,
41+
* @param array{id?:int,url:string,kind:int,category:int,name:string,website:string,description:string,lastUpdate:int,priority?:int,
3842
* pathEntries?:string,httpAuth:string,error:int|bool,ttl?:int,attributes?:string|array<string|mixed>} $valuesTmp
3943
*/
4044
public function addFeed(array $valuesTmp): int|false {
41-
$sql = 'INSERT INTO `_feed` (url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes)
42-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
45+
if (empty($valuesTmp['id'])) { // Auto-generated ID
46+
$sql = <<<'SQL'
47+
INSERT INTO `_feed` (url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes)
48+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
49+
SQL;
50+
} else {
51+
$sql = <<<'SQL'
52+
INSERT INTO `_feed` (id, url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes)
53+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
54+
SQL;
55+
}
4356
$stm = $this->pdo->prepare($sql);
4457

4558
$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
@@ -51,7 +64,8 @@ public function addFeed(array $valuesTmp): int|false {
5164
$valuesTmp['attributes'] = [];
5265
}
5366

54-
$values = [
67+
$values = empty($valuesTmp['id']) ? [] : [$valuesTmp['id']];
68+
$values = array_merge($values, [
5569
$valuesTmp['url'],
5670
$valuesTmp['kind'] ?? FreshRSS_Feed::KIND_RSS,
5771
$valuesTmp['category'],
@@ -65,11 +79,16 @@ public function addFeed(array $valuesTmp): int|false {
6579
isset($valuesTmp['error']) ? (int)$valuesTmp['error'] : 0,
6680
isset($valuesTmp['ttl']) ? (int)$valuesTmp['ttl'] : FreshRSS_Feed::TTL_DEFAULT,
6781
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
68-
];
82+
]);
6983

7084
if ($stm !== false && $stm->execute($values)) {
71-
$feedId = $this->pdo->lastInsertId('`_feed_id_seq`');
72-
return $feedId === false ? false : (int)$feedId;
85+
if (empty($valuesTmp['id'])) {
86+
// Auto-generated ID
87+
$feedId = $this->pdo->lastInsertId('`_feed_id_seq`');
88+
return $feedId === false ? false : (int)$feedId;
89+
}
90+
$this->sqlResetSequence();
91+
return $valuesTmp['id'];
7392
} else {
7493
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
7594
/** @var array{0:string,1:int,2:string} $info */

app/Models/FeedDAOPGSQL.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
class FreshRSS_FeedDAOPGSQL extends FreshRSS_FeedDAO {
5+
6+
#[\Override]
7+
public function sqlResetSequence(): bool {
8+
$sql = <<<'SQL'
9+
SELECT setval('`_feed_id_seq`', COALESCE(MAX(id), 0) + 1, false) FROM `_feed`
10+
SQL;
11+
return $this->pdo->exec($sql) !== false;
12+
}
13+
}

app/Models/FeedDAOSQLite.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
55

6+
#[\Override]
7+
public function sqlResetSequence(): bool {
8+
return true; // Nothing to do for SQLite
9+
}
10+
611
/** @param array{0:string,1:int,2:string} $errorInfo */
712
#[\Override]
813
protected function autoUpdateDb(array $errorInfo): bool {

app/Models/TagDAO.php

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,50 @@ public function sqlIgnore(): string {
77
return 'IGNORE';
88
}
99

10+
public function sqlResetSequence(): bool {
11+
return true; // Nothing to do for MySQL
12+
}
13+
1014
/**
11-
* @param array{'id'?:int,'name':string,'attributes'?:array<string,mixed>} $valuesTmp
15+
* @param array{id?:int,name:string,attributes?:array<string,mixed>} $valuesTmp
1216
*/
1317
public function addTag(array $valuesTmp): int|false {
14-
// TRIM() gives a text type hint to PostgreSQL
15-
// No category of the same name
16-
$sql = <<<'SQL'
18+
if (empty($valuesTmp['id'])) { // Auto-generated ID
19+
$sql = <<<'SQL'
1720
INSERT INTO `_tag`(name, attributes)
18-
SELECT * FROM (SELECT TRIM(?) as name, TRIM(?) as attributes) t2
19-
WHERE NOT EXISTS (SELECT 1 FROM `_category` WHERE name = TRIM(?))
21+
SELECT * FROM (SELECT :name1 AS name, :attributes AS attributes) t2
22+
SQL;
23+
} else {
24+
$sql = <<<'SQL'
25+
INSERT INTO `_tag`(id, name, attributes)
26+
SELECT * FROM (SELECT 1*:id AS id, :name1 AS name, :attributes AS attributes) t2
27+
SQL;
28+
}
29+
// No category of the same name
30+
$sql .= "\n" . <<<'SQL'
31+
WHERE NOT EXISTS (SELECT 1 FROM `_category` WHERE name = :name2)
2032
SQL;
2133
$stm = $this->pdo->prepare($sql);
2234

2335
$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
2436
if (!isset($valuesTmp['attributes'])) {
2537
$valuesTmp['attributes'] = [];
2638
}
27-
$values = [
28-
$valuesTmp['name'],
29-
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
30-
$valuesTmp['name'],
31-
];
32-
33-
if ($stm !== false && $stm->execute($values) && $stm->rowCount() > 0) {
34-
$tagId = $this->pdo->lastInsertId('`_tag_id_seq`');
35-
return $tagId === false ? false : (int)$tagId;
39+
if ($stm !== false) {
40+
$stm->bindValue(':id', empty($valuesTmp['id']) ? null : $valuesTmp['id'], PDO::PARAM_INT);
41+
$stm->bindValue(':name1', $valuesTmp['name'], PDO::PARAM_STR);
42+
$stm->bindValue(':name2', $valuesTmp['name'], PDO::PARAM_STR);
43+
$stm->bindValue(':attributes', is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] :
44+
json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), PDO::PARAM_STR);
45+
}
46+
if ($stm !== false && $stm->execute() && $stm->rowCount() > 0) {
47+
if (empty($valuesTmp['id'])) {
48+
// Auto-generated ID
49+
$tagId = $this->pdo->lastInsertId('`_tag_id_seq`');
50+
return $tagId === false ? false : (int)$tagId;
51+
}
52+
$this->sqlResetSequence();
53+
return $valuesTmp['id'];
3654
} else {
3755
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
3856
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));

app/Models/TagDAOPGSQL.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,12 @@ class FreshRSS_TagDAOPGSQL extends FreshRSS_TagDAO {
77
public function sqlIgnore(): string {
88
return ''; //TODO
99
}
10+
11+
#[\Override]
12+
public function sqlResetSequence(): bool {
13+
$sql = <<<'SQL'
14+
SELECT setval('`_tag_id_seq`', COALESCE(MAX(id), 0) + 1, false) FROM `_tag`
15+
SQL;
16+
return $this->pdo->exec($sql) !== false;
17+
}
1018
}

0 commit comments

Comments
 (0)