Mercurial > p > roundup > code
comparison roundup/backends/back_postgresql.py @ 6935:b5062cb5c2a2
flake8 fixes
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Thu, 08 Sep 2022 13:41:26 -0400 |
| parents | b0dbc13a835a |
| children | 5b41018617f2 |
comparison
equal
deleted
inserted
replaced
| 6934:37e2e3897ddf | 6935:b5062cb5c2a2 |
|---|---|
| 5 # disclaimer are retained in their original form. | 5 # disclaimer are retained in their original form. |
| 6 # | 6 # |
| 7 '''Postgresql backend via psycopg2 for Roundup.''' | 7 '''Postgresql backend via psycopg2 for Roundup.''' |
| 8 __docformat__ = 'restructuredtext' | 8 __docformat__ = 'restructuredtext' |
| 9 | 9 |
| 10 import os, shutil, time | 10 import logging |
| 11 import os | |
| 12 import shutil | |
| 13 import time | |
| 14 | |
| 11 ISOLATION_LEVEL_READ_UNCOMMITTED = None | 15 ISOLATION_LEVEL_READ_UNCOMMITTED = None |
| 12 ISOLATION_LEVEL_READ_COMMITTED = None | 16 ISOLATION_LEVEL_READ_COMMITTED = None |
| 13 ISOLATION_LEVEL_REPEATABLE_READ = None | 17 ISOLATION_LEVEL_REPEATABLE_READ = None |
| 14 ISOLATION_LEVEL_SERIALIZABLE = None | 18 ISOLATION_LEVEL_SERIALIZABLE = None |
| 15 | 19 |
| 16 import psycopg2 | 20 import psycopg2 # noqa: E402 |
| 17 from psycopg2.extensions import QuotedString | 21 from psycopg2 import ProgrammingError # noqa: E402 |
| 18 from psycopg2.extensions import ISOLATION_LEVEL_READ_UNCOMMITTED | 22 from psycopg2.extensions import QuotedString # noqa: E402 |
| 19 from psycopg2.extensions import ISOLATION_LEVEL_READ_COMMITTED | 23 from psycopg2.extensions import ISOLATION_LEVEL_READ_UNCOMMITTED # noqa: F401 E402 |
| 20 from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ | 24 from psycopg2.extensions import ISOLATION_LEVEL_READ_COMMITTED # noqa: E402 |
| 21 from psycopg2.extensions import ISOLATION_LEVEL_SERIALIZABLE | 25 from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ # noqa: E402 |
| 22 from psycopg2 import ProgrammingError | 26 from psycopg2.extensions import ISOLATION_LEVEL_SERIALIZABLE # noqa: E402 |
| 23 from psycopg2.extensions import TransactionRollbackError | 27 from psycopg2.extensions import TransactionRollbackError # noqa: F401 E402 |
| 24 | 28 |
| 25 import logging | 29 from roundup import hyperdb # noqa: E402 |
| 26 | 30 from roundup.backends import rdbms_common # noqa: E402 |
| 27 from roundup import hyperdb, date | 31 from roundup.backends import sessions_rdbms # noqa: E402 |
| 28 from roundup.backends import rdbms_common | 32 |
| 29 from roundup.backends import sessions_rdbms | 33 isolation_levels = { |
| 30 | 34 'read uncommitted': ISOLATION_LEVEL_READ_COMMITTED, |
| 31 isolation_levels = \ | 35 'read committed': ISOLATION_LEVEL_READ_COMMITTED, |
| 32 { 'read uncommitted': ISOLATION_LEVEL_READ_COMMITTED | 36 'repeatable read': ISOLATION_LEVEL_REPEATABLE_READ, |
| 33 , 'read committed': ISOLATION_LEVEL_READ_COMMITTED | 37 'serializable': ISOLATION_LEVEL_SERIALIZABLE |
| 34 , 'repeatable read': ISOLATION_LEVEL_REPEATABLE_READ | 38 } |
| 35 , 'serializable': ISOLATION_LEVEL_SERIALIZABLE | 39 |
| 36 } | |
| 37 | 40 |
| 38 def connection_dict(config, dbnamestr=None): | 41 def connection_dict(config, dbnamestr=None): |
| 39 ''' read_default_group is MySQL-specific, ignore it ''' | 42 ''' read_default_group is MySQL-specific, ignore it ''' |
| 40 d = rdbms_common.connection_dict(config, dbnamestr) | 43 d = rdbms_common.connection_dict(config, dbnamestr) |
| 41 if 'read_default_group' in d: | 44 if 'read_default_group' in d: |
| 42 del d['read_default_group'] | 45 del d['read_default_group'] |
| 43 if 'read_default_file' in d: | 46 if 'read_default_file' in d: |
| 44 del d['read_default_file'] | 47 del d['read_default_file'] |
| 45 return d | 48 return d |
| 46 | 49 |
| 50 | |
| 47 def db_create(config): | 51 def db_create(config): |
| 48 """Clear all database contents and drop database itself""" | 52 """Clear all database contents and drop database itself""" |
| 49 command = "CREATE DATABASE \"%s\" WITH ENCODING='UNICODE'"%config.RDBMS_NAME | 53 command = "CREATE DATABASE \"%s\" WITH ENCODING='UNICODE'" % config.RDBMS_NAME |
| 50 if config.RDBMS_TEMPLATE: | 54 if config.RDBMS_TEMPLATE: |
| 51 command = command + " TEMPLATE=%s" % config.RDBMS_TEMPLATE | 55 command = command + " TEMPLATE=%s" % config.RDBMS_TEMPLATE |
| 52 logging.getLogger('roundup.hyperdb').info(command) | 56 logging.getLogger('roundup.hyperdb').info(command) |
| 53 db_command(config, command) | 57 db_command(config, command) |
| 54 | 58 |
| 59 | |
| 55 def db_nuke(config): | 60 def db_nuke(config): |
| 56 """Clear all database contents and drop database itself""" | 61 """Clear all database contents and drop database itself""" |
| 57 command = 'DROP DATABASE "%s"'% config.RDBMS_NAME | 62 command = 'DROP DATABASE "%s"' % config.RDBMS_NAME |
| 58 logging.getLogger('roundup.hyperdb').info(command) | 63 logging.getLogger('roundup.hyperdb').info(command) |
| 59 db_command(config, command) | 64 db_command(config, command) |
| 60 | 65 |
| 61 if os.path.exists(config.DATABASE): | 66 if os.path.exists(config.DATABASE): |
| 62 shutil.rmtree(config.DATABASE) | 67 shutil.rmtree(config.DATABASE) |
| 68 | |
| 63 | 69 |
| 64 def db_command(config, command, database='postgres'): | 70 def db_command(config, command, database='postgres'): |
| 65 '''Perform some sort of database-level command. Retry 10 times if we | 71 '''Perform some sort of database-level command. Retry 10 times if we |
| 66 fail by conflicting with another user. | 72 fail by conflicting with another user. |
| 67 | 73 |
| 68 Since PostgreSQL version 8.1 there is a database "postgres", | 74 Since PostgreSQL version 8.1 there is a database "postgres", |
| 69 before "template1" seems to have been used, so we fall back to it. | 75 before "template1" seems to have been used, so we fall back to it. |
| 70 Compare to issue2550543. | 76 Compare to issue2550543. |
| 71 ''' | 77 ''' |
| 72 template1 = connection_dict(config) | 78 template1 = connection_dict(config) |
| 73 template1['database'] = database | 79 template1['database'] = database |
| 74 | 80 |
| 80 raise hyperdb.DatabaseError(message) | 86 raise hyperdb.DatabaseError(message) |
| 81 | 87 |
| 82 conn.set_isolation_level(0) | 88 conn.set_isolation_level(0) |
| 83 cursor = conn.cursor() | 89 cursor = conn.cursor() |
| 84 try: | 90 try: |
| 85 for n in range(10): | 91 for _n in range(10): |
| 86 if pg_command(cursor, command): | 92 if pg_command(cursor, command): |
| 87 return | 93 return |
| 88 finally: | 94 finally: |
| 89 conn.close() | 95 conn.close() |
| 90 raise RuntimeError('10 attempts to create database failed') | 96 raise RuntimeError('10 attempts to create database failed') |
| 97 | |
| 91 | 98 |
| 92 def pg_command(cursor, command): | 99 def pg_command(cursor, command): |
| 93 '''Execute the postgresql command, which may be blocked by some other | 100 '''Execute the postgresql command, which may be blocked by some other |
| 94 user connecting to the database, and return a true value if it succeeds. | 101 user connecting to the database, and return a true value if it succeeds. |
| 95 | 102 |
| 106 ) | 113 ) |
| 107 for msg in msgs: | 114 for msg in msgs: |
| 108 if msg in response: | 115 if msg in response: |
| 109 time.sleep(0.1) | 116 time.sleep(0.1) |
| 110 return 0 | 117 return 0 |
| 111 raise RuntimeError (response) | 118 raise RuntimeError(response) |
| 112 return 1 | 119 return 1 |
| 120 | |
| 113 | 121 |
| 114 def db_exists(config): | 122 def db_exists(config): |
| 115 """Check if database already exists""" | 123 """Check if database already exists""" |
| 116 db = connection_dict(config, 'database') | 124 db = connection_dict(config, 'database') |
| 117 try: | 125 try: |
| 118 conn = psycopg2.connect(**db) | 126 conn = psycopg2.connect(**db) |
| 119 conn.close() | 127 conn.close() |
| 120 return 1 | 128 return 1 |
| 121 except: | 129 except Exception: |
| 122 return 0 | 130 return 0 |
| 131 | |
| 123 | 132 |
| 124 class Sessions(sessions_rdbms.Sessions): | 133 class Sessions(sessions_rdbms.Sessions): |
| 125 def set(self, *args, **kwargs): | 134 def set(self, *args, **kwargs): |
| 126 try: | 135 try: |
| 127 sessions_rdbms.Sessions.set(self, *args, **kwargs) | 136 sessions_rdbms.Sessions.set(self, *args, **kwargs) |
| 132 # another client just updated, and we're running on | 141 # another client just updated, and we're running on |
| 133 # serializable isolation. | 142 # serializable isolation. |
| 134 # see http://www.postgresql.org/docs/7.4/interactive/transaction-iso.html | 143 # see http://www.postgresql.org/docs/7.4/interactive/transaction-iso.html |
| 135 self.db.rollback() | 144 self.db.rollback() |
| 136 | 145 |
| 146 | |
| 137 class Database(rdbms_common.Database): | 147 class Database(rdbms_common.Database): |
| 138 """Postgres DB backend implementation | 148 """Postgres DB backend implementation |
| 139 | 149 |
| 140 attributes: | 150 attributes: |
| 141 dbtype: | 151 dbtype: |
| 152 implements_intersect = 1 | 162 implements_intersect = 1 |
| 153 | 163 |
| 154 def sql_open_connection(self): | 164 def sql_open_connection(self): |
| 155 db = connection_dict(self.config, 'database') | 165 db = connection_dict(self.config, 'database') |
| 156 logging.getLogger('roundup.hyperdb').info( | 166 logging.getLogger('roundup.hyperdb').info( |
| 157 'open database %r'%db['database']) | 167 'open database %r' % db['database']) |
| 158 try: | 168 try: |
| 159 conn = psycopg2.connect(**db) | 169 conn = psycopg2.connect(**db) |
| 160 except psycopg2.OperationalError as message: | 170 except psycopg2.OperationalError as message: |
| 161 raise hyperdb.DatabaseError(message) | 171 raise hyperdb.DatabaseError(message) |
| 162 | 172 |
| 163 cursor = conn.cursor() | 173 cursor = conn.cursor() |
| 164 if ISOLATION_LEVEL_REPEATABLE_READ is not None: | 174 if ISOLATION_LEVEL_REPEATABLE_READ is not None: |
| 165 lvl = isolation_levels [self.config.RDBMS_ISOLATION_LEVEL] | 175 lvl = isolation_levels[self.config.RDBMS_ISOLATION_LEVEL] |
| 166 conn.set_isolation_level(lvl) | 176 conn.set_isolation_level(lvl) |
| 167 | 177 |
| 168 return (conn, cursor) | 178 return (conn, cursor) |
| 169 | 179 |
| 170 def sql_new_cursor(self, name='default', conn=None, *args, **kw): | 180 def sql_new_cursor(self, name='default', conn=None, *args, **kw): |
| 263 # Convert all String properties to TEXT | 273 # Convert all String properties to TEXT |
| 264 self._convert_string_properties() | 274 self._convert_string_properties() |
| 265 | 275 |
| 266 # convert session / OTK *_time columns to REAL | 276 # convert session / OTK *_time columns to REAL |
| 267 for name in ('otk', 'session'): | 277 for name in ('otk', 'session'): |
| 268 self.sql('drop index %ss_key_idx'%name) | 278 self.sql('drop index %ss_key_idx' % name) |
| 269 self.sql('drop table %ss'%name) | 279 self.sql('drop table %ss' % name) |
| 270 self.sql('''CREATE TABLE %ss (%s_key VARCHAR(255), | 280 self.sql('''CREATE TABLE %ss (%s_key VARCHAR(255), |
| 271 %s_value VARCHAR(255), %s_time REAL)'''%(name, name, name, | 281 %s_value VARCHAR(255), %s_time REAL)''' % (name, name, |
| 272 name)) | 282 name, name)) |
| 273 self.sql('CREATE INDEX %ss_key_idx ON %ss(%s_key)'%(name, name, | 283 self.sql('CREATE INDEX %ss_key_idx ON %ss(%s_key)' % (name, name, |
| 274 name)) | 284 name)) |
| 275 | 285 |
| 276 def fix_version_3_tables(self): | 286 def fix_version_3_tables(self): |
| 277 rdbms_common.Database.fix_version_3_tables(self) | 287 rdbms_common.Database.fix_version_3_tables(self) |
| 278 self.sql('''CREATE INDEX words_both_idx ON public.__words | 288 self.sql('''CREATE INDEX words_both_idx ON public.__words |
| 279 USING btree (_word, _textid)''') | 289 USING btree (_word, _textid)''') |
| 280 | 290 |
| 281 def _add_fts_table(self): | 291 def _add_fts_table(self): |
| 282 self.sql('CREATE TABLE __fts (_class VARCHAR(255), ' | 292 self.sql( |
| 283 '_itemid VARCHAR(255), _prop VARCHAR(255), _tsv tsvector)' | 293 'CREATE TABLE __fts (_class VARCHAR(255), ' |
| 294 '_itemid VARCHAR(255), _prop VARCHAR(255), _tsv tsvector)' | |
| 284 ) | 295 ) |
| 285 | 296 |
| 286 self.sql('CREATE INDEX __fts_idx ON __fts USING GIN (_tsv)') | 297 self.sql('CREATE INDEX __fts_idx ON __fts USING GIN (_tsv)') |
| 287 | 298 |
| 288 def fix_version_6_tables(self): | 299 def fix_version_6_tables(self): |
| 306 | 317 |
| 307 def add_new_columns_v2(self): | 318 def add_new_columns_v2(self): |
| 308 # update existing tables to have the new actor column | 319 # update existing tables to have the new actor column |
| 309 tables = self.database_schema['tables'] | 320 tables = self.database_schema['tables'] |
| 310 for name in tables: | 321 for name in tables: |
| 311 self.sql('ALTER TABLE _%s add __actor VARCHAR(255)'%name) | 322 self.sql('ALTER TABLE _%s add __actor VARCHAR(255)' % name) |
| 312 | 323 |
| 313 def __repr__(self): | 324 def __repr__(self): |
| 314 return '<roundpsycopgsql 0x%x>' % id(self) | 325 return '<roundpsycopgsql 0x%x>' % id(self) |
| 315 | 326 |
| 316 def sql_stringquote(self, value): | 327 def sql_stringquote(self, value): |
| 318 single-quotes around it... ''' | 329 single-quotes around it... ''' |
| 319 return str(QuotedString(str(value)))[1:-1] | 330 return str(QuotedString(str(value)))[1:-1] |
| 320 | 331 |
| 321 def sql_index_exists(self, table_name, index_name): | 332 def sql_index_exists(self, table_name, index_name): |
| 322 sql = 'select count(*) from pg_indexes where ' \ | 333 sql = 'select count(*) from pg_indexes where ' \ |
| 323 'tablename=%s and indexname=%s'%(self.arg, self.arg) | 334 'tablename=%s and indexname=%s' % (self.arg, self.arg) |
| 324 self.sql(sql, (table_name, index_name)) | 335 self.sql(sql, (table_name, index_name)) |
| 325 return self.cursor.fetchone()[0] | 336 return self.cursor.fetchone()[0] |
| 326 | 337 |
| 327 def create_class_table(self, spec, create_sequence=1): | 338 def create_class_table(self, spec, create_sequence=1): |
| 328 if create_sequence: | 339 if create_sequence: |
| 329 sql = 'CREATE SEQUENCE _%s_ids'%spec.classname | 340 sql = 'CREATE SEQUENCE _%s_ids' % spec.classname |
| 330 self.sql(sql) | 341 self.sql(sql) |
| 331 | 342 |
| 332 return rdbms_common.Database.create_class_table(self, spec) | 343 return rdbms_common.Database.create_class_table(self, spec) |
| 333 | 344 |
| 334 def drop_class_table(self, cn): | 345 def drop_class_table(self, cn): |
| 335 sql = 'drop table _%s'%cn | 346 sql = 'drop table _%s' % cn |
| 336 self.sql(sql) | 347 self.sql(sql) |
| 337 | 348 |
| 338 sql = 'drop sequence _%s_ids'%cn | 349 sql = 'drop sequence _%s_ids' % cn |
| 339 self.sql(sql) | 350 self.sql(sql) |
| 340 | 351 |
| 341 def newid(self, classname): | 352 def newid(self, classname): |
| 342 sql = "select nextval('_%s_ids') from dual"%classname | 353 sql = "select nextval('_%s_ids') from dual" % classname |
| 343 self.sql(sql) | 354 self.sql(sql) |
| 344 return str(self.cursor.fetchone()[0]) | 355 return str(self.cursor.fetchone()[0]) |
| 345 | 356 |
| 346 def setid(self, classname, setid): | 357 def setid(self, classname, setid): |
| 347 sql = "select setval('_%s_ids', %s) from dual"%(classname, int(setid)) | 358 sql = "select setval('_%s_ids', %s) from dual" % (classname, |
| 359 int(setid)) | |
| 348 self.sql(sql) | 360 self.sql(sql) |
| 349 | 361 |
| 350 def clear(self): | 362 def clear(self): |
| 351 rdbms_common.Database.clear(self) | 363 rdbms_common.Database.clear(self) |
| 352 | 364 |
| 353 # reset the sequences | 365 # reset the sequences |
| 354 for cn in self.classes: | 366 for cn in self.classes: |
| 355 self.cursor.execute('DROP SEQUENCE _%s_ids'%cn) | 367 self.cursor.execute('DROP SEQUENCE _%s_ids' % cn) |
| 356 self.cursor.execute('CREATE SEQUENCE _%s_ids'%cn) | 368 self.cursor.execute('CREATE SEQUENCE _%s_ids' % cn) |
| 369 | |
| 357 | 370 |
| 358 class PostgresqlClass: | 371 class PostgresqlClass: |
| 359 order_by_null_values = '(%s is not NULL)' | 372 order_by_null_values = '(%s is not NULL)' |
| 360 case_insensitive_like = 'ILIKE' | 373 case_insensitive_like = 'ILIKE' |
| 361 | 374 |
| 375 | |
| 362 class Class(PostgresqlClass, rdbms_common.Class): | 376 class Class(PostgresqlClass, rdbms_common.Class): |
| 363 pass | 377 pass |
| 378 | |
| 379 | |
| 364 class IssueClass(PostgresqlClass, rdbms_common.IssueClass): | 380 class IssueClass(PostgresqlClass, rdbms_common.IssueClass): |
| 365 pass | 381 pass |
| 382 | |
| 383 | |
| 366 class FileClass(PostgresqlClass, rdbms_common.FileClass): | 384 class FileClass(PostgresqlClass, rdbms_common.FileClass): |
| 367 pass | 385 pass |
| 368 | 386 |
| 369 # vim: set et sts=4 sw=4 : | 387 # vim: set et sts=4 sw=4 : |
