changes for the logo
[odoo/odoo.git] / bin / sql_db.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution   
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import netsvc
24 from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_SERIALIZABLE
25 from psycopg2.pool import ThreadedConnectionPool
26 from psycopg2.psycopg1 import cursor as psycopg1cursor
27
28 import psycopg2.extensions
29
30 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
31
32 types_mapping = {
33     'date': (1082,),
34     'time': (1083,),
35     'datetime': (1114,),
36 }
37
38 def unbuffer(symb, cr):
39     if symb is None: return None
40     return str(symb)
41
42 def undecimalize(symb, cr):
43     if symb is None: return None
44     return float(symb)
45
46 for name, typeoid in types_mapping.items():
47     psycopg2.extensions.register_type(psycopg2.extensions.new_type(typeoid, name, lambda x, cr: x))
48 psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,), 'float', undecimalize))
49
50
51 import tools
52 import re
53
54 from mx import DateTime as mdt
55 re_from = re.compile('.* from "?([a-zA-Z_0-9]+)"? .*$');
56 re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$');
57
58 def log(msg, lvl=netsvc.LOG_DEBUG):
59     logger = netsvc.Logger()
60     logger.notifyChannel('sql', lvl, msg)
61
62 class Cursor(object):
63     IN_MAX = 1000
64     sql_from_log = {}
65     sql_into_log = {}
66     sql_log = False
67     count = 0
68     
69     def check(f):
70         from tools.func import wraps
71
72         @wraps(f)
73         def wrapper(self, *args, **kwargs):
74             if not hasattr(self, '_obj'):
75                 raise psycopg2.ProgrammingError('Unable to use the cursor after having closing it')
76             return f(self, *args, **kwargs)
77         return wrapper
78
79     def __init__(self, pool):
80         self._pool = pool
81         self._cnx = pool.getconn()
82         self._obj = self._cnx.cursor(cursor_factory=psycopg1cursor)
83         self.autocommit(False)
84         self.dbname = pool.dbname
85         
86         from inspect import stack
87         self.__caller = tuple(stack()[2][1:3])
88         
89     def __del__(self):
90         if hasattr(self, '_obj'):
91             # Oops. 'self' has not been closed explicitly.
92             # The cursor will be deleted by the garbage collector, 
93             # but the database connection is not put back into the connection
94             # pool, preventing some operation on the database like dropping it.
95             # This can also lead to a server overload.
96             msg = "Cursor not closed explicitly\n"  \
97                   "Cursor was created at %s:%s" % self.__caller
98
99             log(msg, netsvc.LOG_WARNING)
100             self.close()
101
102     @check
103     def execute(self, query, params=None):
104         self.count+=1
105         if '%d' in query or '%f' in query:
106             log(query, netsvc.LOG_WARNING)
107             log("SQL queries mustn't containt %d or %f anymore. Use only %s", netsvc.LOG_WARNING)
108             if params:
109                 query = query.replace('%d', '%s').replace('%f', '%s')
110
111         if self.sql_log:
112             now = mdt.now()
113         
114         res = self._obj.execute(query, params)
115
116         if self.sql_log:
117             log("query: %s" % self._obj.query)
118             self.count+=1
119             res_from = re_from.match(query.lower())
120             if res_from:
121                 self.sql_from_log.setdefault(res_from.group(1), [0, 0])
122                 self.sql_from_log[res_from.group(1)][0] += 1
123                 self.sql_from_log[res_from.group(1)][1] += mdt.now() - now
124             res_into = re_into.match(query.lower())
125             if res_into:
126                 self.sql_into_log.setdefault(res_into.group(1), [0, 0])
127                 self.sql_into_log[res_into.group(1)][0] += 1
128                 self.sql_into_log[res_into.group(1)][1] += mdt.now() - now
129         return res
130
131     def print_log(self):
132         def process(type):
133             sqllogs = {'from':self.sql_from_log, 'into':self.sql_into_log}
134             if not sqllogs[type]:
135                 return
136             sqllogitems = sqllogs[type].items()
137             sqllogitems.sort(key=lambda k: k[1][1])
138             sum = 0
139             log("SQL LOG %s:" % (type,))
140             for r in sqllogitems:
141                 log("table: %s: %s/%s" %(r[0], str(r[1][1]), r[1][0]))
142                 sum+= r[1][1]
143             log("SUM:%s/%d" % (sum, self.count))
144             sqllogs[type].clear()
145         process('from')
146         process('into')
147         self.count = 0
148         self.sql_log = False
149
150     @check
151     def close(self):
152         self.print_log()
153         self._obj.close()
154
155         # This force the cursor to be freed, and thus, available again. It is
156         # important because otherwise we can overload the server very easily
157         # because of a cursor shortage (because cursors are not garbage
158         # collected as fast as they should). The problem is probably due in
159         # part because browse records keep a reference to the cursor.
160         del self._obj
161         self._pool.putconn(self._cnx)
162     
163     @check
164     def autocommit(self, on):
165         self._cnx.set_isolation_level([ISOLATION_LEVEL_SERIALIZABLE, ISOLATION_LEVEL_AUTOCOMMIT][bool(on)])
166     
167     @check
168     def commit(self):
169         return self._cnx.commit()
170     
171     @check
172     def rollback(self):
173         return self._cnx.rollback()
174
175     @check
176     def __getattr__(self, name):
177         return getattr(self._obj, name)
178
179 class ConnectionPool(object):
180     def __init__(self, pool, dbname):
181         self.dbname = dbname
182         self._pool = pool
183
184     def cursor(self):
185         return Cursor(self)
186
187     def __getattr__(self, name):
188         return getattr(self._pool, name)
189
190 class PoolManager(object):
191     _pools = {}
192     _dsn = None
193     maxconn =  int(tools.config['db_maxconn']) or 64
194     
195     @classmethod 
196     def dsn(cls, db_name):
197         if cls._dsn is None:
198             cls._dsn = ''
199             for p in ('host', 'port', 'user', 'password'):
200                 cfg = tools.config['db_' + p]
201                 if cfg:
202                     cls._dsn += '%s=%s ' % (p, cfg)
203         return '%s dbname=%s' % (cls._dsn, db_name)
204
205     @classmethod
206     def get(cls, db_name):
207         if db_name not in cls._pools:
208             logger = netsvc.Logger()
209             try:
210                 logger.notifyChannel('dbpool', netsvc.LOG_INFO, 'Connecting to %s' % (db_name,))
211                 cls._pools[db_name] = ConnectionPool(ThreadedConnectionPool(1, cls.maxconn, cls.dsn(db_name)), db_name)
212             except Exception, e:
213                 logger.notifyChannel('dbpool', netsvc.LOG_ERROR, 'Unable to connect to %s: %s' %
214                                      (db_name, str(e)))
215                 raise
216         return cls._pools[db_name]
217
218     @classmethod
219     def close(cls, db_name):
220         if db_name in cls._pools:
221             logger = netsvc.Logger()
222             logger.notifyChannel('dbpool', netsvc.LOG_INFO, 'Closing all connections to %s' % (db_name,))
223             cls._pools[db_name].closeall()
224             del cls._pools[db_name]
225
226 def db_connect(db_name):
227     return PoolManager.get(db_name)
228
229 def close_db(db_name):
230     PoolManager.close(db_name)
231     tools.cache.clean_caches_for_db(db_name)
232     
233
234 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
235