# HG changeset patch # User stuart # Date 1410395241 21600 # Wed Sep 10 18:27:21 2014 -0600 # Node ID 8806fc2f201cebc231b43d011373c1fbb2aa3254 # Parent 5a59c723e57fa357ed6cf4a93cacf72e8e15fc7d Support for roundup database in postgresql schema. Allow config.RDBMS_NAME to be of the form . in which case Roundup will expect or create its database in the schema of database rather than in schema "public" as will happen when the . part is not given. When a schema name is given, the database must already exist, and, if Roundup is initializing it, the roundup user (given in config.RDBMS_USER) must have previously been given "create" privilege in the database. diff -r 5a59c723e57f -r 8806fc2f201c roundup/backends/back_postgresql.py --- a/roundup/backends/back_postgresql.py Sat Sep 06 21:53:46 2014 +0200 +++ b/roundup/backends/back_postgresql.py Wed Sep 10 18:27:21 2014 -0600 @@ -7,7 +7,7 @@ '''Postgresql backend via psycopg for Roundup.''' __docformat__ = 'restructuredtext' -import os, shutil, time +import os, shutil, time, re ISOLATION_LEVEL_READ_UNCOMMITTED = None ISOLATION_LEVEL_READ_COMMITTED = None ISOLATION_LEVEL_REPEATABLE_READ = None @@ -55,20 +55,35 @@ del d['read_default_file'] return d +def db_schema_split (database_name): + ''' Split database_name into database and schema parts''' + if '.' in database_name: + return database_name.split ('.') + return [database_name, ''] + def db_create(config): """Clear all database contents and drop database itself""" - command = "CREATE DATABASE \"%s\" WITH ENCODING='UNICODE'"%config.RDBMS_NAME - if config.RDBMS_TEMPLATE : - command = command + " TEMPLATE=%s" % config.RDBMS_TEMPLATE - logging.getLogger('roundup.hyperdb').info(command) - db_command(config, command) + db_name, schema_name = db_schema_split(config.RDBMS_NAME) + if not schema_name: + command = "CREATE DATABASE \"%s\" WITH ENCODING='UNICODE'" % db_name + if config.RDBMS_TEMPLATE: + command = command + " TEMPLATE=%s" % config.RDBMS_TEMPLATE + logging.getLogger('roundup.hyperdb').info(command) + db_command(config, command) + else: + command = "CREATE SCHEMA \"%s\" AUTHORIZATION \"%s\"" % (schema_name, config.RDBMS_USER) + db_command(config, command, db_name) def db_nuke(config, fail_ok=0): - """Clear all database contents and drop database itself""" - command = 'DROP DATABASE "%s"'% config.RDBMS_NAME - logging.getLogger('roundup.hyperdb').info(command) - db_command(config, command) - + """Drop the database (and all its contents) or the schema.""" + db_name, schema_name = db_schema_split(config.RDBMS_NAME) + if not schema_name: + command = 'DROP DATABASE "%s"'% db_name + logging.getLogger('roundup.hyperdb').info(command) + db_command(config, command) + else: + command = 'DROP SCHEMA "%s" CASCADE' % schema_name + db_command(config, command, db_name) if os.path.exists(config.DATABASE): shutil.rmtree(config.DATABASE) @@ -77,17 +92,19 @@ fail by conflicting with another user. Since PostgreSQL version 8.1 there is a database "postgres", - before "template1" seems to habe been used, so we fall back to it. + before "template1" seems to have been used, so we fall back to it. Compare to issue2550543. ''' - template1 = connection_dict(config) + template1 = connection_dict(config, 'database') + db_name, schema_name = db_schema_split(template1['database']) template1['database'] = database try: conn = psycopg.connect(**template1) except psycopg.OperationalError, message: - if str(message).find('database "postgres" does not exist') >= 0: - return db_command(config, command, database='template1') + if not schema_name: + if re.search(r'database ".+" does not exist', str(message)): + return db_command(config, command, database='template1') raise hyperdb.DatabaseError(message) conn.set_isolation_level(0) @@ -98,16 +115,16 @@ return finally: conn.close() - raise RuntimeError('10 attempts to create database failed') + raise RuntimeError('10 attempts to create database or schema failed') -def pg_command(cursor, command): +def pg_command(cursor, command, args=()): '''Execute the postgresql command, which may be blocked by some other user connecting to the database, and return a true value if it succeeds. If there is a concurrent update, retry the command. ''' try: - cursor.execute(command) + cursor.execute(command, args) except psycopg.ProgrammingError, err: response = str(err).split('\n')[0] if response.find('FATAL') != -1: @@ -128,14 +145,27 @@ return 1 def db_exists(config): - """Check if database already exists""" + """Check if database or schema already exists""" + #import pdb; pdb.set_trace() db = connection_dict(config, 'database') + db_name, schema_name = db_schema_split(db['database']) + if schema_name: + db['database'] = db_name try: conn = psycopg.connect(**db) - conn.close() - return 1 + if not schema_name: + conn.close() + return 1 except: return 0 + # will have a non-false value here; otherwise one + # of the above returns would have returned. + # Get a count of the number of schemas named (either 0 or 1). + command = "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = %s" + cursor = conn.cursor() + pg_command(cursor, command, (schema_name,)) + count = cursor.fetchall()[0][0] + return count # 'count' will be 0 or 1. class Sessions(sessions_rdbms.Sessions): def set(self, *args, **kwargs): @@ -161,6 +191,10 @@ def sql_open_connection(self): db = connection_dict(self.config, 'database') + db_name, schema_name = db_schema_split (db['database']) + if schema_name: + db['database'] = db_name + logging.getLogger('roundup.hyperdb').info( 'open database %r'%db['database']) try: @@ -173,6 +207,11 @@ lvl = isolation_levels [self.config.RDBMS_ISOLATION_LEVEL] conn.set_isolation_level(lvl) + if schema_name: + self.sql ('SET search_path TO %s' % schema_name, cursor=cursor) + # Commit is required so that a subsequent rollback + # will not also rollback the search_path change. + self.sql ('COMMIT', cursor=cursor) return (conn, cursor) def open_connection(self):