From 109d0ef64f286a81e073d78a4566674c8b49196d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 4 Sep 2015 11:16:09 +0200 Subject: [PATCH] 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 --- CHANGELOG | 1 + trytond/backend/postgresql/database.py | 61 ++++----- trytond/backend/postgresql/table.py | 167 ++++++++++++++----------- 3 files changed, 117 insertions(+), 112 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b1d9884e03..abd6625bec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Manage PostgreSQL schema * Remove colors attribute on tree view * Remove style on Report * Add StateReport to Wizard diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index 3ef34cb3e1..9776784343 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -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) diff --git a/trytond/backend/postgresql/table.py b/trytond/backend/postgresql/table.py index 4d60c26830..66407f5f7b 100644 --- a/trytond/backend/postgresql/table.py +++ b/trytond/backend/postgresql/table.py @@ -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()