1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import importlib
18 import os
19 import time
20 import urllib
21 from abc import ABCMeta, abstractmethod
22
23 from trac import db_default
24 from trac.api import IEnvironmentSetupParticipant, ISystemInfoProvider
25 from trac.config import BoolOption, ConfigurationError, IntOption, Option
26 from trac.core import *
27 from trac.db.pool import ConnectionPool
28 from trac.db.schema import Table
29 from trac.db.util import ConnectionWrapper
30 from trac.util.concurrency import ThreadLocal
31 from trac.util.html import tag
32 from trac.util.text import unicode_passwd
33 from trac.util.translation import _, tag_
34
35
36 -class DbContextManager(object):
37 """Database Context Manager
38
39 The outermost `DbContextManager` will close the connection.
40 """
41
42 db = None
43
44 - def __init__(self, env):
45 self.dbmgr = DatabaseManager(env)
46
47 - def execute(self, query, params=None):
48 """Shortcut for directly executing a query."""
49 with self as db:
50 return db.execute(query, params)
51
52 __call__ = execute
53
54 - def executemany(self, query, params=None):
55 """Shortcut for directly calling "executemany" on a query."""
56 with self as db:
57 return db.executemany(query, params)
58
59
60 -class TransactionContextManager(DbContextManager):
61 """Transactioned Database Context Manager for retrieving a
62 `~trac.db.util.ConnectionWrapper`.
63
64 The outermost such context manager will perform a commit upon
65 normal exit or a rollback after an exception.
66 """
67
68 - def __enter__(self):
69 db = self.dbmgr._transaction_local.wdb
70 if not db:
71 db = self.dbmgr._transaction_local.rdb
72 if db:
73 db = ConnectionWrapper(db.cnx, db.log)
74 else:
75 db = self.dbmgr.get_connection()
76 self.dbmgr._transaction_local.wdb = self.db = db
77 return db
78
79 - def __exit__(self, et, ev, tb):
80 if self.db:
81 self.dbmgr._transaction_local.wdb = None
82 if et is None:
83 self.db.commit()
84 else:
85 self.db.rollback()
86 if not self.dbmgr._transaction_local.rdb:
87 self.db.close()
88
89
90 -class QueryContextManager(DbContextManager):
91 """Database Context Manager for retrieving a read-only
92 `~trac.db.util.ConnectionWrapper`.
93 """
94
95 - def __enter__(self):
96 db = self.dbmgr._transaction_local.rdb
97 if not db:
98 db = self.dbmgr._transaction_local.wdb
99 if db:
100 db = ConnectionWrapper(db.cnx, db.log, readonly=True)
101 else:
102 db = self.dbmgr.get_connection(readonly=True)
103 self.dbmgr._transaction_local.rdb = self.db = db
104 return db
105
106 - def __exit__(self, et, ev, tb):
107 if self.db:
108 self.dbmgr._transaction_local.rdb = None
109 if not self.dbmgr._transaction_local.wdb:
110 self.db.close()
111
114 """Abstract base class for database connection classes."""
115
116 __metaclass__ = ABCMeta
117
118 @abstractmethod
119 - def cast(self, column, type):
120 """Returns a clause casting `column` as `type`."""
121 pass
122
123 @abstractmethod
125 """Returns a clause concatenating the sequence `args`."""
126 pass
127
128 @abstractmethod
130 """Drops the `column` from `table`."""
131 pass
132
133 @abstractmethod
135 """Drops the `table`."""
136 pass
137
138 @abstractmethod
140 """Returns the list of the column names in `table`."""
141 pass
142
143 @abstractmethod
145 """Returns the current value of the primary key sequence for `table`.
146 The `column` of the primary key may be specified, which defaults
147 to `id`."""
148 pass
149
150 @abstractmethod
152 """Returns a list of the sequence names."""
153 pass
154
155 @abstractmethod
157 """Returns a list of the table names."""
158 pass
159
160 @abstractmethod
162 """Returns whether the table exists."""
163 pass
164
165 @abstractmethod
167 """Returns a case-insensitive `LIKE` clause."""
168 pass
169
170 @abstractmethod
172 """Returns `text` escaped for use in a `LIKE` clause."""
173 pass
174
175 @abstractmethod
177 """Return a case sensitive prefix-matching operator."""
178 pass
179
180 @abstractmethod
182 """Return a value for case sensitive prefix-matching operator."""
183 pass
184
185 @abstractmethod
186 - def quote(self, identifier):
187 """Returns the quoted `identifier`."""
188 pass
189
190 @abstractmethod
192 """Deletes all data from the tables and resets autoincrement indexes.
193
194 :return: list of names of the tables that were reset.
195 """
196 pass
197
198 @abstractmethod
200 """Updates the current value of the primary key sequence for `table`.
201 The `column` of the primary key may be specified, which defaults
202 to `id`."""
203 pass
204
207 """Extension point interface for components that support the
208 connection to relational databases.
209 """
210
212 """Return the connection URL schemes supported by the
213 connector, and their relative priorities as an iterable of
214 `(scheme, priority)` tuples.
215
216 If `priority` is a negative number, this is indicative of an
217 error condition with the connector. An error message should be
218 attached to the `error` attribute of the connector.
219 """
220
222 """Create a new connection to the database."""
223
225 """Return an object (typically a module) containing all the
226 backend-specific exception types as attributes, named
227 according to the Python Database API
228 (http://www.python.org/dev/peps/pep-0249/).
229 """
230
231 - def init_db(path, schema=None, log=None, **kwargs):
232 """Initialize the database."""
233
235 """Destroy the database."""
236
237 - def db_exists(self, path, log=None, **kwargs):
238 """Return `True` if the database exists."""
239
241 """Return the DDL statements necessary to create the specified
242 table, including indices."""
243
245 """Backup the database to a location defined by
246 trac.backup_dir"""
247
249 """Yield a sequence of `(name, version)` tuples describing the
250 name and version information of external packages used by the
251 connector.
252 """
253
256 """Component used to manage the `IDatabaseConnector` implementations."""
257
258 implements(IEnvironmentSetupParticipant, ISystemInfoProvider)
259
260 connectors = ExtensionPoint(IDatabaseConnector)
261
262 connection_uri = Option('trac', 'database', 'sqlite:db/trac.db',
263 """Database connection
264 [wiki:TracEnvironment#DatabaseConnectionStrings string] for this
265 project""")
266
267 backup_dir = Option('trac', 'backup_dir', 'db',
268 """Database backup location""")
269
270 timeout = IntOption('trac', 'timeout', '20',
271 """Timeout value for database connection, in seconds.
272 Use '0' to specify ''no timeout''.""")
273
274 debug_sql = BoolOption('trac', 'debug_sql', False,
275 """Show the SQL queries in the Trac log, at DEBUG level.
276 """)
277
279 self._cnx_pool = None
280 self._transaction_local = ThreadLocal(wdb=None, rdb=None)
281
289
292
298
302
304 """Create the specified tables.
305
306 :param schema: an iterable of table objects.
307
308 :since: version 1.0.2
309 """
310 connector = self.get_connector()[0]
311 with self.env.db_transaction as db:
312 for table in schema:
313 for sql in connector.to_sql(table):
314 db(sql)
315
317 """Drops the specified columns from table.
318
319 :since: version 1.2
320 """
321 table_name = table.name if isinstance(table, Table) else table
322 with self.env.db_transaction as db:
323 if not db.has_table(table_name):
324 raise self.env.db_exc.OperationalError('Table %s not found' %
325 db.quote(table_name))
326 for col in columns:
327 db.drop_column(table_name, col)
328
330 """Drop the specified tables.
331
332 :param schema: an iterable of `Table` objects or table names.
333
334 :since: version 1.0.2
335 """
336 with self.env.db_transaction as db:
337 for table in schema:
338 table_name = table.name if isinstance(table, Table) else table
339 db.drop_table(table_name)
340
342 """Insert data into existing tables.
343
344 :param data_or_callable: Nested tuples of table names, column names
345 and row data::
346
347 (table1,
348 (column1, column2),
349 ((row1col1, row1col2),
350 (row2col1, row2col2)),
351 table2, ...)
352
353 or a callable that takes a single parameter
354 `db` and returns the aforementioned nested
355 tuple.
356 :since: version 1.1.3
357 """
358 with self.env.db_transaction as db:
359 data = data_or_callable(db) if callable(data_or_callable) \
360 else data_or_callable
361 for table, cols, vals in data:
362 db.executemany("INSERT INTO %s (%s) VALUES (%s)"
363 % (db.quote(table), ','.join(cols),
364 ','.join(['%s'] * len(cols))), vals)
365
367 """Deletes all data from the tables and resets autoincrement indexes.
368
369 :return: list of names of the tables that were reset.
370
371 :since: version 1.1.3
372 """
373 with self.env.db_transaction as db:
374 return db.reset_tables()
375
377 """Upgrade table schema to `new_schema`, preserving data in
378 columns that exist in the current schema and `new_schema`.
379
380 :param new_schema: tuple or list of `Table` objects
381
382 :since: version 1.2
383 """
384 with self.env.db_transaction as db:
385 cursor = db.cursor()
386 for new_table in new_schema:
387 temp_table_name = new_table.name + '_old'
388 has_table = self.has_table(new_table)
389 if has_table:
390 old_column_names = set(self.get_column_names(new_table))
391 new_column_names = {col.name for col in new_table.columns}
392 column_names = old_column_names & new_column_names
393 if column_names:
394 cols_to_copy = ','.join(db.quote(name)
395 for name in column_names)
396 cursor.execute("""
397 CREATE TEMPORARY TABLE %s AS SELECT * FROM %s
398 """ % (db.quote(temp_table_name),
399 db.quote(new_table.name)))
400 self.drop_tables((new_table,))
401 self.create_tables((new_table,))
402 if has_table and column_names:
403 cursor.execute("""
404 INSERT INTO %s (%s) SELECT %s FROM %s
405 """ % (db.quote(new_table.name), cols_to_copy,
406 cols_to_copy, db.quote(temp_table_name)))
407 for col in new_table.columns:
408 if col.auto_increment:
409 db.update_sequence(cursor, new_table.name,
410 col.name)
411 self.drop_tables((temp_table_name,))
412
426
428 """Returns the database version from the SYSTEM table as an int,
429 or `False` if the entry is not found.
430
431 :param name: The name of the entry that contains the database version
432 in the SYSTEM table. Defaults to `database_version`,
433 which contains the database version for Trac.
434 """
435 with self.env.db_query as db:
436 for value, in db("""
437 SELECT value FROM {0} WHERE name=%s
438 """.format(db.quote('system')), (name,)):
439 return int(value)
440 else:
441 return False
442
445
453
461
463 """Returns a list of the column names for `table`.
464
465 :param table: a `Table` object or table name.
466
467 :since: 1.2
468 """
469 table_name = table.name if isinstance(table, Table) else table
470 with self.env.db_query as db:
471 if not db.has_table(table_name):
472 raise self.env.db_exc.OperationalError('Table %s not found' %
473 db.quote(table_name))
474 return db.get_column_names(table_name)
475
477 """Returns whether the table exists."""
478 table_name = table.name if isinstance(table, Table) else table
479 with self.env.db_query as db:
480 return db.has_table(table_name)
481
483 """Sets the database version in the SYSTEM table.
484
485 :param version: an integer database version.
486 :param name: The name of the entry that contains the database version
487 in the SYSTEM table. Defaults to `database_version`,
488 which contains the database version for Trac.
489 """
490 current_database_version = self.get_database_version(name)
491 if current_database_version is False:
492 with self.env.db_transaction as db:
493 db("""
494 INSERT INTO {0} (name, value) VALUES (%s, %s)
495 """.format(db.quote('system')), (name, version))
496 elif version != self.get_database_version(name):
497 with self.env.db_transaction as db:
498 db("""
499 UPDATE {0} SET value=%s WHERE name=%s
500 """.format(db.quote('system')), (version, name))
501 self.log.info("Upgraded %s from %d to %d",
502 name, current_database_version, version)
503
505 """Checks the database version to determine if an upgrade is needed.
506
507 :param version: the expected integer database version.
508 :param name: the name of the entry in the SYSTEM table that contains
509 the database version. Defaults to `database_version`,
510 which contains the database version for Trac.
511
512 :return: `True` if the stored version is less than the expected
513 version, `False` if it is equal to the expected version.
514 :raises TracError: if the stored version is greater than the expected
515 version.
516 """
517 dbver = self.get_database_version(name)
518 if dbver == version:
519 return False
520 elif dbver > version:
521 raise TracError(_("Need to downgrade %(name)s.", name=name))
522 self.log.info("Need to upgrade %s from %d to %d",
523 name, dbver, version)
524 return True
525
526 - def upgrade(self, version, name='database_version', pkg='trac.upgrades'):
527 """Invokes `do_upgrade(env, version, cursor)` in module
528 `"%s/db%i.py" % (pkg, version)`, for each required version upgrade.
529
530 :param version: the expected integer database version.
531 :param name: the name of the entry in the SYSTEM table that contains
532 the database version. Defaults to `database_version`,
533 which contains the database version for Trac.
534 :param pkg: the package containing the upgrade modules.
535
536 :raises TracError: if the package or module doesn't exist.
537 """
538 dbver = self.get_database_version(name)
539 for i in xrange(dbver + 1, version + 1):
540 module = '%s.db%i' % (pkg, i)
541 try:
542 upgrader = importlib.import_module(module)
543 except ImportError:
544 raise TracError(_("No upgrade module %(module)s.py",
545 module=module))
546 with self.env.db_transaction as db:
547 cursor = db.cursor()
548 upgrader.do_upgrade(self.env, i, cursor)
549 self.set_database_version(i, name)
550
552 if self._cnx_pool:
553 self._cnx_pool.shutdown(tid)
554 if not tid:
555 self._cnx_pool = None
556
579
609
610
611
614
617
620
621
622
627
630 """Retrieve column names from a cursor, if possible."""
631 return [unicode(d[0], 'utf-8') if isinstance(d[0], str) else d[0]
632 for d in cursor.description] if cursor.description else []
633
636 """Parse the database connection string.
637
638 The database connection string for an environment is specified through
639 the `database` option in the `[trac]` section of trac.ini.
640
641 :return: a tuple containing the scheme and a dictionary of attributes:
642 `user`, `password`, `host`, `port`, `path`, `params`.
643 :since: 1.1.3
644 """
645 if not db_str:
646 section = tag.a("[trac]",
647 title=_("TracIni documentation"),
648 class_='trac-target-new',
649 href='https://trac.edgewall.org/wiki/TracIni'
650 '#trac-section')
651 raise ConfigurationError(
652 tag_("Database connection string is empty. Set the %(option)s "
653 "configuration option in the %(section)s section of "
654 "trac.ini. Please refer to the %(doc)s for help.",
655 option=tag.code("database"), section=section,
656 doc=_doc_db_str()))
657
658 try:
659 scheme, rest = db_str.split(':', 1)
660 except ValueError:
661 raise _invalid_db_str(db_str)
662
663 if not rest.startswith('/'):
664 if scheme == 'sqlite' and rest:
665
666 host = None
667 path = rest
668 else:
669 raise _invalid_db_str(db_str)
670 else:
671 if not rest.startswith('//'):
672 host = None
673 rest = rest[1:]
674 elif rest.startswith('///'):
675 host = None
676 rest = rest[3:]
677 else:
678 rest = rest[2:]
679 if '/' in rest:
680 host, rest = rest.split('/', 1)
681 else:
682 host = rest
683 rest = ''
684 path = None
685
686 if host and '@' in host:
687 user, host = host.split('@', 1)
688 if ':' in user:
689 user, password = user.split(':', 1)
690 else:
691 password = None
692 if user:
693 user = urllib.unquote(user)
694 if password:
695 password = unicode_passwd(urllib.unquote(password))
696 else:
697 user = password = None
698
699 if host and ':' in host:
700 host, port = host.split(':', 1)
701 try:
702 port = int(port)
703 except ValueError:
704 raise _invalid_db_str(db_str)
705 else:
706 port = None
707
708 if not path:
709 path = '/' + rest
710 if os.name == 'nt':
711
712 if len(rest) > 1 and rest[1] == '|':
713 path = "%s:%s" % (rest[0], rest[2:])
714
715 params = {}
716 if '?' in path:
717 path, qs = path.split('?', 1)
718 qs = qs.split('&')
719 for param in qs:
720 try:
721 name, value = param.split('=', 1)
722 except ValueError:
723 raise _invalid_db_str(db_str)
724 value = urllib.unquote(value)
725 params[name] = value
726
727 args = zip(('user', 'password', 'host', 'port', 'path', 'params'),
728 (user, password, host, port, path, params))
729 return scheme, {key: value for key, value in args if value}
730
733 return ConfigurationError(
734 tag_("Invalid format %(db_str)s for the database connection string. "
735 "Please refer to the %(doc)s for help.",
736 db_str=tag.code(db_str), doc=_doc_db_str()))
737
740 return tag.a(_("documentation"),
741 title=_("Database Connection Strings documentation"),
742 class_='trac-target-new',
743 href='https://trac.edgewall.org/wiki/'
744 'TracIni#DatabaseConnectionStrings')
745