Manage PostgreSQL schema

Replace usage of pg_catalog tables by information_schema to take care of user
access.
Ensure to fetch the columns information from the right schema.
Prevent to update default column values if it did not change.
Comment table and column only if the current user is the owner.

issue4924
review24291002
This commit is contained in:
Cédric Krier
2015-09-04 11:16:09 +02:00
parent 14d5de6ac9
commit 109d0ef64f
3 changed files with 117 additions and 112 deletions
+1
View File
@@ -1,3 +1,4 @@
* Manage PostgreSQL schema
* Remove colors attribute on tree view
* Remove style on Report
* Add StateReport to Wizard
+24 -37
View File
@@ -5,8 +5,6 @@ import logging
import re
import os
import urllib
if os.name == 'posix':
import pwd
from decimal import Decimal
try:
@@ -202,27 +200,8 @@ class Database(DatabaseInterface):
res = Database._list_cache
if res and abs(Database._list_cache_timestamp - now) < timeout:
return res
uri = parse_uri(config.get('database', 'uri'))
db_user = uri.username or os.environ.get('PGUSER')
if not db_user and os.name == 'posix':
db_user = pwd.getpwuid(os.getuid())[0]
if db_user:
cursor.execute("SELECT datname "
"FROM pg_database "
"WHERE datdba = ("
"SELECT usesysid "
"FROM pg_user "
"WHERE usename=%s) "
"AND datname not in "
"('template0', 'template1', 'postgres') "
"ORDER BY datname",
(db_user,))
else:
cursor.execute("SELECT datname "
"FROM pg_database "
"WHERE datname not in "
"('template0', 'template1','postgres') "
"ORDER BY datname")
cursor.execute('SELECT datname FROM pg_database '
'WHERE datistemplate = false ORDER BY datname')
res = []
for db_name, in cursor.fetchall():
db_name = db_name.encode('utf-8')
@@ -275,6 +254,8 @@ class Cursor(CursorInterface):
self._connpool = connpool
self._conn = conn
self._database = database
self._current_user = None
self._search_path = None
self.cursor = conn.cursor()
self.commit()
self.sql_from_log = {}
@@ -316,20 +297,11 @@ class Cursor(CursorInterface):
self._conn.rollback()
def test(self):
self.cursor.execute("SELECT relname "
"FROM pg_class "
"WHERE relkind = 'r' AND relname in ("
"'ir_model', "
"'ir_model_field', "
"'ir_ui_view', "
"'ir_ui_menu', "
"'res_user', "
"'res_group', "
"'ir_module', "
"'ir_module_dependency', "
"'ir_translation', "
"'ir_lang'"
")")
self.cursor.execute('SELECT 1 FROM information_schema.tables '
'WHERE table_name IN %s',
(('ir_model', 'ir_model_field', 'ir_ui_view', 'ir_ui_menu',
'res_user', 'res_group', 'ir_module',
'ir_module_dependency', 'ir_translation', 'ir_lang'),))
return len(self.cursor.fetchall()) != 0
def nextid(self, table):
@@ -364,6 +336,21 @@ class Cursor(CursorInterface):
def has_multirow_insert(self):
return True
@property
def current_user(self):
if self._current_user is None:
self.execute('SELECT current_user')
self._current_user, = self.fetchone()
return self._current_user
@property
def search_path(self):
if self._search_path is None:
self.execute('SHOW search_path')
self._search_path = self.fetchone()[0].replace(
'"$user"', self.current_user).split(',')
return self._search_path
register_type(UNICODE)
if PYDATE:
register_type(PYDATE)
+92 -75
View File
@@ -27,8 +27,14 @@ class TableHandler(TableHandlerInterface):
# Create new table if necessary
if not self.table_exist(self.cursor, self.table_name):
self.cursor.execute('CREATE TABLE "%s" ()' % self.table_name)
self.table_schema = self.get_table_schema(self.cursor, self.table_name)
if model.__doc__:
self.cursor.execute('SELECT tableowner = current_user FROM pg_tables '
'WHERE tablename = %s AND schemaname = %s',
(self.table_name, self.table_schema))
self.is_owner, = self.cursor.fetchone()
if model.__doc__ and self.is_owner:
self.cursor.execute('COMMENT ON TABLE "%s" IS \'%s\'' %
(self.table_name, model.__doc__.replace("'", "''")))
@@ -52,22 +58,32 @@ class TableHandler(TableHandlerInterface):
(self.table_name, self.sequence_name))
self.cursor.execute('ALTER TABLE "%s" '
'ADD PRIMARY KEY(__id)' % self.table_name)
if self.history:
self.cursor.execute('ALTER TABLE "%s" '
'ALTER __id SET DEFAULT nextval(\'"%s"\')'
% (self.table_name, self.sequence_name))
else:
self.cursor.execute('ALTER TABLE "%s" '
'ALTER id SET DEFAULT nextval(\'"%s"\')'
% (self.table_name, self.sequence_name))
default = "nextval('%s'::regclass)" % self.sequence_name
if self.history:
if self._columns['__id']['default'] != default:
self.cursor.execute('ALTER TABLE "%s" '
'ALTER __id SET DEFAULT %s'
% (self.table_name, default))
if self._columns['id']['default'] != default:
self.cursor.execute('ALTER TABLE "%s" '
'ALTER id SET DEFAULT %s'
% (self.table_name, default))
self._update_definitions()
@staticmethod
def table_exist(cursor, table_name):
cursor.execute("SELECT relname FROM pg_class "
"WHERE relkind = 'r' AND relname = %s",
(table_name,))
return bool(cursor.rowcount)
@classmethod
def get_table_schema(cls, cursor, table_name):
for schema in cursor.search_path:
cursor.execute('SELECT 1 '
'FROM information_schema.tables '
'WHERE table_name = %s AND table_schema = %s',
(table_name, schema))
if cursor.rowcount:
return schema
@classmethod
def table_exist(cls, cursor, table_name):
return bool(cls.get_table_schema(cursor, table_name))
@staticmethod
def table_rename(cursor, old_name, new_name):
@@ -88,11 +104,19 @@ class TableHandler(TableHandlerInterface):
cursor.execute('ALTER TABLE "%s" RENAME TO "%s"'
% (old_history, new_history))
@staticmethod
def sequence_exist(cursor, sequence_name):
cursor.execute('SELECT relname FROM pg_class '
'WHERE relkind = \'S\' and relname = %s', (sequence_name,))
return bool(cursor.rowcount)
@classmethod
def sequence_schema(cls, cursor, sequence_name):
for schema in cursor.search_path:
cursor.execute('SELECT 1 '
'FROM information_schema.sequences '
'WHERE sequence_name = %s AND sequence_schema = %s',
(sequence_name, schema))
if cursor.rowcount:
return schema
@classmethod
def sequence_exist(cls, cursor, sequence_name):
return bool(cls.sequence_schema(cursor, sequence_name))
@staticmethod
def sequence_rename(cursor, old_name, new_name):
@@ -117,53 +141,46 @@ class TableHandler(TableHandlerInterface):
self.table_name, new_name))
def _update_definitions(self):
# Fetch columns definitions from the table
self.cursor.execute("SELECT at.attname, at.attlen, "
"at.atttypmod, at.attnotnull, at.atthasdef, ty.typname, "
"CASE WHEN at.attlen = -1 "
"THEN at.atttypmod-4 "
"ELSE at.attlen END as size "
"FROM pg_class cl "
"JOIN pg_attribute at on (cl.oid = at.attrelid) "
"JOIN pg_type ty on (at.atttypid = ty.oid) "
"WHERE cl.relname = %s AND at.attnum > 0",
(self.table_name,))
self._columns = {}
for line in self.cursor.fetchall():
column, length, typmod, notnull, hasdef, typname, size = line
# Fetch columns definitions from the table
self.cursor.execute('SELECT '
'column_name, udt_name, is_nullable, character_maximum_length, '
'column_default '
'FROM information_schema.columns '
'WHERE table_name = %s AND table_schema = %s',
(self.table_name, self.table_schema))
for column, typname, nullable, size, default in self.cursor.fetchall():
self._columns[column] = {
"length": length,
"typmod": typmod,
"notnull": notnull,
"hasdef": hasdef,
"size": size,
"typname": typname}
'typname': typname,
'notnull': True if nullable == 'NO' else False,
'size': size,
'default': default,
}
# fetch constraints for the table
self.cursor.execute("SELECT co.contype, co.confdeltype, at.attname, "
"cl2.relname, co.conname "
"FROM pg_constraint co "
"LEFT JOIN pg_class cl ON (co.conrelid = cl.oid) "
"LEFT JOIN pg_class cl2 ON (co.confrelid = cl2.oid) "
"LEFT JOIN pg_attribute at ON (co.conkey[1] = at.attnum) "
"WHERE cl.relname = %s AND at.attrelid = cl.oid",
(self.table_name,))
self._constraints = []
self._fk_deltypes = {}
for line in self.cursor.fetchall():
contype, confdeltype, column, ref, conname = line
if contype == 'f':
self._fk_deltypes[column] = confdeltype
if conname not in self._constraints:
self._constraints.append(conname)
self.cursor.execute('SELECT constraint_name '
'FROM information_schema.table_constraints '
'WHERE table_name = %s AND table_schema = %s',
(self.table_name, self.table_schema))
self._constraints = [c for c, in self.cursor.fetchall()]
self.cursor.execute('SELECT k.column_name, r.delete_rule '
'FROM information_schema.key_column_usage AS k '
'JOIN information_schema.referential_constraints AS r '
'ON r.constraint_schema = k.constraint_schema '
'AND r.constraint_name = k.constraint_name '
'WHERE k.table_name = %s AND k.table_schema = %s',
(self.table_name, self.table_schema))
self._fk_deltypes = dict(self.cursor.fetchall())
# Fetch indexes defined for the table
self.cursor.execute("SELECT cl2.relname "
"FROM pg_index ind "
"JOIN pg_class cl on (cl.oid = ind.indrelid) "
"JOIN pg_namespace n ON (cl.relnamespace = n.oid) "
"JOIN pg_class cl2 on (cl2.oid = ind.indexrelid) "
"WHERE cl.relname = %s",
(self.table_name,))
"WHERE cl.relname = %s AND n.nspname = %s",
(self.table_name, self.table_schema))
self._indexes = [l[0] for l in self.cursor.fetchall()]
# Keep track of which module created each field
@@ -199,14 +216,20 @@ class TableHandler(TableHandlerInterface):
self._update_definitions()
def db_default(self, column_name, value):
self.cursor.execute('ALTER TABLE "' + self.table_name + '" '
'ALTER COLUMN "' + column_name + '" SET DEFAULT %s',
(value,))
if value in [True, False]:
test = str(value).lower()
else:
test = value
if self._columns[column_name]['default'] != test:
self.cursor.execute('ALTER TABLE "' + self.table_name + '" '
'ALTER COLUMN "' + column_name + '" SET DEFAULT %s',
(value,))
def add_raw_column(self, column_name, column_type, column_format,
default_fun=None, field_size=None, migrate=True, string=''):
def comment():
self.cursor.execute('COMMENT ON COLUMN "%s"."%s" IS \'%s\'' %
if self.is_owner:
self.cursor.execute('COMMENT ON COLUMN "%s"."%s" IS \'%s\'' %
(self.table_name, column_name, string.replace("'", "''")))
if self.column_exist(column_name):
if (column_name in ('create_date', 'write_date')
@@ -274,33 +297,27 @@ class TableHandler(TableHandlerInterface):
self._update_definitions()
def add_fk(self, column_name, reference, on_delete=None):
on_delete_code = {
'RESTRICT': 'r',
'NO ACTION': 'a',
'CASCADE': 'c',
'SET NULL': 'n',
'SET DEFAULT': 'd',
}
if on_delete is not None:
on_delete = on_delete.upper()
if on_delete not in on_delete_code:
raise Exception('On delete action not supported!')
else:
on_delete = 'SET NULL'
code = on_delete_code[on_delete]
self.cursor.execute('SELECT conname FROM pg_constraint '
'WHERE conname = %s',
(self.table_name + '_' + column_name + '_fkey',))
name = self.table_name + '_' + column_name + '_fkey'
self.cursor.execute('SELECT 1 '
'FROM information_schema.key_column_usage '
'WHERE table_name = %s AND table_schema = %s '
'AND constraint_name = %s',
(self.table_name, self.table_schema, name))
add = False
if not self.cursor.rowcount:
add = True
elif self._fk_deltypes.get(column_name) != code:
elif self._fk_deltypes.get(column_name) != on_delete:
self.drop_fk(column_name)
add = True
if add:
self.cursor.execute('ALTER TABLE "' + self.table_name + '" '
'ADD FOREIGN KEY ("' + column_name + '") '
'ADD CONSTRAINT "' + name + '" '
'FOREIGN KEY ("' + column_name + '") '
'REFERENCES "' + reference + '" '
'ON DELETE ' + on_delete)
self._update_definitions()