| 1 | # This program is free software; you can redistribute it and/or modify it under |
|---|
| 2 | # the terms of the GNU General Public License as published by the Free Software |
|---|
| 3 | # Foundation; either version 2 of the License, or (at your option) any later |
|---|
| 4 | # version. |
|---|
| 5 | # |
|---|
| 6 | # This program is distributed in the hope that it will be useful, but WITHOUT |
|---|
| 7 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 8 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
|---|
| 9 | # |
|---|
| 10 | # You should have received a copy of the GNU General Public License along with |
|---|
| 11 | # this program; if not, write to the Free Software Foundation, Inc., |
|---|
| 12 | # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
|---|
| 13 | """This module contains helpers for DBMS specific (advanced or non standard) |
|---|
| 14 | functionalities |
|---|
| 15 | |
|---|
| 16 | Helpers are provided for postgresql, mysql and sqlite. |
|---|
| 17 | |
|---|
| 18 | :author: Logilab |
|---|
| 19 | :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE) |
|---|
| 20 | :contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org |
|---|
| 21 | """ |
|---|
| 22 | __docformat__ = "restructuredtext en" |
|---|
| 23 | |
|---|
| 24 | from logilab.common.deprecation import obsolete |
|---|
| 25 | |
|---|
| 26 | class BadQuery(Exception): pass |
|---|
| 27 | class UnsupportedFunction(BadQuery): pass |
|---|
| 28 | |
|---|
| 29 | |
|---|
| 30 | class metafunc(type): |
|---|
| 31 | def __new__(mcs, name, bases, dict): |
|---|
| 32 | dict['name'] = name.upper() |
|---|
| 33 | return type.__new__(mcs, name, bases, dict) |
|---|
| 34 | |
|---|
| 35 | |
|---|
| 36 | class FunctionDescr(object): |
|---|
| 37 | __metaclass__ = metafunc |
|---|
| 38 | |
|---|
| 39 | rtype = None # None <-> returned type should be the same as the first argument |
|---|
| 40 | aggregat = False |
|---|
| 41 | minargs = 1 |
|---|
| 42 | maxargs = 1 |
|---|
| 43 | |
|---|
| 44 | def __init__(self, name=None, rtype=rtype, aggregat=aggregat): |
|---|
| 45 | if name is not None: |
|---|
| 46 | name = name.upper() |
|---|
| 47 | self.name = name |
|---|
| 48 | self.rtype = rtype |
|---|
| 49 | self.aggregat = aggregat |
|---|
| 50 | |
|---|
| 51 | #@classmethod |
|---|
| 52 | def check_nbargs(cls, nbargs): |
|---|
| 53 | if cls.minargs is not None and \ |
|---|
| 54 | nbargs < cls.minargs: |
|---|
| 55 | raise BadQuery('not enough argument for function %s' % cls.name) |
|---|
| 56 | if cls.maxargs is not None and \ |
|---|
| 57 | nbargs < cls.maxargs: |
|---|
| 58 | raise BadQuery('too many arguments for function %s' % cls.name) |
|---|
| 59 | check_nbargs = classmethod(check_nbargs) |
|---|
| 60 | |
|---|
| 61 | class AggrFunctionDescr(FunctionDescr): |
|---|
| 62 | aggregat = True |
|---|
| 63 | rtype = None |
|---|
| 64 | |
|---|
| 65 | class MAX(AggrFunctionDescr): pass |
|---|
| 66 | class MIN(AggrFunctionDescr): pass |
|---|
| 67 | class SUM(AggrFunctionDescr): pass |
|---|
| 68 | class COUNT(AggrFunctionDescr): |
|---|
| 69 | rtype = 'Int' |
|---|
| 70 | class AVG(AggrFunctionDescr): |
|---|
| 71 | rtype = 'Float' |
|---|
| 72 | |
|---|
| 73 | class UPPER(FunctionDescr): |
|---|
| 74 | rtype = 'String' |
|---|
| 75 | class LOWER(FunctionDescr): |
|---|
| 76 | rtype = 'String' |
|---|
| 77 | class IN(FunctionDescr): |
|---|
| 78 | """this is actually a 'keyword' function...""" |
|---|
| 79 | maxargs = None |
|---|
| 80 | class LENGTH(FunctionDescr): |
|---|
| 81 | rtype = 'Int' |
|---|
| 82 | |
|---|
| 83 | class _GenericAdvFuncHelper: |
|---|
| 84 | """Generic helper, trying to provide generic way to implement |
|---|
| 85 | specific functionnalities from others DBMS |
|---|
| 86 | |
|---|
| 87 | An exception is raised when the functionality is not emulatable |
|---|
| 88 | """ |
|---|
| 89 | # DBMS resources descriptors and accessors |
|---|
| 90 | |
|---|
| 91 | needs_from_clause = False |
|---|
| 92 | |
|---|
| 93 | users_support = True |
|---|
| 94 | groups_support = True |
|---|
| 95 | ilike_support = True |
|---|
| 96 | |
|---|
| 97 | FUNCTIONS = { |
|---|
| 98 | # aggregat functions |
|---|
| 99 | 'MIN': MIN, 'MAX': MAX, |
|---|
| 100 | 'SUM': SUM, |
|---|
| 101 | 'COUNT': COUNT, |
|---|
| 102 | 'AVG': AVG, |
|---|
| 103 | # transformation functions |
|---|
| 104 | 'UPPER': UPPER, 'LOWER': LOWER, |
|---|
| 105 | 'LENGTH': LENGTH, |
|---|
| 106 | # keyword function |
|---|
| 107 | 'IN': IN |
|---|
| 108 | } |
|---|
| 109 | |
|---|
| 110 | TYPE_MAPPING = { |
|---|
| 111 | 'String' : 'text', |
|---|
| 112 | 'Int' : 'integer', |
|---|
| 113 | 'Float' : 'float', |
|---|
| 114 | 'Boolean' : 'boolean', |
|---|
| 115 | 'Date' : 'date', |
|---|
| 116 | 'Time' : 'time', |
|---|
| 117 | 'Datetime' : 'timestamp', |
|---|
| 118 | 'Interval' : 'interval', |
|---|
| 119 | 'Password' : 'bytea', |
|---|
| 120 | 'Bytes' : 'bytea', |
|---|
| 121 | # FIXME: still there for use from erudi, should be moved out |
|---|
| 122 | # XXX think it can be safely removed now |
|---|
| 123 | 'COUNT' : 'integer', |
|---|
| 124 | 'MIN' : 'integer', |
|---|
| 125 | 'MAX' : 'integer', |
|---|
| 126 | 'SUM' : 'integer', |
|---|
| 127 | 'LOWER' : 'text', |
|---|
| 128 | 'UPPER' : 'text', |
|---|
| 129 | 'LENGTH' :'integer', |
|---|
| 130 | } |
|---|
| 131 | |
|---|
| 132 | |
|---|
| 133 | #@classmethod |
|---|
| 134 | def register_function(cls, funcdef): |
|---|
| 135 | if isinstance(funcdef, basestring) : |
|---|
| 136 | funcdef = FunctionDescr(funcdef.upper()) |
|---|
| 137 | assert not funcdef.name in cls.FUNCTIONS, \ |
|---|
| 138 | '%s is already registered' % funcdef.name |
|---|
| 139 | cls.FUNCTIONS[funcdef.name] = funcdef |
|---|
| 140 | register_function = classmethod(register_function) |
|---|
| 141 | |
|---|
| 142 | #@classmethod |
|---|
| 143 | def function_description(cls, funcname): |
|---|
| 144 | """return the description (`FunctionDescription`) for a RQL function""" |
|---|
| 145 | try: |
|---|
| 146 | return cls.FUNCTIONS[funcname.upper()] |
|---|
| 147 | except KeyError: |
|---|
| 148 | raise UnsupportedFunction(funcname) |
|---|
| 149 | function_description = classmethod(function_description) |
|---|
| 150 | |
|---|
| 151 | #@obsolete('use users_support attribute') |
|---|
| 152 | def support_users(self): |
|---|
| 153 | """return True if the DBMS support users (this is usually |
|---|
| 154 | not true for in memory DBMS) |
|---|
| 155 | """ |
|---|
| 156 | return self.users_support |
|---|
| 157 | support_user = obsolete('use users_support attribute')(support_users) |
|---|
| 158 | |
|---|
| 159 | #@obsolete('use groups_support attribute') |
|---|
| 160 | def support_groups(self): |
|---|
| 161 | """return True if the DBMS support groups""" |
|---|
| 162 | return self.groups_support |
|---|
| 163 | support_user = obsolete('use groups_support attribute')(support_groups) |
|---|
| 164 | |
|---|
| 165 | def system_database(self): |
|---|
| 166 | """return the system database for the given driver""" |
|---|
| 167 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 168 | |
|---|
| 169 | def backup_command(self, dbname, dbhost, dbuser, dbpassword, backupfile, |
|---|
| 170 | keepownership=True): |
|---|
| 171 | """return a command to backup the given database""" |
|---|
| 172 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 173 | |
|---|
| 174 | def restore_commands(self, dbname, dbhost, dbuser, backupfile, |
|---|
| 175 | encoding='utf-8', keepownership=True, drop=True): |
|---|
| 176 | """return a list of commands to restore a backup the given database""" |
|---|
| 177 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 178 | |
|---|
| 179 | # helpers to standardize SQL according to the database |
|---|
| 180 | |
|---|
| 181 | def sql_current_date(self): |
|---|
| 182 | return 'CURRENT_DATE' |
|---|
| 183 | |
|---|
| 184 | def sql_current_time(self): |
|---|
| 185 | return 'CURRENT_TIME' |
|---|
| 186 | |
|---|
| 187 | def sql_current_timestamp(self): |
|---|
| 188 | return 'CURRENT_TIMESTAMP' |
|---|
| 189 | |
|---|
| 190 | def sql_create_sequence(self, seq_name): |
|---|
| 191 | return '''CREATE TABLE %s (last INTEGER); |
|---|
| 192 | INSERT INTO %s VALUES (0);''' % (seq_name, seq_name) |
|---|
| 193 | |
|---|
| 194 | def sql_create_index(self, table, column, unique=False): |
|---|
| 195 | idx = self._index_name(table, column, unique) |
|---|
| 196 | if unique: |
|---|
| 197 | return 'CREATE UNIQUE INDEX %s ON %s(%s);' % (idx, table, column) |
|---|
| 198 | else: |
|---|
| 199 | return 'CREATE INDEX %s ON %s(%s);' % (idx, table, column) |
|---|
| 200 | |
|---|
| 201 | def sql_drop_sequence(self, seq_name): |
|---|
| 202 | return 'DROP TABLE %s;' % seq_name |
|---|
| 203 | |
|---|
| 204 | def sqls_increment_sequence(self, seq_name): |
|---|
| 205 | return ('UPDATE %s SET last=last+1;' % seq_name, |
|---|
| 206 | 'SELECT last FROM %s;' % seq_name) |
|---|
| 207 | |
|---|
| 208 | def sql_temporary_table(self, table_name, table_schema, |
|---|
| 209 | drop_on_commit=True): |
|---|
| 210 | return "CREATE TEMPORARY TABLE %s (%s);" % (table_name, table_schema) |
|---|
| 211 | |
|---|
| 212 | def boolean_value(self, value): |
|---|
| 213 | if value: |
|---|
| 214 | return 'TRUE' |
|---|
| 215 | else: |
|---|
| 216 | return 'FALSE' |
|---|
| 217 | |
|---|
| 218 | def increment_sequence(self, cursor, seq_name): |
|---|
| 219 | for sql in self.sqls_increment_sequence(seq_name): |
|---|
| 220 | cursor.execute(sql) |
|---|
| 221 | return cursor.fetchone()[0] |
|---|
| 222 | |
|---|
| 223 | def create_user(self, cursor, user, password): |
|---|
| 224 | """create a new database user""" |
|---|
| 225 | if not self.users_support: |
|---|
| 226 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 227 | cursor.execute("CREATE USER %(user)s " |
|---|
| 228 | "WITH PASSWORD '%(password)s'" % locals()) |
|---|
| 229 | |
|---|
| 230 | def _index_name(self, table, column, unique=False): |
|---|
| 231 | if unique: |
|---|
| 232 | # note: this naming is consistent with indices automatically |
|---|
| 233 | # created by postgres when UNIQUE appears in a table schema |
|---|
| 234 | return '%s_%s_key' % (table.lower(), column.lower()) |
|---|
| 235 | else: |
|---|
| 236 | return '%s_%s_idx' % (table.lower(), column.lower()) |
|---|
| 237 | |
|---|
| 238 | def create_index(self, cursor, table, column, unique=False): |
|---|
| 239 | if not self.index_exists(cursor, table, column, unique): |
|---|
| 240 | cursor.execute(self.sql_create_index(table, column, unique)) |
|---|
| 241 | |
|---|
| 242 | def drop_index(self, cursor, table, column, unique=False): |
|---|
| 243 | if self.index_exists(cursor, table, column, unique): |
|---|
| 244 | idx = self._index_name(table, column, unique) |
|---|
| 245 | cursor.execute('DROP INDEX %s' % idx) |
|---|
| 246 | |
|---|
| 247 | def index_exists(self, cursor, table, column, unique=False): |
|---|
| 248 | idx = self._index_name(table, column, unique) |
|---|
| 249 | return idx in self.list_indices(cursor, table) |
|---|
| 250 | |
|---|
| 251 | def user_exists(self, cursor, username): |
|---|
| 252 | """return True if a user with the given username exists""" |
|---|
| 253 | return username in self.list_users(cursor) |
|---|
| 254 | |
|---|
| 255 | def list_users(self, cursor): |
|---|
| 256 | """return the list of existing database users""" |
|---|
| 257 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 258 | |
|---|
| 259 | def create_database(self, cursor, dbname, owner=None, encoding='utf-8'): |
|---|
| 260 | """create a new database""" |
|---|
| 261 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 262 | |
|---|
| 263 | def list_databases(self): |
|---|
| 264 | """return the list of existing databases""" |
|---|
| 265 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 266 | |
|---|
| 267 | def list_tables(self, cursor): |
|---|
| 268 | """return the list of tables of a database""" |
|---|
| 269 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 270 | |
|---|
| 271 | def list_indices(self, cursor, table=None): |
|---|
| 272 | """return the list of indices of a database, only for the given table if specified""" |
|---|
| 273 | raise NotImplementedError('not supported by this DBMS') |
|---|
| 274 | |
|---|
| 275 | |
|---|
| 276 | |
|---|
| 277 | def pgdbcmd(cmd, dbhost, dbuser): |
|---|
| 278 | cmd = [cmd] |
|---|
| 279 | if dbhost: |
|---|
| 280 | cmd.append('--host=%s' % dbhost) |
|---|
| 281 | if dbuser: |
|---|
| 282 | cmd.append('--username=%s' % dbuser) |
|---|
| 283 | return cmd |
|---|
| 284 | |
|---|
| 285 | |
|---|
| 286 | class _PGAdvFuncHelper(_GenericAdvFuncHelper): |
|---|
| 287 | """Postgres helper, taking advantage of postgres SEQUENCE support |
|---|
| 288 | """ |
|---|
| 289 | # modifiable but should not be shared |
|---|
| 290 | FUNCTIONS = _GenericAdvFuncHelper.FUNCTIONS.copy() |
|---|
| 291 | |
|---|
| 292 | def system_database(self): |
|---|
| 293 | """return the system database for the given driver""" |
|---|
| 294 | return 'template1' |
|---|
| 295 | |
|---|
| 296 | def backup_command(self, dbname, dbhost, dbuser, backupfile, |
|---|
| 297 | keepownership=True): |
|---|
| 298 | """return a command to backup the given database""" |
|---|
| 299 | cmd = ['pg_dump -Fc'] |
|---|
| 300 | if dbhost: |
|---|
| 301 | cmd.append('--host=%s' % dbhost) |
|---|
| 302 | if dbuser: |
|---|
| 303 | cmd.append('--username=%s' % dbuser) |
|---|
| 304 | if not keepownership: |
|---|
| 305 | cmd.append('--no-owner') |
|---|
| 306 | cmd.append('--file=%s' % backupfile) |
|---|
| 307 | cmd.append(dbname) |
|---|
| 308 | return ' '.join(cmd) |
|---|
| 309 | |
|---|
| 310 | def restore_commands(self, dbname, dbhost, dbuser, backupfile, |
|---|
| 311 | encoding='utf-8', keepownership=True, drop=True): |
|---|
| 312 | """return a list of commands to restore a backup the given database""" |
|---|
| 313 | cmds = [] |
|---|
| 314 | if drop: |
|---|
| 315 | cmd = pgdbcmd('dropdb', dbhost, dbuser) |
|---|
| 316 | cmd.append(dbname) |
|---|
| 317 | cmds.append(' '.join(cmd)) |
|---|
| 318 | cmd = pgdbcmd('createdb -T template0 -E %s' % encoding, dbhost, dbuser) |
|---|
| 319 | cmd.append(dbname) |
|---|
| 320 | cmds.append(' '.join(cmd)) |
|---|
| 321 | cmd = pgdbcmd('pg_restore -Fc', dbhost, dbuser) |
|---|
| 322 | cmd.append('--dbname %s' % dbname) |
|---|
| 323 | if not keepownership: |
|---|
| 324 | cmd.append('--no-owner') |
|---|
| 325 | cmd.append(backupfile) |
|---|
| 326 | cmds.append(' '.join(cmd)) |
|---|
| 327 | return cmds |
|---|
| 328 | |
|---|
| 329 | def sql_create_sequence(self, seq_name): |
|---|
| 330 | return 'CREATE SEQUENCE %s;' % seq_name |
|---|
| 331 | |
|---|
| 332 | def sql_drop_sequence(self, seq_name): |
|---|
| 333 | return 'DROP SEQUENCE %s;' % seq_name |
|---|
| 334 | |
|---|
| 335 | def sqls_increment_sequence(self, seq_name): |
|---|
| 336 | return ("SELECT nextval('%s');" % seq_name,) |
|---|
| 337 | |
|---|
| 338 | def sql_temporary_table(self, table_name, table_schema, |
|---|
| 339 | drop_on_commit=True): |
|---|
| 340 | if not drop_on_commit: |
|---|
| 341 | return "CREATE TEMPORARY TABLE %s (%s);" % (table_name, |
|---|
| 342 | table_schema) |
|---|
| 343 | return "CREATE TEMPORARY TABLE %s (%s) ON COMMIT DROP;" % (table_name, |
|---|
| 344 | table_schema) |
|---|
| 345 | |
|---|
| 346 | def create_database(self, cursor, dbname, owner=None, encoding='utf-8'): |
|---|
| 347 | """create a new database""" |
|---|
| 348 | sql = "CREATE DATABASE %(dbname)s" |
|---|
| 349 | if owner: |
|---|
| 350 | sql += " WITH OWNER=%(owner)s" |
|---|
| 351 | if encoding: |
|---|
| 352 | sql += " ENCODING='%(encoding)s'" |
|---|
| 353 | cursor.execute(sql % locals()) |
|---|
| 354 | |
|---|
| 355 | def create_language(self, cursor, extlang): |
|---|
| 356 | """postgres specific method to install a procedural language on a database""" |
|---|
| 357 | # make sure plpythonu is not directly in template1 |
|---|
| 358 | cursor.execute("SELECT * FROM pg_language WHERE lanname='%s';" % extlang) |
|---|
| 359 | if cursor.fetchall(): |
|---|
| 360 | print '%s language already installed' % extlang |
|---|
| 361 | else: |
|---|
| 362 | cursor.execute('CREATE LANGUAGE %s' % extlang) |
|---|
| 363 | print '%s language installed' % extlang |
|---|
| 364 | |
|---|
| 365 | def list_users(self, cursor, username=None): |
|---|
| 366 | """return the list of existing database users""" |
|---|
| 367 | if username: |
|---|
| 368 | warn('username argument is deprecated, use user_exists method', |
|---|
| 369 | DeprecationWarning, stacklevel=2) |
|---|
| 370 | return self.user_exists(cursor, username) |
|---|
| 371 | cursor.execute("SELECT usename FROM pg_user") |
|---|
| 372 | return [r[0] for r in cursor.fetchall()] |
|---|
| 373 | |
|---|
| 374 | def list_databases(self, cursor): |
|---|
| 375 | """return the list of existing databases""" |
|---|
| 376 | cursor.execute('SELECT datname FROM pg_database') |
|---|
| 377 | return [r[0] for r in cursor.fetchall()] |
|---|
| 378 | |
|---|
| 379 | def list_tables(self, cursor): |
|---|
| 380 | """return the list of tables of a database""" |
|---|
| 381 | cursor.execute("SELECT tablename FROM pg_tables") |
|---|
| 382 | return [r[0] for r in cursor.fetchall()] |
|---|
| 383 | |
|---|
| 384 | def list_indices(self, cursor, table=None): |
|---|
| 385 | """return the list of indices of a database, only for the given table if specified""" |
|---|
| 386 | sql = "SELECT indexname FROM pg_indexes" |
|---|
| 387 | if table: |
|---|
| 388 | sql += " WHERE LOWER(tablename)='%s'" % table.lower() |
|---|
| 389 | cursor.execute(sql) |
|---|
| 390 | return [r[0] for r in cursor.fetchall()] |
|---|
| 391 | |
|---|
| 392 | |
|---|
| 393 | class _SqliteAdvFuncHelper(_GenericAdvFuncHelper): |
|---|
| 394 | """Generic helper, trying to provide generic way to implement |
|---|
| 395 | specific functionnalities from others DBMS |
|---|
| 396 | |
|---|
| 397 | An exception is raised when the functionality is not emulatable |
|---|
| 398 | """ |
|---|
| 399 | # modifiable but should not be shared |
|---|
| 400 | FUNCTIONS = _GenericAdvFuncHelper.FUNCTIONS.copy() |
|---|
| 401 | |
|---|
| 402 | users_support = |
|---|