root / logilab.pylintinstaller / logilab / common / adbh.py

Revision 202:d67e86292521, 18.7 kB (checked in by tziade@…, 9 months ago)

added logilab.pylintinstaller

Line 
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)
14functionalities
15
16Helpers 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
24from logilab.common.deprecation import obsolete
25
26class BadQuery(Exception): pass
27class UnsupportedFunction(BadQuery): pass
28
29
30class metafunc(type):
31    def __new__(mcs, name, bases, dict):
32        dict['name'] = name.upper()
33        return type.__new__(mcs, name, bases, dict)
34
35   
36class 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
61class AggrFunctionDescr(FunctionDescr):
62    aggregat = True
63    rtype = None 
64
65class MAX(AggrFunctionDescr): pass
66class MIN(AggrFunctionDescr): pass
67class SUM(AggrFunctionDescr): pass
68class COUNT(AggrFunctionDescr): 
69    rtype = 'Int'
70class AVG(AggrFunctionDescr):
71    rtype = 'Float'
72
73class UPPER(FunctionDescr):
74    rtype = 'String'
75class LOWER(FunctionDescr):
76    rtype = 'String'
77class IN(FunctionDescr):
78    """this is actually a 'keyword' function..."""
79    maxargs = None
80class LENGTH(FunctionDescr):
81    rtype = 'Int'
82
83class _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);
192INSERT 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
277def 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
286class _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           
393class _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 =