Files
tryton/trytond/backend/postgresql/database.py
T
Cédric Krier 109d0ef64f 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
2015-09-04 11:16:09 +02:00

365 lines
12 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import time
import logging
import re
import os
import urllib
from decimal import Decimal
try:
from psycopg2cffi import compat
compat.register()
except ImportError:
pass
from psycopg2.pool import ThreadedConnectionPool
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from psycopg2.extensions import register_type, register_adapter
from psycopg2.extensions import UNICODE, AsIs
try:
from psycopg2.extensions import PYDATE, PYDATETIME, PYTIME, PYINTERVAL
except ImportError:
PYDATE, PYDATETIME, PYTIME, PYINTERVAL = None, None, None, None
from psycopg2 import IntegrityError as DatabaseIntegrityError
from psycopg2 import OperationalError as DatabaseOperationalError
from sql import Flavor
from trytond.backend.database import DatabaseInterface, CursorInterface
from trytond.config import config, parse_uri
__all__ = ['Database', 'DatabaseIntegrityError', 'DatabaseOperationalError',
'Cursor']
logger = logging.getLogger(__name__)
RE_VERSION = re.compile(r'\S+ (\d+)\.(\d+)')
os.environ['PGTZ'] = os.environ.get('TZ', '')
class Database(DatabaseInterface):
_databases = {}
_connpool = None
_list_cache = None
_list_cache_timestamp = None
_version_cache = {}
flavor = Flavor(ilike=True)
def __new__(cls, database_name='template1'):
if database_name in cls._databases:
return cls._databases[database_name]
return DatabaseInterface.__new__(cls, database_name=database_name)
def __init__(self, database_name='template1'):
super(Database, self).__init__(database_name=database_name)
self._databases.setdefault(database_name, self)
def connect(self):
if self._connpool is not None:
return self
logger.info('connect to "%s"', self.database_name)
uri = parse_uri(config.get('database', 'uri'))
assert uri.scheme == 'postgresql'
host = uri.hostname and "host=%s" % uri.hostname or ''
port = uri.port and "port=%s" % uri.port or ''
name = "dbname=%s" % self.database_name
user = uri.username and "user=%s" % uri.username or ''
password = ("password=%s" % urllib.unquote_plus(uri.password)
if uri.password else '')
minconn = config.getint('database', 'minconn', default=1)
maxconn = config.getint('database', 'maxconn', default=64)
dsn = '%s %s %s %s %s' % (host, port, name, user, password)
self._connpool = ThreadedConnectionPool(minconn, maxconn, dsn)
return self
def cursor(self, autocommit=False, readonly=False):
if self._connpool is None:
self.connect()
conn = self._connpool.getconn()
if autocommit:
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
else:
conn.set_isolation_level(ISOLATION_LEVEL_REPEATABLE_READ)
cursor = Cursor(self._connpool, conn, self)
if readonly:
cursor.execute('SET TRANSACTION READ ONLY')
return cursor
def close(self):
if self._connpool is None:
return
self._connpool.closeall()
self._connpool = None
@classmethod
def create(cls, cursor, database_name):
cursor.execute('CREATE DATABASE "' + database_name + '" '
'TEMPLATE template0 ENCODING \'unicode\'')
cls._list_cache = None
@classmethod
def drop(cls, cursor, database_name):
cursor.execute('DROP DATABASE "' + database_name + '"')
cls._list_cache = None
def get_version(self, cursor):
if self.database_name not in self._version_cache:
cursor.execute('SELECT version()')
version, = cursor.fetchone()
self._version_cache[self.database_name] = tuple(map(int,
RE_VERSION.search(version).groups()))
return self._version_cache[self.database_name]
@staticmethod
def dump(database_name):
from trytond.tools import exec_command_pipe
cmd = ['pg_dump', '--format=c', '--no-owner']
env = {}
uri = parse_uri(config.get('database', 'uri'))
if uri.username:
cmd.append('--username=' + uri.username)
if uri.hostname:
cmd.append('--host=' + uri.hostname)
if uri.port:
cmd.append('--port=' + str(uri.port))
if uri.password:
# if db_password is set in configuration we should pass
# an environment variable PGPASSWORD to our subprocess
# see libpg documentation
env['PGPASSWORD'] = uri.password
cmd.append(database_name)
pipe = exec_command_pipe(*tuple(cmd), env=env)
pipe.stdin.close()
data = pipe.stdout.read()
res = pipe.wait()
if res:
raise Exception('Couldn\'t dump database!')
return data
@staticmethod
def restore(database_name, data):
from trytond.tools import exec_command_pipe
database = Database().connect()
cursor = database.cursor(autocommit=True)
database.create(cursor, database_name)
cursor.commit()
cursor.close()
database.close()
cmd = ['pg_restore', '--no-owner']
env = {}
uri = parse_uri(config.get('database', 'uri'))
if uri.username:
cmd.append('--username=' + uri.username)
if uri.hostname:
cmd.append('--host=' + uri.hostname)
if uri.port:
cmd.append('--port=' + str(uri.port))
if uri.password:
env['PGPASSWORD'] = uri.password
cmd.append('--dbname=' + database_name)
args2 = tuple(cmd)
if os.name == "nt":
tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
with open(tmpfile, 'wb') as fp:
fp.write(data)
args2 = list(args2)
args2.append(' ' + tmpfile)
args2 = tuple(args2)
pipe = exec_command_pipe(*args2, env=env)
if not os.name == "nt":
pipe.stdin.write(data)
pipe.stdin.close()
res = pipe.wait()
if res:
raise Exception('Couldn\'t restore database')
database = Database(database_name).connect()
cursor = database.cursor()
if not cursor.test():
cursor.close()
database.close()
raise Exception('Couldn\'t restore database!')
cursor.close()
database.close()
Database._list_cache = None
return True
@staticmethod
def list(cursor):
now = time.time()
timeout = config.getint('session', 'timeout')
res = Database._list_cache
if res and abs(Database._list_cache_timestamp - now) < timeout:
return res
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')
try:
database = Database(db_name).connect()
except Exception:
continue
cursor2 = database.cursor()
if cursor2.test():
res.append(db_name)
cursor2.close(close=True)
else:
cursor2.close(close=True)
database.close()
Database._list_cache = res
Database._list_cache_timestamp = now
return res
@staticmethod
def init(cursor):
from trytond.modules import get_module_info
sql_file = os.path.join(os.path.dirname(__file__), 'init.sql')
with open(sql_file) as fp:
for line in fp.read().split(';'):
if (len(line) > 0) and (not line.isspace()):
cursor.execute(line)
for module in ('ir', 'res', 'webdav'):
state = 'uninstalled'
if module in ('ir', 'res'):
state = 'to install'
info = get_module_info(module)
cursor.execute('SELECT NEXTVAL(\'ir_module_id_seq\')')
module_id = cursor.fetchone()[0]
cursor.execute('INSERT INTO ir_module '
'(id, create_uid, create_date, name, state) '
'VALUES (%s, %s, now(), %s, %s)',
(module_id, 0, module, state))
for dependency in info.get('depends', []):
cursor.execute('INSERT INTO ir_module_dependency '
'(create_uid, create_date, module, name) '
'VALUES (%s, now(), %s, %s)',
(0, module_id, dependency))
class Cursor(CursorInterface):
def __init__(self, connpool, conn, database):
super(Cursor, self).__init__()
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 = {}
self.sql_into_log = {}
self.count = {
'from': 0,
'into': 0,
}
@property
def database_name(self):
return self._database.database_name
# TODO to remove
@property
def dbname(self):
return self.database_name
def __getattr__(self, name):
return getattr(self.cursor, name)
def execute(self, sql, params=None):
if params:
return self.cursor.execute(sql, params)
else:
return self.cursor.execute(sql)
def close(self, close=False):
self.cursor.close()
self.rollback()
self._connpool.putconn(self._conn, close=close)
def commit(self):
super(Cursor, self).commit()
self._conn.commit()
def rollback(self):
super(Cursor, self).rollback()
self._conn.rollback()
def test(self):
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):
self.cursor.execute("SELECT NEXTVAL('" + table + "_id_seq')")
return self.cursor.fetchone()[0]
def setnextid(self, table, value):
self.cursor.execute("SELECT SETVAL('" + table + "_id_seq', %d)"
% value)
def currid(self, table):
self.cursor.execute('SELECT last_value FROM "' + table + '_id_seq"')
return self.cursor.fetchone()[0]
def lock(self, table):
self.cursor.execute('LOCK "%s" IN EXCLUSIVE MODE NOWAIT' % table)
def has_constraint(self):
return True
def limit_clause(self, select, limit=None, offset=None):
if limit is not None:
select += ' LIMIT %d' % limit
if offset is not None and offset != 0:
select += ' OFFSET %d' % offset
return select
def has_returning(self):
# RETURNING clause is available since PostgreSQL 8.2
return self._database.get_version(self) >= (8, 2)
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)
if PYDATETIME:
register_type(PYDATETIME)
if PYTIME:
register_type(PYTIME)
if PYINTERVAL:
register_type(PYINTERVAL)
register_adapter(float, lambda value: AsIs(repr(value)))
register_adapter(Decimal, lambda value: AsIs(str(value)))