Package trac ::
Package db ::
Module sqlite_backend
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C)2005-2010 Edgewall Software
4 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
5 # All rights reserved.
6 #
7 # This software is licensed as described in the file COPYING, which
8 # you should have received as part of this distribution. The terms
9 # are also available at http://trac.edgewall.org/wiki/TracLicense.
10 #
11 # This software consists of voluntary contributions made by many
12 # individuals. For the exact contribution history, see the revision
13 # history and logs, available at http://trac.edgewall.org/log/.
14 #
15 # Author: Christopher Lenz <cmlenz@gmx.de>
16
17 import os
18 import re
19 import weakref
20
21 from trac .config import ListOption
22 from trac .core import *
23 from trac .db .api import IDatabaseConnector
24 from trac .db .util import ConnectionWrapper , IterableCursor
25 from trac .util import get_pkginfo , getuser
26 from trac .util .translation import _
27
28 _like_escape_re = re.compile(r'([/_%])')
29
30 _glob_escape_re = re.compile(r'[*?\[]')
31
32 try:
33 import pysqlite2.dbapi2 as sqlite
34 have_pysqlite = 2
35 except ImportError:
36 try:
37 import sqlite3 as sqlite
38 have_pysqlite = 2
39 except ImportError:
40 have_pysqlite = 0
41
42 if have_pysqlite == 2:
43 # Force values to integers because PySQLite 2.2.0 had (2, 2, '0')
44 sqlite_version = tuple([int(x ) for x in sqlite.sqlite_version_info])
45 sqlite_version_string = sqlite.sqlite_version
46
65
66 # EagerCursor taken from the example in pysqlite's repository:
67 #
68 # http://code.google.com/p/pysqlite/source/browse/misc/eager.py
69 #
70 # Only change is to subclass it from PyFormatCursor instead of
71 # sqlite.Cursor.
72
78
84
86 try:
87 row = self.rows[self.pos]
88 self.pos += 1
89 return row
90 except IndexError:
91 return None
92
94 if num is None:
95 num = self.arraysize
96
97 result = self.rows[self.pos:self.pos+num]
98 self.pos += num
99 return result
100
102 result = self.rows[self.pos:]
103 self.pos = len(self.rows)
104 return result
105
106
107 # Mapping from "abstract" SQL types to DB-specific types
108 _type_map = {
109 'int': 'integer',
110 'int64': 'integer',
111 }
112
113
115 sql = ["CREATE TABLE %s (" % table.name ]
116 coldefs = []
117 for column in table.columns:
118 ctype = column.type .lower()
119 ctype = _type_map .get (ctype, ctype)
120 if column.auto_increment:
121 ctype = "integer PRIMARY KEY"
122 elif len(table.key) == 1 and column.name in table.key:
123 ctype += " PRIMARY KEY"
124 coldefs.append(" %s %s" % (column.name , ctype))
125 if len(table.key) > 1:
126 coldefs.append(" UNIQUE (%s)" % ','.join (table.key))
127 sql .append(',\n'.join (coldefs) + '\n);')
128 yield '\n'.join (sql )
129 for index in table.indices:
130 unique = index.unique and 'UNIQUE' or ''
131 yield "CREATE %s INDEX %s_%s_idx ON %s (%s);" % (unique, table.name ,
132 '_'.join (index.columns), table.name , ','.join (index.columns))
133
134
136 """Database connector for SQLite.
137
138 Database URLs should be of the form:
139 {{{
140 sqlite:path/to/trac.db
141 }}}
142 """
143 implements (IDatabaseConnector )
144
145 extensions = ListOption ('sqlite', 'extensions',
146 doc="""Paths to sqlite extensions, relative to Trac environment's
147 directory or absolute. (''since 0.12'')""")
148
150 self._version = None
151 self.error = None
152 self._extensions = None
153
155 if not have_pysqlite :
156 self.error = _ ("Cannot load Python bindings for SQLite")
157 elif sqlite_version >= (3, 3, 3) and sqlite.version_info[0] == 2 and \
158 sqlite.version_info < (2, 0, 7):
159 self.error = _ ("Need at least PySqlite %(version)s or higher",
160 version ='2.0.7')
161 elif (2, 5, 2) <= sqlite.version_info < (2, 5, 5):
162 self.error = _ ("PySqlite 2.5.2 - 2.5.4 break Trac, please use "
163 "2.5.5 or higher")
164 yield ('sqlite', self.error and -1 or 1)
165
167 if not self._version:
168 self._version = get_pkginfo (sqlite).get (
169 'version', '%d.%d.%s' % sqlite.version_info)
170 self.env .systeminfo.extend([('SQLite', sqlite_version_string ),
171 ('pysqlite', self._version)])
172 self.required = True
173 # construct list of sqlite extension libraries
174 if self._extensions is None:
175 self._extensions = []
176 for extpath in self.extensions :
177 if not os.path .isabs(extpath):
178 extpath = os.path .join (self.env .path , extpath)
179 self._extensions.append(extpath)
180 params ['extensions'] = self._extensions
181 return SQLiteConnection (path , log , params )
182
183 - def init_db (self, path, log=None, params={}):
201
203 return _to_sql(table)
204
206 """Yield SQL statements altering the type of one or more columns of
207 a table.
208
209 Type changes are specified as a `columns` dict mapping column names
210 to `(from, to)` SQL type tuples.
211 """
212 for name , (from_, to) in sorted(columns.iteritems()):
213 if _type_map .get (to, to) != _type_map .get (from_, from_):
214 raise NotImplementedError('Conversion from %s to %s is not '
215 'implemented' % (from_, to))
216 return ()
217
218 - def backup (self, dest_file):
219 """Simple SQLite-specific backup of the database.
220
221 @param dest_file: Destination file basename
222 """
223 import shutil
224 db_str = self.config .get ('trac', 'database')
225 try:
226 db_str = db_str[:db_str.index('?')]
227 except ValueError:
228 pass
229 db_name = os.path .join (self.env .path , db_str[7:])
230 shutil.copy(db_name, dest_file)
231 if not os.path .exists (dest_file):
232 raise TracError (_ ("No destination file created"))
233 return dest_file
234
235
237 """Connection wrapper for SQLite."""
238
239 __slots__ = ['_active_cursors', '_eager']
240
241 poolable = have_pysqlite and sqlite_version >= (3, 3, 8) \
242 and sqlite.version_info >= (2, 5, 0)
243
244 - def __init__ (self, path, log=None, params={}):
245 if have_pysqlite == 0:
246 raise TracError (_ ("Cannot load Python bindings for SQLite"))
247 self.cnx = None
248 if path != ':memory:':
249 if not os.access(path , os.F_OK):
250 raise TracError (_ ('Database "%(path)s" not found.', path =path ))
251
252 dbdir = os.path .dirname(path )
253 if not os.access(path , os.R_OK + os.W_OK) or \
254 not os.access(dbdir, os.R_OK + os.W_OK):
255 raise TracError (
256 _ ('The user %(user)s requires read _and_ write '
257 'permissions to the database file %(path)s '
258 'and the directory it is located in.',
259 user=getuser (), path =path ))
260
261 self._active_cursors = weakref.WeakKeyDictionary()
262 timeout = int(params .get ('timeout', 10.0))
263 self._eager = params .get ('cursor', 'eager') == 'eager'
264 # eager is default, can be turned off by specifying ?cursor=
265 if isinstance(path , unicode): # needed with 2.4.0
266 path = path .encode('utf-8')
267 cnx = sqlite.connect(path , detect_types=sqlite.PARSE_DECLTYPES,
268 check_same_thread=sqlite_version < (3, 3, 1),
269 timeout =timeout )
270 # load extensions
271 extensions = params .get ('extensions', [])
272 if len(extensions ) > 0:
273 cnx .enable_load_extension(True)
274 for ext in extensions :
275 cnx .load_extension(ext)
276 cnx .enable_load_extension(False)
277
278 ConnectionWrapper .__init__ (self, cnx , log )
279
285
290
291 - def cast (self, column, type):
292 if sqlite_version >= (3, 2, 3):
293 return 'CAST(%s AS %s)' % (column, _type_map .get (type , type ))
294 elif type == 'int':
295 # hack to force older SQLite versions to convert column to an int
296 return '1*' + column
297 else:
298 return column
299
302
304 """Return a case-insensitive LIKE clause."""
305 if sqlite_version >= (3, 1, 0):
306 return "LIKE %s ESCAPE '/'"
307 else:
308 return 'LIKE %s'
309
315
317 """Return a case sensitive prefix-matching operator."""
318 return 'GLOB %s'
319
321 """Return a value for case sensitive prefix-matching operator."""
322 return _glob_escape_re .sub(lambda m: '[%s]' % m.group (0), prefix) + '*'
323
324 - def quote (self, identifier):
325 """Return the quoted identifier."""
326 return "`%s`" % identifier.replace ('`', '``')
327
330
332 # SQLite handles sequence updates automagically
333 # http://www.sqlite.org/autoinc.html
334 pass
335
339