mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-10-27 23:20:05 -05:00
PostgreSQL 18 rejects UNLOGGED on a partitioned table, in both CREATE TABLE and ALTER TABLE ... SET UNLOGGED/LOGGED, so on PG18 and later the Unlogged switch is now disabled for partitioned tables (including a partitioned partition) and the generated CREATE, ALTER and reverse engineered SQL no longer include it. Leaf partitions can still be unlogged, and older servers are unchanged. The Partitions SQL section of a partitioned table's script also now keeps UNLOGGED for partitions that are unlogged, which it previously dropped.
This commit is contained in:
1 parent
6a90e4f837
commit
3ec0aafa36
12 files changed
+365
-1
No files matched your search
@@ -961,6 +961,8 @@ class TableView(BaseTableView, DataTypeReader, SchemaDiffTableCompare):
|
||||
partitions_sql = ''
|
||||
if self.is_table_partitioned(data):
|
||||
data['relkind'] = 'p'
|
||||
if self._unlogged_unsupported(data, self.manager.version):
|
||||
data['relpersistence'] = False
|
||||
# create partition scheme
|
||||
data['partition_scheme'] = self.get_partition_scheme(data)
|
||||
partitions_sql = self.get_partitions_sql(data)
|
||||
|
||||
+14
-1
@@ -141,6 +141,10 @@ export default class PartitionTableSchema extends BaseUISchema {
|
||||
return false;
|
||||
}
|
||||
|
||||
isUnloggedUnsupported(state) {
|
||||
return Boolean(state.is_partitioned) && this.getServerVersion() >= 180000;
|
||||
}
|
||||
|
||||
isPartitioned(state) {
|
||||
if(state.is_partitioned) {
|
||||
return true;
|
||||
@@ -260,7 +264,16 @@ export default class PartitionTableSchema extends BaseUISchema {
|
||||
},{
|
||||
id: 'relpersistence', label: gettext('Unlogged?'), cell: 'switch',
|
||||
type: 'switch', mode: ['properties', 'create', 'edit'],
|
||||
disabled: obj.inSchemaWithModelCheck,
|
||||
deps: ['is_partitioned'],
|
||||
// PostgreSQL 18 and later do not allow a partitioned table to be
|
||||
// unlogged, and that includes a partition that is itself partitioned.
|
||||
disabled: (state) => obj.inSchemaWithModelCheck(state) ||
|
||||
obj.isUnloggedUnsupported(state),
|
||||
depChange: (state) => {
|
||||
if (obj.isUnloggedUnsupported(state) && state.relpersistence) {
|
||||
return {relpersistence: false};
|
||||
}
|
||||
},
|
||||
group: 'advanced',
|
||||
},{
|
||||
id: 'conname', label: gettext('Primary key'), cell: 'text',
|
||||
|
||||
+13
@@ -525,6 +525,10 @@ export default class TableSchema extends BaseUISchema {
|
||||
return this.inCatalog();
|
||||
}
|
||||
|
||||
isUnloggedUnsupported(state) {
|
||||
return Boolean(state.is_partitioned) && this.getServerVersion() >= 180000;
|
||||
}
|
||||
|
||||
changeColumnOptions(columns) {
|
||||
let colOptions = (columns||[]).map(
|
||||
(c) => ({label: c.name, value: c.name, image:'icon-column'})
|
||||
@@ -917,6 +921,15 @@ export default class TableSchema extends BaseUISchema {
|
||||
id: 'relpersistence', label: gettext('Unlogged?'), cell: 'switch',
|
||||
type: 'switch', mode: ['properties', 'create', 'edit'],
|
||||
readonly: obj.inSchemaWithModelCheck,
|
||||
deps: ['is_partitioned'],
|
||||
// PostgreSQL 18 and later do not allow a partitioned table to be
|
||||
// unlogged (its partitions still can be).
|
||||
disabled: (state) => obj.isUnloggedUnsupported(state),
|
||||
depChange: (state) => {
|
||||
if (obj.isUnloggedUnsupported(state) && state.relpersistence) {
|
||||
return {relpersistence: false};
|
||||
}
|
||||
},
|
||||
group: 'advanced',
|
||||
},{
|
||||
id: 'conname', label: gettext('Primary key'), cell: 'text',
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
-- Table: public.unlogged_partitioned_tbl
|
||||
|
||||
-- DROP TABLE IF EXISTS public.unlogged_partitioned_tbl;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.unlogged_partitioned_tbl
|
||||
(
|
||||
id integer,
|
||||
status text COLLATE pg_catalog."default"
|
||||
) PARTITION BY LIST (status);
|
||||
|
||||
ALTER TABLE IF EXISTS public.unlogged_partitioned_tbl
|
||||
OWNER to <OWNER>;
|
||||
|
||||
COMMENT ON TABLE public.unlogged_partitioned_tbl
|
||||
IS 'unlogged partitioned table updated';
|
||||
|
||||
-- Partitions SQL
|
||||
|
||||
CREATE UNLOGGED TABLE public.unlogged_partitioned_tbl_p1 PARTITION OF public.unlogged_partitioned_tbl
|
||||
FOR VALUES IN ('ACTIVE')
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.unlogged_partitioned_tbl_p1
|
||||
OWNER to <OWNER>;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
COMMENT ON TABLE public.unlogged_partitioned_tbl
|
||||
IS 'unlogged partitioned table updated';
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
-- Table: public.unlogged_partitioned_tbl
|
||||
|
||||
-- DROP TABLE IF EXISTS public.unlogged_partitioned_tbl;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.unlogged_partitioned_tbl
|
||||
(
|
||||
id integer,
|
||||
status text COLLATE pg_catalog."default"
|
||||
) PARTITION BY LIST (status);
|
||||
|
||||
ALTER TABLE IF EXISTS public.unlogged_partitioned_tbl
|
||||
OWNER to <OWNER>;
|
||||
|
||||
COMMENT ON TABLE public.unlogged_partitioned_tbl
|
||||
IS 'unlogged partitioned table';
|
||||
|
||||
-- Partitions SQL
|
||||
|
||||
CREATE TABLE public.unlogged_partitioned_tbl_p1 PARTITION OF public.unlogged_partitioned_tbl
|
||||
FOR VALUES IN ('ACTIVE')
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.unlogged_partitioned_tbl_p1
|
||||
OWNER to <OWNER>;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE public.unlogged_partitioned_tbl
|
||||
(
|
||||
id integer,
|
||||
status text
|
||||
) PARTITION BY LIST (status);
|
||||
|
||||
ALTER TABLE IF EXISTS public.unlogged_partitioned_tbl
|
||||
OWNER to <OWNER>;
|
||||
|
||||
COMMENT ON TABLE public.unlogged_partitioned_tbl
|
||||
IS 'unlogged partitioned table';
|
||||
|
||||
CREATE TABLE public.unlogged_partitioned_tbl_p1 PARTITION OF public.unlogged_partitioned_tbl
|
||||
FOR VALUES IN ('ACTIVE');
|
||||
+162
@@ -1676,6 +1676,168 @@
|
||||
"name": "table_with_patition_$%{}[]()&*^!@\"'`\\/#"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "create",
|
||||
"name": "Create Table with list partition and Unlogged set (PG18+ ignores it)",
|
||||
"endpoint": "NODE-table.obj",
|
||||
"sql_endpoint": "NODE-table.sql_id",
|
||||
"msql_endpoint": "NODE-table.msql",
|
||||
"precondition_sql": "SELECT CASE WHEN current_setting('server_version_num')::int >= 180000 THEN 1 ELSE 0 END",
|
||||
"data": {
|
||||
"name": "unlogged_partitioned_tbl",
|
||||
"relowner": "<OWNER>",
|
||||
"relacl": [],
|
||||
"description": "unlogged partitioned table",
|
||||
"coll_inherits": "[]",
|
||||
"hastoasttable": true,
|
||||
"toast_autovacuum_enabled": "x",
|
||||
"autovacuum_enabled": "x",
|
||||
"primary_key": [],
|
||||
"partitions": [
|
||||
{
|
||||
"is_attach": false,
|
||||
"partition_name": "unlogged_partitioned_tbl_p1",
|
||||
"values_in": "'ACTIVE'",
|
||||
"is_sub_partitioned": false,
|
||||
"sub_partition_type": "range",
|
||||
"sub_partition_keys": []
|
||||
}
|
||||
],
|
||||
"partition_type": "list",
|
||||
"is_partitioned": true,
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"cltype": "integer",
|
||||
"attacl": [],
|
||||
"is_primary_key": false,
|
||||
"attnotnull": false,
|
||||
"attlen": null,
|
||||
"attprecision": null,
|
||||
"attidentity": "a",
|
||||
"colconstype": "n",
|
||||
"attoptions": [],
|
||||
"seclabels": []
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"cltype": "text",
|
||||
"attacl": [],
|
||||
"is_primary_key": false,
|
||||
"attnotnull": false,
|
||||
"attlen": null,
|
||||
"attprecision": null,
|
||||
"attidentity": "a",
|
||||
"colconstype": "n",
|
||||
"attoptions": [],
|
||||
"seclabels": []
|
||||
}
|
||||
],
|
||||
"foreign_key": [],
|
||||
"check_constraint": [],
|
||||
"unique_constraint": [],
|
||||
"exclude_constraint": [],
|
||||
"partition_keys": [
|
||||
{
|
||||
"key_type": "column",
|
||||
"pt_column": "status"
|
||||
}
|
||||
],
|
||||
"vacuum_table": [
|
||||
{
|
||||
"name": "autovacuum_analyze_scale_factor"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_analyze_threshold"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_freeze_max_age"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_vacuum_cost_delay"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_vacuum_cost_limit"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_vacuum_scale_factor"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_vacuum_threshold"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_freeze_min_age"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_freeze_table_age"
|
||||
}
|
||||
],
|
||||
"vacuum_toast": [
|
||||
{
|
||||
"name": "autovacuum_freeze_max_age"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_vacuum_cost_delay"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_vacuum_cost_limit"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_vacuum_scale_factor"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_vacuum_threshold"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_freeze_min_age"
|
||||
},
|
||||
{
|
||||
"name": "autovacuum_freeze_table_age"
|
||||
}
|
||||
],
|
||||
"seclabels": [],
|
||||
"forcerlspolicy": false,
|
||||
"like_default_value": false,
|
||||
"like_constraints": false,
|
||||
"like_indexes": false,
|
||||
"like_storage": false,
|
||||
"like_comments": false,
|
||||
"like_identity": false,
|
||||
"like_statistics": false,
|
||||
"like_generated": false,
|
||||
"like_compression": false,
|
||||
"autovacuum_custom": false,
|
||||
"relpersistence": true
|
||||
},
|
||||
"store_object_id": true,
|
||||
"expected_sql_file": "create_unlogged_partitioned_table.sql",
|
||||
"expected_msql_file": "create_unlogged_partitioned_table_msql.sql"
|
||||
},
|
||||
{
|
||||
"type": "alter",
|
||||
"name": "Alter partitioned Table to set Unlogged (PG18+ ignores it) with an unlogged partition",
|
||||
"endpoint": "NODE-table.obj_id",
|
||||
"sql_endpoint": "NODE-table.sql_id",
|
||||
"msql_endpoint": "NODE-table.msql_id",
|
||||
"precondition_sql": "SELECT CASE WHEN current_setting('server_version_num')::int >= 180000 THEN 1 ELSE 0 END",
|
||||
"pre_scenario_sql": "ALTER TABLE public.unlogged_partitioned_tbl_p1 SET UNLOGGED;",
|
||||
"data": {
|
||||
"relpersistence": true,
|
||||
"description": "unlogged partitioned table updated"
|
||||
},
|
||||
"expected_sql_file": "alter_unlogged_partitioned_table.sql",
|
||||
"expected_msql_file": "alter_unlogged_partitioned_table_msql.sql"
|
||||
},
|
||||
{
|
||||
"type": "delete",
|
||||
"name": "Delete Table with list partition and Unlogged set",
|
||||
"endpoint": "NODE-table.obj_id",
|
||||
"precondition_sql": "SELECT CASE WHEN current_setting('server_version_num')::int >= 180000 THEN 1 ELSE 0 END",
|
||||
"data": {
|
||||
"name": "unlogged_partitioned_tbl"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "create",
|
||||
"name": "Create Table with range partition",
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Unit tests for BaseTableView._unlogged_unsupported(), which stops
|
||||
UNLOGGED being generated for a partitioned table on PostgreSQL 18 and
|
||||
later, where the server rejects it (#9057).
|
||||
"""
|
||||
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.tables.utils \
|
||||
import BaseTableView
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
|
||||
|
||||
class TestUnloggedUnsupported(BaseTestGenerator):
|
||||
"""Unit tests for BaseTableView._unlogged_unsupported()."""
|
||||
|
||||
scenarios = [
|
||||
('Partitioned table on PostgreSQL 18',
|
||||
dict(data={'is_partitioned': True}, version=180000,
|
||||
expected=True)),
|
||||
('Partitioned table on a later release',
|
||||
dict(data={'is_partitioned': True}, version=190000,
|
||||
expected=True)),
|
||||
('Partitioned table on PostgreSQL 17',
|
||||
dict(data={'is_partitioned': True}, version=170000,
|
||||
expected=False)),
|
||||
('Leaf partition or plain table on PostgreSQL 18',
|
||||
dict(data={'is_partitioned': False}, version=180000,
|
||||
expected=False)),
|
||||
('Table data without is_partitioned on PostgreSQL 18',
|
||||
dict(data={}, version=180000, expected=False)),
|
||||
]
|
||||
|
||||
def runTest(self):
|
||||
self.assertEqual(
|
||||
BaseTableView._unlogged_unsupported(self.data, self.version),
|
||||
self.expected)
|
||||
@@ -679,6 +679,16 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
|
||||
c['cltype'], c['hasSqrBracket'] = \
|
||||
column_utils.type_formatter(c['cltype'])
|
||||
|
||||
@staticmethod
|
||||
def _unlogged_unsupported(data, server_version):
|
||||
"""
|
||||
PostgreSQL 18 and later reject UNLOGGED on a partitioned table,
|
||||
both in CREATE TABLE and in ALTER TABLE ... SET UNLOGGED/LOGGED,
|
||||
whereas a leaf partition may still be unlogged.
|
||||
"""
|
||||
return server_version >= 180000 and \
|
||||
bool(data.get('is_partitioned', False))
|
||||
|
||||
def _get_resql_for_table(self, did, scid, tid, data, json_resp, main_sql,
|
||||
add_not_exists_clause=False):
|
||||
"""
|
||||
@@ -718,6 +728,9 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
|
||||
driver.qtIdent(None, acl['grantee'])
|
||||
if acl['grantee'] != 'PUBLIC' else 'PUBLIC')
|
||||
|
||||
if self._unlogged_unsupported(data, self.manager.version):
|
||||
data['relpersistence'] = False
|
||||
|
||||
# if table is partitions then
|
||||
if 'relispartition' in data and data['relispartition']:
|
||||
table_sql = \
|
||||
@@ -929,6 +942,9 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
|
||||
part_data['name'] = row['name']
|
||||
part_data['partition_value'] = row['partition_value']
|
||||
part_data['is_partitioned'] = row['is_partitioned']
|
||||
part_data['relpersistence'] = \
|
||||
row['relpersistence'] and not self._unlogged_unsupported(
|
||||
row, self.manager.version)
|
||||
part_data['partition_scheme'] = row['partition_scheme']
|
||||
part_data['description'] = row['description']
|
||||
part_data['relowner'] = row['relowner']
|
||||
@@ -1554,6 +1570,8 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
|
||||
partitions_sql = ''
|
||||
if 'is_partitioned' in data and data['is_partitioned']:
|
||||
data['relkind'] = 'p'
|
||||
if self._unlogged_unsupported(data, self.manager.version):
|
||||
data['relpersistence'] = False
|
||||
# create partition scheme
|
||||
data['partition_scheme'] = self.get_partition_scheme(data)
|
||||
partitions_sql = self.get_partitions_sql(data)
|
||||
@@ -1616,6 +1634,9 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
|
||||
# Update the vacuum toast table settings.
|
||||
self.update_vacuum_settings('vacuum_toast', old_data, data)
|
||||
|
||||
if self._unlogged_unsupported(old_data, self.manager.version):
|
||||
data.pop('relpersistence', None)
|
||||
|
||||
sql = render_template(
|
||||
"/".join([self.table_template_path, self._UPDATE_SQL]),
|
||||
o_data=old_data, data=data, conn=self.conn
|
||||
|
||||
@@ -84,6 +84,32 @@ describe('PartitionTableSchema', ()=>{
|
||||
});
|
||||
});
|
||||
|
||||
it('relpersistence on a partitioned partition', () => {
|
||||
const field = _.find(schemaObj.fields, (f)=>f.id=='relpersistence');
|
||||
const verSpy = jest.spyOn(schemaObj, 'getServerVersion');
|
||||
const newSpy = jest.spyOn(schemaObj, 'isNew').mockReturnValue(true);
|
||||
|
||||
// PostgreSQL 18 and later reject UNLOGGED on a partitioned table.
|
||||
verSpy.mockReturnValue(180000);
|
||||
expect(field.disabled({is_partitioned: true})).toBe(true);
|
||||
expect(field.depChange({is_partitioned: true, relpersistence: true})).toEqual({
|
||||
relpersistence: false,
|
||||
});
|
||||
expect(field.disabled({is_partitioned: false})).toBe(false);
|
||||
expect(field.depChange({is_partitioned: false, relpersistence: true})).toBeUndefined();
|
||||
|
||||
// Older servers still accept it.
|
||||
verSpy.mockReturnValue(170000);
|
||||
expect(field.disabled({is_partitioned: true})).toBe(false);
|
||||
expect(field.depChange({is_partitioned: true, relpersistence: true})).toBeUndefined();
|
||||
|
||||
// An existing partition stays read-only, as before.
|
||||
newSpy.mockReturnValue(false);
|
||||
expect(field.disabled({is_partitioned: false})).toBe(true);
|
||||
verSpy.mockRestore();
|
||||
newSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('validate', ()=>{
|
||||
let state = {is_partitioned: true};
|
||||
let setError = jest.fn();
|
||||
|
||||
@@ -304,6 +304,26 @@ describe('TableSchema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('relpersistence on a partitioned table', () => {
|
||||
const field = _.find(schemaObj.fields, (f)=>f.id=='relpersistence');
|
||||
const spy = jest.spyOn(schemaObj, 'getServerVersion');
|
||||
|
||||
// PostgreSQL 18 and later reject UNLOGGED on a partitioned table.
|
||||
spy.mockReturnValue(180000);
|
||||
expect(field.disabled({is_partitioned: true})).toBe(true);
|
||||
expect(field.depChange({is_partitioned: true, relpersistence: true})).toEqual({
|
||||
relpersistence: false,
|
||||
});
|
||||
expect(field.disabled({is_partitioned: false})).toBe(false);
|
||||
expect(field.depChange({is_partitioned: false, relpersistence: true})).toBeUndefined();
|
||||
|
||||
// Older servers still accept it.
|
||||
spy.mockReturnValue(170000);
|
||||
expect(field.disabled({is_partitioned: true})).toBe(false);
|
||||
expect(field.depChange({is_partitioned: true, relpersistence: true})).toBeUndefined();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('depChange column rename propagation', () => {
|
||||
jest.spyOn(schemaObj, 'getServerVersion').mockReturnValue(110000);
|
||||
schemaObj.constraintsObj.top = schemaObj;
|
||||
|
||||
Reference in new issue
Block a user