Files
tryton/trytond/backend/postgresql/database.py
T

361 lines
12 KiB
Python
Raw Normal View History

2014-12-02 12:54:01 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2015-03-15 15:52:03 +01:00
import time
import logging
import re
import os
2015-05-19 09:48:07 +02:00
import urllib
2015-03-15 15:52:03 +01:00
from decimal import Decimal
try:
from psycopg2cffi import compat
compat.register()
except ImportError:
pass
2016-03-16 22:12:22 +01:00
from psycopg2 import connect
2009-02-04 15:29:28 +01:00
from psycopg2.pool import ThreadedConnectionPool
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
2009-02-04 15:29:28 +01:00
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from psycopg2.extensions import register_type, register_adapter
from psycopg2.extensions import UNICODE, AsIs
try:
2015-02-09 22:58:56 +01:00
from psycopg2.extensions import PYDATE, PYDATETIME, PYTIME, PYINTERVAL
except ImportError:
2015-02-09 22:58:56 +01:00
PYDATE, PYDATETIME, PYTIME, PYINTERVAL = None, None, None, None
2009-02-04 15:29:28 +01:00
from psycopg2 import IntegrityError as DatabaseIntegrityError
2009-02-06 10:26:38 +01:00
from psycopg2 import OperationalError as DatabaseOperationalError
2015-03-15 15:52:03 +01:00
2013-09-06 22:32:37 +02:00
from sql import Flavor
from trytond.backend.database import DatabaseInterface
2015-03-15 15:52:03 +01:00
from trytond.config import config, parse_uri
__all__ = ['Database', 'DatabaseIntegrityError', 'DatabaseOperationalError']
2009-02-04 15:29:28 +01:00
2015-03-05 11:32:18 +01:00
logger = logging.getLogger(__name__)
2011-08-21 10:20:23 +02:00
RE_VERSION = re.compile(r'\S+ (\d+)\.(\d+)')
2009-02-04 15:29:28 +01:00
os.environ['PGTZ'] = os.environ.get('TZ', '')
2009-02-04 15:29:28 +01:00
def unescape_quote(s):
if s.startswith('"') and s.endswith('"'):
return s.strip('"').replace('""', '"')
return s
def replace_special_values(s, **mapping):
for name, value in mapping.iteritems():
s = s.replace('$' + name, value)
return s
2009-02-04 15:29:28 +01:00
class Database(DatabaseInterface):
2009-02-11 14:52:22 +01:00
_databases = {}
_connpool = None
2009-08-25 16:58:09 +02:00
_list_cache = None
_list_cache_timestamp = None
2011-08-21 10:20:23 +02:00
_version_cache = {}
2013-09-06 22:32:37 +02:00
flavor = Flavor(ilike=True)
2009-02-11 14:52:22 +01:00
def __new__(cls, name='template1'):
if name in cls._databases:
return cls._databases[name]
return DatabaseInterface.__new__(cls, name=name)
2009-02-11 14:52:22 +01:00
def __init__(self, name='template1'):
super(Database, self).__init__(name=name)
self._databases.setdefault(name, self)
self._search_path = None
self._current_user = None
2009-02-04 15:29:28 +01:00
2016-03-16 22:12:22 +01:00
@classmethod
def dsn(cls, 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 ''
2016-03-16 22:12:22 +01:00
name = "dbname=%s" % name
user = uri.username and "user=%s" % uri.username or ''
2015-05-19 09:48:07 +02:00
password = ("password=%s" % urllib.unquote_plus(uri.password)
if uri.password else '')
2016-03-16 22:12:22 +01:00
return '%s %s %s %s %s' % (host, port, name, user, password)
def connect(self):
if self._connpool is not None:
return self
logger.info('connect to "%s"', self.name)
2015-03-22 03:20:53 +01:00
minconn = config.getint('database', 'minconn', default=1)
maxconn = config.getint('database', 'maxconn', default=64)
2016-03-16 22:12:22 +01:00
self._connpool = ThreadedConnectionPool(
minconn, maxconn, self.dsn(self.name))
2009-02-04 15:29:28 +01:00
return self
def get_connection(self, autocommit=False, readonly=False):
2009-02-04 15:29:28 +01:00
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)
2011-08-21 11:05:51 +02:00
if readonly:
cursor = conn.cursor()
2011-08-21 11:05:51 +02:00
cursor.execute('SET TRANSACTION READ ONLY')
return conn
def put_connection(self, connection, close=False):
self._connpool.putconn(connection, close=close)
2009-02-04 15:29:28 +01:00
def close(self):
if self._connpool is None:
return
self._connpool.closeall()
self._connpool = None
@classmethod
def create(cls, connection, database_name):
cursor = connection.cursor()
2013-02-27 22:50:31 +01:00
cursor.execute('CREATE DATABASE "' + database_name + '" '
'TEMPLATE template0 ENCODING \'unicode\'')
connection.commit()
cls._list_cache = None
2009-02-04 15:29:28 +01:00
def drop(self, connection, database_name):
cursor = connection.cursor()
2009-02-04 15:29:28 +01:00
cursor.execute('DROP DATABASE "' + database_name + '"')
Database._list_cache = None
2009-02-04 15:29:28 +01:00
def get_version(self, connection):
if self.name not in self._version_cache:
cursor = connection.cursor()
2011-08-21 10:20:23 +02:00
cursor.execute('SELECT version()')
version, = cursor.fetchone()
self._version_cache[self.name] = tuple(map(int,
RE_VERSION.search(version).groups()))
return self._version_cache[self.name]
2011-08-21 10:20:23 +02:00
2009-02-05 16:44:38 +01:00
@staticmethod
def dump(database_name):
from trytond.tools import exec_command_pipe
2009-05-03 14:35:28 +02:00
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
2009-02-05 16:44:38 +01:00
cmd.append(database_name)
pipe = exec_command_pipe(*tuple(cmd), env=env)
pipe.stdin.close()
data = pipe.stdout.read()
res = pipe.wait()
2009-02-05 16:44:38 +01:00
if res:
raise Exception('Couldn\'t dump database!')
return data
@staticmethod
def restore(database_name, data):
from trytond.tools import exec_command_pipe
2009-02-05 16:44:38 +01:00
database = Database().connect()
connection = database.get_connection(autocommit=True)
database.create(connection, database_name)
database.close()
2009-02-05 16:44:38 +01:00
2009-05-03 14:35:28 +02:00
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
2009-02-05 16:44:38 +01:00
cmd.append('--dbname=' + database_name)
args2 = tuple(cmd)
if os.name == "nt":
tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
2011-01-08 20:29:00 +01:00
with open(tmpfile, 'wb') as fp:
fp.write(data)
2009-02-05 16:44:38 +01:00
args2 = list(args2)
args2.append(' ' + tmpfile)
args2 = tuple(args2)
pipe = exec_command_pipe(*args2, env=env)
2009-02-05 16:44:38 +01:00
if not os.name == "nt":
pipe.stdin.write(data)
pipe.stdin.close()
res = pipe.wait()
2009-02-05 16:44:38 +01:00
if res:
raise Exception('Couldn\'t restore database')
database = Database(database_name).connect()
cursor = database.get_connection().cursor()
if not database.test():
2009-02-05 16:44:38 +01:00
cursor.close()
database.close()
2009-02-05 16:44:38 +01:00
raise Exception('Couldn\'t restore database!')
cursor.close()
database.close()
2009-08-25 16:58:09 +02:00
Database._list_cache = None
2009-02-05 16:44:38 +01:00
return True
def list(self):
2009-08-25 16:58:09 +02:00
now = time.time()
timeout = config.getint('session', 'timeout')
2009-08-25 16:58:09 +02:00
res = Database._list_cache
if res and abs(Database._list_cache_timestamp - now) < timeout:
return res
connection = self.get_connection()
cursor = connection.cursor()
2015-09-04 11:16:09 +02:00
cursor.execute('SELECT datname FROM pg_database '
'WHERE datistemplate = false ORDER BY datname')
2009-02-04 15:29:28 +01:00
res = []
for db_name, in cursor:
2009-02-04 15:29:28 +01:00
try:
2016-03-16 22:12:22 +01:00
with connect(self.dsn(db_name)) as conn:
if self._test(conn):
res.append(db_name)
2010-07-08 19:53:39 +02:00
except Exception:
2009-02-04 15:29:28 +01:00
continue
self.put_connection(connection)
2009-08-25 16:58:09 +02:00
Database._list_cache = res
Database._list_cache_timestamp = now
2009-02-04 15:29:28 +01:00
return res
def init(self):
from trytond.modules import get_module_info
connection = self.get_connection()
cursor = connection.cursor()
2009-02-04 15:29:28 +01:00
sql_file = os.path.join(os.path.dirname(__file__), 'init.sql')
2011-01-08 20:29:00 +01:00
with open(sql_file) as fp:
for line in fp.read().split(';'):
2012-04-01 23:08:15 +02:00
if (len(line) > 0) and (not line.isspace()):
2011-01-08 20:29:00 +01:00
cursor.execute(line)
2009-02-04 15:29:28 +01:00
2016-01-16 19:20:30 +01:00
for module in ('ir', 'res'):
state = 'uninstalled'
if module in ('ir', 'res'):
2009-02-04 15:29:28 +01:00
state = 'to install'
info = get_module_info(module)
2015-05-28 21:02:01 +02:00
cursor.execute('SELECT NEXTVAL(\'ir_module_id_seq\')')
2009-02-04 15:29:28 +01:00
module_id = cursor.fetchone()[0]
2015-05-28 21:02:01 +02:00
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', []):
2015-05-28 21:02:01 +02:00
cursor.execute('INSERT INTO ir_module_dependency '
'(create_uid, create_date, module, name) '
'VALUES (%s, now(), %s, %s)',
(0, module_id, dependency))
2009-02-04 15:29:28 +01:00
connection.commit()
self.put_connection(connection)
2009-02-04 15:29:28 +01:00
def test(self):
connection = self.get_connection()
2016-03-16 22:12:22 +01:00
is_tryton_database = self._test(connection)
self.put_connection(connection)
return is_tryton_database
@classmethod
def _test(cls, connection):
cursor = connection.cursor()
cursor.execute('SELECT 1 FROM information_schema.tables '
2015-09-04 11:16:09 +02:00
'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'),))
2016-03-16 22:12:22 +01:00
return len(cursor.fetchall()) != 0
2009-02-04 15:29:28 +01:00
def nextid(self, connection, table):
cursor = connection.cursor()
cursor.execute("SELECT NEXTVAL('" + table + "_id_seq')")
return cursor.fetchone()[0]
2009-07-07 19:32:31 +02:00
def setnextid(self, connection, table, value):
cursor = connection.cursor()
cursor.execute("SELECT SETVAL('" + table + "_id_seq', %d)" % value)
2009-07-07 19:32:31 +02:00
def currid(self, connection, table):
cursor = connection.cursor()
cursor.execute('SELECT last_value FROM "' + table + '_id_seq"')
return cursor.fetchone()[0]
2009-07-07 19:32:31 +02:00
def lock(self, connection, table):
cursor = connection.cursor()
cursor.execute('LOCK "%s" IN EXCLUSIVE MODE NOWAIT' % table)
2009-07-07 19:32:31 +02:00
def has_constraint(self):
return True
2015-07-28 08:04:29 +02:00
def has_multirow_insert(self):
return True
def get_table_schema(self, connection, table_name):
cursor = connection.cursor()
for schema in self.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
2015-09-04 11:16:09 +02:00
@property
def current_user(self):
if self._current_user is None:
connection = self.get_connection()
cursor = connection.cursor()
cursor.execute('SELECT current_user')
self._current_user = cursor.fetchone()[0]
2015-09-04 11:16:09 +02:00
return self._current_user
@property
def search_path(self):
if self._search_path is None:
connection = self.get_connection()
cursor = connection.cursor()
cursor.execute('SHOW search_path')
path, = cursor.fetchone()
special_values = {
'user': self.current_user,
}
self._search_path = [
unescape_quote(replace_special_values(
p.strip(), **special_values))
for p in path.split(',')]
2015-09-04 11:16:09 +02:00
return self._search_path
2009-02-04 15:29:28 +01:00
register_type(UNICODE)
if PYDATE:
register_type(PYDATE)
if PYDATETIME:
register_type(PYDATETIME)
if PYTIME:
register_type(PYTIME)
2015-02-09 22:58:56 +01:00
if PYINTERVAL:
register_type(PYINTERVAL)
register_adapter(float, lambda value: AsIs(repr(value)))
register_adapter(Decimal, lambda value: AsIs(str(value)))