17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """SQL utilities functions and classes.""" |
18 """SQL utilities functions and classes.""" |
19 |
19 |
20 __docformat__ = "restructuredtext en" |
20 __docformat__ = "restructuredtext en" |
21 |
21 |
|
22 import sys |
22 import os |
23 import os |
23 import re |
24 import re |
24 import subprocess |
25 import subprocess |
25 from datetime import datetime, date |
26 from os.path import abspath |
26 from itertools import ifilter |
27 from itertools import ifilter |
|
28 from logging import getLogger |
27 |
29 |
28 from logilab import database as db, common as lgc |
30 from logilab import database as db, common as lgc |
29 from logilab.common.shellutils import ProgressBar |
31 from logilab.common.shellutils import ProgressBar |
30 from logilab.common.date import todate, todatetime, utcdatetime, utctime |
32 from logilab.common.deprecation import deprecated |
|
33 from logilab.common.logging_ext import set_log_methods |
31 from logilab.database.sqlgen import SQLGenerator |
34 from logilab.database.sqlgen import SQLGenerator |
32 |
35 |
33 from cubicweb import Binary, ConfigurationError, server |
36 from cubicweb import Binary, ConfigurationError |
34 from cubicweb.uilib import remove_html_tags |
37 from cubicweb.uilib import remove_html_tags |
35 from cubicweb.schema import PURE_VIRTUAL_RTYPES |
38 from cubicweb.schema import PURE_VIRTUAL_RTYPES |
36 from cubicweb.server import SQL_CONNECT_HOOKS |
39 from cubicweb.server import SQL_CONNECT_HOOKS |
37 from cubicweb.server.utils import crypt_password |
40 from cubicweb.server.utils import crypt_password |
38 from rql.utils import RQL_FUNCTIONS_REGISTRY |
|
39 |
41 |
40 lgc.USE_MX_DATETIME = False |
42 lgc.USE_MX_DATETIME = False |
41 SQL_PREFIX = 'cw_' |
43 SQL_PREFIX = 'cw_' |
42 |
44 |
43 def _run_command(cmd): |
45 def _run_command(cmd): |
176 cmds += ['DROP TABLE %s;' % name |
178 cmds += ['DROP TABLE %s;' % name |
177 for name in ifilter(_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION, dbhelper.list_tables(sqlcursor))] |
179 for name in ifilter(_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION, dbhelper.list_tables(sqlcursor))] |
178 return '\n'.join(cmds) |
180 return '\n'.join(cmds) |
179 |
181 |
180 |
182 |
|
183 class ConnectionWrapper(object): |
|
184 """handle connection to the system source, at some point associated to a |
|
185 :class:`Session` |
|
186 """ |
|
187 |
|
188 # since 3.19, we only have to manage the system source connection |
|
189 def __init__(self, system_source): |
|
190 # dictionary of (source, connection), indexed by sources'uri |
|
191 self._source = system_source |
|
192 self.cnx = system_source.get_connection() |
|
193 self.cu = self.cnx.cursor() |
|
194 |
|
195 def commit(self): |
|
196 """commit the current transaction for this user""" |
|
197 # let exception propagates |
|
198 self.cnx.commit() |
|
199 |
|
200 def rollback(self): |
|
201 """rollback the current transaction for this user""" |
|
202 # catch exceptions, rollback other sources anyway |
|
203 try: |
|
204 self.cnx.rollback() |
|
205 except Exception: |
|
206 self._source.critical('rollback error', exc_info=sys.exc_info()) |
|
207 # error on rollback, the connection is much probably in a really |
|
208 # bad state. Replace it by a new one. |
|
209 self.reconnect() |
|
210 |
|
211 def close(self, i_know_what_i_do=False): |
|
212 """close all connections in the set""" |
|
213 if i_know_what_i_do is not True: # unexpected closing safety belt |
|
214 raise RuntimeError('connections set shouldn\'t be closed') |
|
215 try: |
|
216 self.cu.close() |
|
217 self.cu = None |
|
218 except Exception: |
|
219 pass |
|
220 try: |
|
221 self.cnx.close() |
|
222 self.cnx = None |
|
223 except Exception: |
|
224 pass |
|
225 |
|
226 # internals ############################################################### |
|
227 |
|
228 def cnxset_freed(self): |
|
229 """connections set is being freed from a session""" |
|
230 pass # no nothing by default |
|
231 |
|
232 def reconnect(self): |
|
233 """reopen a connection for this source or all sources if none specified |
|
234 """ |
|
235 try: |
|
236 # properly close existing connection if any |
|
237 self.cnx.close() |
|
238 except Exception: |
|
239 pass |
|
240 self._source.info('trying to reconnect') |
|
241 self.cnx = self._source.get_connection() |
|
242 self.cu = self.cnx.cursor() |
|
243 |
|
244 @deprecated('[3.19] use .cu instead') |
|
245 def __getitem__(self, uri): |
|
246 assert uri == 'system' |
|
247 return self.cu |
|
248 |
|
249 @deprecated('[3.19] use repo.system_source instead') |
|
250 def source(self, uid): |
|
251 assert uid == 'system' |
|
252 return self._source |
|
253 |
|
254 @deprecated('[3.19] use .cnx instead') |
|
255 def connection(self, uid): |
|
256 assert uid == 'system' |
|
257 return self.cnx |
|
258 |
|
259 |
|
260 class SqliteConnectionWrapper(ConnectionWrapper): |
|
261 """Sqlite specific connection wrapper: close the connection each time it's |
|
262 freed (and reopen it later when needed) |
|
263 """ |
|
264 def __init__(self, system_source): |
|
265 # don't call parent's __init__, we don't want to initiate the connection |
|
266 self._source = system_source |
|
267 |
|
268 _cnx = None |
|
269 |
|
270 def cnxset_freed(self): |
|
271 self.cu.close() |
|
272 self.cnx.close() |
|
273 self.cnx = self.cu = None |
|
274 |
|
275 @property |
|
276 def cnx(self): |
|
277 if self._cnx is None: |
|
278 self._cnx = self._source.get_connection() |
|
279 self._cu = self._cnx.cursor() |
|
280 return self._cnx |
|
281 @cnx.setter |
|
282 def cnx(self, value): |
|
283 self._cnx = value |
|
284 |
|
285 @property |
|
286 def cu(self): |
|
287 if self._cnx is None: |
|
288 self._cnx = self._source.get_connection() |
|
289 self._cu = self._cnx.cursor() |
|
290 return self._cu |
|
291 @cu.setter |
|
292 def cu(self, value): |
|
293 self._cu = value |
|
294 |
|
295 |
181 class SQLAdapterMixIn(object): |
296 class SQLAdapterMixIn(object): |
182 """Mixin for SQL data sources, getting a connection from a configuration |
297 """Mixin for SQL data sources, getting a connection from a configuration |
183 dictionary and handling connection locking |
298 dictionary and handling connection locking |
184 """ |
299 """ |
|
300 cnx_wrap = ConnectionWrapper |
185 |
301 |
186 def __init__(self, source_config): |
302 def __init__(self, source_config): |
187 try: |
303 try: |
188 self.dbdriver = source_config['db-driver'].lower() |
304 self.dbdriver = source_config['db-driver'].lower() |
189 dbname = source_config['db-name'] |
305 dbname = source_config['db-name'] |
207 self.InterfaceError = dbapi_module.InterfaceError |
323 self.InterfaceError = dbapi_module.InterfaceError |
208 self.DbapiError = dbapi_module.Error |
324 self.DbapiError = dbapi_module.Error |
209 self._binary = self.dbhelper.binary_value |
325 self._binary = self.dbhelper.binary_value |
210 self._process_value = dbapi_module.process_value |
326 self._process_value = dbapi_module.process_value |
211 self._dbencoding = dbencoding |
327 self._dbencoding = dbencoding |
|
328 if self.dbdriver == 'sqlite': |
|
329 self.cnx_wrap = SqliteConnectionWrapper |
|
330 self.dbhelper.dbname = abspath(self.dbhelper.dbname) |
|
331 |
|
332 def wrapped_connection(self): |
|
333 """open and return a connection to the database, wrapped into a class |
|
334 handling reconnection and all |
|
335 """ |
|
336 return self.cnx_wrap(self) |
212 |
337 |
213 def get_connection(self): |
338 def get_connection(self): |
214 """open and return a connection to the database""" |
339 """open and return a connection to the database""" |
215 return self.dbhelper.get_connection() |
340 return self.dbhelper.get_connection() |
216 |
341 |
318 |
443 |
319 # these are overridden by set_log_methods below |
444 # these are overridden by set_log_methods below |
320 # only defining here to prevent pylint from complaining |
445 # only defining here to prevent pylint from complaining |
321 info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None |
446 info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None |
322 |
447 |
323 from logging import getLogger |
|
324 from cubicweb import set_log_methods |
|
325 set_log_methods(SQLAdapterMixIn, getLogger('cubicweb.sqladapter')) |
448 set_log_methods(SQLAdapterMixIn, getLogger('cubicweb.sqladapter')) |
326 |
449 |
327 |
450 |
328 class SqliteCnxLoggingWrapper(object): |
451 # connection initialization functions ########################################## |
329 def __init__(self, source=None): |
|
330 self.source = source |
|
331 self._cnx = None |
|
332 |
|
333 def cursor(self): |
|
334 # sqlite connections can only be used in the same thread, so |
|
335 # create a new one each time necessary. If it appears to be time |
|
336 # consuming, find another way |
|
337 if self._cnx is None: |
|
338 # direct access to SQLAdapterMixIn to get an unwrapped connection |
|
339 self._cnx = SQLAdapterMixIn.get_connection(self.source) |
|
340 if server.DEBUG & server.DBG_SQL: |
|
341 print 'sql cnx OPEN', self._cnx |
|
342 return self._cnx.cursor() |
|
343 |
|
344 def commit(self): |
|
345 if self._cnx is not None: |
|
346 if server.DEBUG & (server.DBG_SQL | server.DBG_RQL): |
|
347 print 'sql cnx COMMIT', self._cnx |
|
348 self._cnx.commit() |
|
349 |
|
350 def rollback(self): |
|
351 if self._cnx is not None: |
|
352 if server.DEBUG & (server.DBG_SQL | server.DBG_RQL): |
|
353 print 'sql cnx ROLLBACK', self._cnx |
|
354 self._cnx.rollback() |
|
355 |
|
356 def close(self): |
|
357 if self._cnx is not None: |
|
358 if server.DEBUG & server.DBG_SQL: |
|
359 print 'sql cnx CLOSE', self._cnx |
|
360 self._cnx.close() |
|
361 self._cnx = None |
|
362 |
|
363 |
452 |
364 def init_sqlite_connexion(cnx): |
453 def init_sqlite_connexion(cnx): |
365 |
454 |
366 class group_concat(object): |
455 class group_concat(object): |
367 def __init__(self): |
456 def __init__(self): |