Skip to content

Commit 4e96693

Browse files
committed
PostgreSQL: Edit and delete rows of a partitioned table
limit1() identifies the row by its ctid, which is unique only within a single relation. A partitioned table is not one relation, so the condition is evaluated in every partition and matches whatever row sits at the same (block, offset) in each of them. Editing or deleting one row of a table with 64 partitions can affect 64 rows. The ctid is used only when the row has no unique key, which is also the case where the damage goes unnoticed - the affected rows are unrelated to the edited one. Matching (tableoid, ctid) restricts the condition to the partition holding the row. tableoid is a system column of every table, so the statement keeps its behaviour on ordinary tables, and tables using INHERITS are fixed as well.
1 parent 0bd9ef7 commit 4e96693

3 files changed

Lines changed: 25 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Cache the assets of the compiled version in a service worker
77
- MySQL: Display the check constraint clause without the extra escaping added by information_schema
88
- MySQL 9: Create routines with LANGUAGE JAVASCRIPT
9+
- PostgreSQL: Edit and delete only the selected row of a partitioned table without a unique key, rows in the other partitions were affected too
910
- MS SQL: Get the base type of a column declared with a user-defined type
1011
- ClickHouse: Print only errors from ClickHouse (GHSA-77qq-q8fv-x45v, regression from 6.0.0)
1112
- ClickHouse: Highlight the queries

adminer/drivers/pgsql.inc.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ function limit(string $query, string $where, int $limit, int $offset = 0, string
490490
function limit1(string $table, string $query, string $where, string $separator = "\n"): string {
491491
return (preg_match('~^INTO~', $query)
492492
? limit($query, $where, 1, 0, $separator)
493-
: " $query" . (is_view(table_status1($table)) ? $where : $separator . "WHERE ctid = (SELECT ctid FROM " . table($table) . $where . $separator . "LIMIT 1)")
493+
: " $query" . (is_view(table_status1($table)) ? $where : $separator . "WHERE (tableoid, ctid) = (SELECT tableoid, ctid FROM " . table($table) . $where . $separator . "LIMIT 1)")
494494
);
495495
}
496496

tests/pgsql.spec.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,29 @@ test('Partitioning', async () => {
588588
await expect(page.locator('body')).toContainText('No tables.');
589589
});
590590

591+
test('Partitioned rows', async () => {
592+
const sql = "CREATE TABLE parts (id int, val text) PARTITION BY LIST (id);"
593+
+ " CREATE TABLE parts_1 PARTITION OF parts FOR VALUES IN (1);"
594+
+ " CREATE TABLE parts_2 PARTITION OF parts FOR VALUES IN (2);"
595+
+ " INSERT INTO parts VALUES (1, 'one'), (2, 'two')"; // both rows are the first one in their partition, so they share the ctid
596+
await goto(page, '/adminer/?pgsql=&username=ODBC&db=adminer_test&ns=public&sql=' + encodeURIComponent(sql));
597+
await button(page, 'Execute').click();
598+
await expect(page.locator('body')).toContainText('Query executed OK');
599+
const select = '/adminer/?pgsql=&username=ODBC&db=adminer_test&ns=public&select=parts&order[0]=id';
600+
await goto(page, select);
601+
await link(page, 'edit').click(); // the table has no unique key, so the row is identified by the ctid, which is unique only within a partition
602+
await page.locator('[name="fields[val]"]').fill('uno');
603+
await button(page, 'Save').click();
604+
await expect(page.locator('body')).toContainText('Item has been updated.');
605+
await goto(page, select);
606+
await expect(page.locator('body')).toContainText('two'); // the row in the other partition must keep its value
607+
await page.locator('input[name="check[]"]').first().click();
608+
await page.locator('[name="delete"]').click();
609+
await expect(page.locator('body')).toContainText('1 item has been affected.');
610+
await goto(page, '/adminer/?pgsql=&username=ODBC&db=adminer_test&ns=public&sql=' + encodeURIComponent('DROP TABLE parts'));
611+
await button(page, 'Execute').click();
612+
});
613+
591614
test('Variables', async () => {
592615
await goto(page, '/adminer/?pgsql=&username=ODBC&variables=');
593616
await expect(page.locator('body')).toContainText('autovacuum');

0 commit comments

Comments
 (0)