3 Simple convenience wrapper for SQLite.
5 ------------------------------------------------------------------------------
6 This file is part of dblite - simple query interface for SQL databases.
7 Released under the MIT License.
12 ------------------------------------------------------------------------------
23 from six
import binary_type, integer_types, string_types, text_type
25 from ..
import api, util
27 logger = logging.getLogger(__name__)
32 "ACTION",
"ADD",
"AFTER",
"ALL",
"ALTER",
"ALWAYS",
"ANALYZE",
"AND",
"AS",
"ASC",
"ATTACH",
33 "AUTOINCREMENT",
"BEFORE",
"BEGIN",
"BETWEEN",
"BY",
"CASE",
"CAST",
"CHECK",
"COLLATE",
34 "COMMIT",
"CONSTRAINT",
"CREATE",
"CURRENT_DATE",
"CURRENT_TIME",
"CURRENT_TIMESTAMP",
35 "DEFAULT",
"DEFERRABLE",
"DEFERRED",
"DELETE",
"DESC",
"DETACH",
"DISTINCT",
"DO",
"DROP",
36 "EACH",
"ELSE",
"END",
"ESCAPE",
"EXCEPT",
"EXISTS",
"EXPLAIN",
"FOR",
"FOREIGN",
"FROM",
37 "GENERATED",
"GROUP",
"HAVING",
"IF",
"IMMEDIATE",
"IN",
"INDEX",
"INITIALLY",
"INSERT",
38 "INSTEAD",
"INTERSECT",
"INTO",
"IS",
"ISNULL",
"JOIN",
"KEY",
"LIKE",
"LIMIT",
"MATCH",
39 "NO",
"NOT",
"NOTHING",
"NOTNULL",
"NULL",
"OF",
"ON",
"OR",
"ORDER",
"OVER",
"PRAGMA",
40 "PRECEDING",
"PRIMARY",
"RAISE",
"RECURSIVE",
"REFERENCES",
"REGEXP",
"REINDEX",
"RELEASE",
41 "RENAME",
"REPLACE",
"RESTRICT",
"ROLLBACK",
"SAVEPOINT",
"SELECT",
"SET",
"TABLE",
42 "TEMPORARY",
"THEN",
"TIES",
"TO",
"TRANSACTION",
"TRIGGER",
"UNBOUNDED",
"UNION",
43 "UNIQUE",
"UPDATE",
"USING",
"VACUUM",
"VALUES",
"VIEW",
"WHEN",
"WHERE",
"WITHOUT"
50 OPS = [
"||",
"*",
"/",
"%",
"+",
"-",
"<<",
">>",
"&",
"|",
"<",
"<=",
">",
51 ">=",
"=",
"==",
"!=",
"<>",
"IS",
"IS NOT",
"IN",
"NOT IN",
"LIKE",
52 "GLOB",
"MATCH",
"REGEXP",
"AND",
"OR"]
58 def insert(self, table, values=(), **kwargs):
60 Convenience wrapper for database INSERT, returns inserted row ID.
61 Keyword arguments are added to VALUES.
63 sql, args = self.
makeSQL(
"INSERT", table, values=values, kwargs=kwargs)
64 return self.
execute(sql, args).lastrowid
67 def insertmany(self, table, rows=(), **kwargs):
69 Convenience wrapper for database multiple INSERTs, returns list of inserted row IDs.
70 Keyword arguments are added to VALUES of every single row, overriding individual row values.
75 commons = {util.nameify(k, parent=table): v
for k, v
in kwargs.items()}
77 cols, values, valueidx = [], [], {}
78 for k, v
in util.keyvalues(row):
79 k = util.nameify(k, parent=table)
80 cols.append(k), values.append((k, v)), valueidx.update({k: len(valueidx)})
81 for k, v
in commons.items():
82 if k
in valueidx: values[valueidx[k]] = (k, v)
83 else: cols.append(k), values.append((k, v))
84 cols.sort(key=
lambda x: x.lower()), values.sort(key=
lambda x: x[0].lower())
85 cachekey = tuple(cols)
87 sql = sqlcache.get(cachekey)
89 sql, args = self.
makeSQL(
"INSERT", table, values=values)
90 sqlcache[cachekey] = sql
92 keys = [
"%sI%s" % (re.sub(
r"\W+",
"_", k), i)
for i, (k, _)
in enumerate(values)]
93 args = {n: self.
_cast(k, v)
for n, (k, v)
in zip(keys, values)}
94 result.append(self.
execute(sql, args).lastrowid)
98 def makeSQL(self, action, table, cols="*", where=(), group=(), order=(), limit=(), values=(),
100 """Returns (SQL statement string, parameter dict)."""
102 def parse_members(i, col, op, val):
103 """Returns (col, op, val, argkey)."""
104 col = util.nameify(col, quote, table)
105 key =
"%sW%s" % (re.sub(
r"\W+",
"_", col), i)
106 if "EXPR" == col.upper():
108 col, op, val, key = val[0],
"EXPR", val[1],
"EXPRW%s" % i
109 elif col.count(
"?") == argcount(val):
111 op, val, key =
"EXPR", listify(val),
"EXPRW%s" % i
112 elif isinstance(val, (list, tuple))
and len(val) == 2 \
113 and isinstance(val[0], string_types):
114 tmp = val[0].strip().upper()
117 op, val = tmp, val[1]
118 elif val[0].count(
"?") == argcount(val[1]):
120 col, val, op =
"%s = %s" % (col, val[0]), listify(val[1]),
"EXPR"
121 return col, op, val, key
122 def argcount(x) :
return len(x)
if isinstance(x, (list, set, tuple))
else 1
123 def listify(x) :
return x
if isinstance(x, (list, tuple))
else \
124 list(x)
if isinstance(x, set)
else [x]
125 def keylistify(x):
return x
if isinstance(x, (list, tuple))
else \
126 list(x)
if isinstance(x, (dict, set))
else [x]
128 action = action.upper()
129 where, group, order, limit, values = (()
if x
is None else x
130 for x
in (where, group, order, limit, values))
131 tablesql = util.nameify(table, quote)
132 cols =
", ".join(util.nameify(x, quote, table)
for x
in keylistify(cols))
or "*"
133 group =
", ".join(util.nameify(x, quote, table)
for x
in keylistify(group))
134 where = util.keyvalues(where, quote)
135 order = list(order.items())
if isinstance(order, dict)
else listify(order)
136 order = [order]
if isinstance(order, (list, tuple)) \
137 and len(order) == 2
and isinstance(order[1], bool)
else order
138 limit = [limit]
if isinstance(limit, string_types + integer_types)
else limit
139 values = util.keyvalues(values, quote)
140 sql =
"SELECT %s FROM %s" % (cols, tablesql)
if "SELECT" == action
else ""
141 sql =
"DELETE FROM %s" % (tablesql)
if "DELETE" == action
else sql
142 sql =
"INSERT INTO %s" % (tablesql)
if "INSERT" == action
else sql
143 sql =
"UPDATE %s" % (tablesql)
if "UPDATE" == action
else sql
145 if kwargs
and action
in (
"SELECT",
"DELETE",
"UPDATE"): where += list(kwargs.items())
146 if kwargs
and action
in (
"INSERT", ): values += list(kwargs.items())
148 if "INSERT" == action:
149 keys = [
"%sI%s" % (re.sub(
r"\W+",
"_", self.
_column(k, table=table)), i)
150 for i, (k, _)
in enumerate(values)]
151 args.update((n, self.
_cast(k, v))
for n, (k, v)
in zip(keys, values))
152 cols =
", ".join(self.
_column(k, sql=
True, table=table)
for k, _
in values)
153 vals =
", ".join(
":%s" % n
for n
in keys)
154 sql +=
" (%s) VALUES (%s)" % (cols, vals)
155 if "UPDATE" == action:
157 for i, (col, val)
in enumerate(values):
158 key =
"%sU%s" % (re.sub(
r"\W+",
"_", self.
_column(col, table=table)), i)
159 sql += (
", " if i
else "") + \
160 "%s = :%s" % (self.
_column(col, sql=
True, table=table), key)
161 args[key] = self.
_cast(col, val)
164 for i, clause
in enumerate(where):
165 if isinstance(clause, string_types):
169 col, op, val, key = clause[0],
"EXPR", [],
None
170 elif len(clause) == 2:
171 col, op, val, key = parse_members(i, clause[0],
"=", clause[1])
173 col, op, val, key = parse_members(i, *clause)
175 if op
in (
"IN",
"NOT IN"):
176 keys = [
"%s_%s" % (key, j)
for j
in range(len(val))]
177 args.update({k: self.
_cast(col, v)
for k, v
in zip(keys, val)})
178 sql += (
" AND " if i
else "") +
"%s %s (%s)" % (
179 col, op,
", ".join(
":" + x
for x
in keys))
181 for j
in range(col.count(
"?")):
182 col = col.replace(
"?",
":%s_%s" % (key, j), 1)
183 args[
"%s_%s" % (key, j)] = self.
_cast(
None, val[j])
184 sql += (
" AND " if i
else "") +
"(%s)" % col
186 op = {
"=":
"IS",
"!=":
"IS NOT",
"<>":
"IS NOT"}.get(op, op)
187 sql += (
" AND " if i
else "") +
"%s %s NULL" % (col, op)
189 args[key] = self.
_cast(col, val)
190 sql += (
" AND " if i
else "") +
"%s %s :%s" % (col, op, key)
192 sql +=
" GROUP BY " + group
195 for i, col
in enumerate(order):
196 name = util.nameify(col[0]
if isinstance(col, (list, tuple))
else col, quote, table)
197 sort = col[1]
if name != col
and isinstance(col, (list, tuple))
and len(col) > 1 \
199 if not isinstance(sort, string_types): sort =
"" if sort
else "DESC"
200 sql += (
", " if i
else "") + name + (
" " if sort
else "") + sort
202 limit = [
None if isinstance(v, integer_types)
and v < 0
else v
for v
in limit]
203 for i, (k, v)
in enumerate(zip((
"limit",
"offset"), limit)):
205 if i
or len(limit) < 2
or not limit[1]:
continue
207 sql +=
" %s :%s" % (k.upper(), k)
210 logger.log(logging.DEBUG // 2, sql)
215 def quote(cls, value, force=False):
217 Returns identifier in quotes and proper-escaped for queries,
218 if value needs quoting (has non-alphanumerics, starts with number, or is reserved).
220 @param value the value to quote, returned as-is if not string
221 @param force whether to quote value even if not required
223 return quote(value, force)
226 def _cast(self, col, val):
227 """Returns column value cast to correct type for use in sqlite."""
228 return tuple(val)
if isinstance(val, set)
else val
231 def _column(self, col, sql=False, table=None):
232 """Returns column name from string/property, quoted if object and `sql`."""
233 if inspect.isdatadescriptor(col): col = util.nameify(col, quote
if sql
else None, table)
234 return col
if isinstance(col, string_types)
else text_type(col)
240 Convenience wrapper around sqlite3.Connection.
242 Queries directly on the Database object use autocommit mode.
246 MUTEX = collections.defaultdict(threading.RLock)
252 def __init__(self, opts=":memory:
", **kwargs):
254 Creates a new Database instance for SQLite.
256 @param opts file path or `":memory:"`
257 @param kwargs supported arguments are passed to sqlite3.connect() in open(),
258 like `detect_types=sqlite3.PARSE_COLNAMES`
265 rowtype = dict
if sys.version_info > (3, )
else collections.OrderedDict
266 self.
_def_factory =
lambda cursor, row: rowtype(sqlite3.Row(cursor, row))
272 """Context manager entry, opens database if not already open, returns Database object."""
277 def __exit__(self, exc_type, exc_val, exc_trace):
278 """Context manager exit, closes database and any pending transactions if open."""
279 txs, self.
_txs[:] = self.
_txs[:], []
280 for tx
in txs: tx.close(commit=
None if exc_type
is None else False)
282 return exc_type
is None
285 def execute(self, sql, args=()):
287 Executes the SQL statement and returns sqlite3.Cursor.
289 @param sql SQL statement to execute, with SQLite-specific parameter bindings, if any
290 @param args dictionary for :name placeholders,
291 or a sequence for positional ? placeholders
298 Executes the SQL statement against all parameter sequences.
300 @param sql SQL statement to execute, with SQLite-specific parameter bindings
301 @param args iterable of query parameters, as dictionaries for :name placeholders
302 or sequences for positional ? placeholders
309 Executes the SQL as script of any number of statements.
311 @param sql script with one or more SQL statements
317 """Opens the database connection, if not already open."""
319 KWS = (
"timeout",
"detect_types",
"isolation_level",
"check_same_thread",
320 "factory",
"cached_statements",
"uri")
321 args = dict(detect_types=sqlite3.PARSE_DECLTYPES,
322 isolation_level=
None, check_same_thread=
False)
323 args.update({k: v
for k, v
in self.
_kwargs.items()
if k
in KWS})
324 if ":memory:" != self.
path and not os.path.exists(self.
path):
325 try: os.makedirs(os.path.dirname(self.
path))
326 except Exception:
pass
330 if row_factory
in (
False,
None): row_factory = self.
_def_factory
334 def close(self, commit=None):
336 Closes the database and any pending transactions, if open.
338 @param commit `True` for explicit commit on open transactions,
339 `False` for explicit rollback on open transactions,
340 `None` defaults to `commit` flag from transaction creations
342 txs, self.
_txs[:] = self.
_txs[:], []
343 for tx
in txs: tx.close(commit)
350 """Whether database is currently not open."""
356 """Database engine cursor object, or `None` if closed."""
362 """The custom row factory, if any, as `function(cursor, row tuple)`."""
369 Sets custom row factory, as `function(cursor, row tuple)`, or `None` to reset to default.
371 `cursor.description` is a sequence of 7-element tuples, as `(column name, None, None, ..)`.
373 self.
_row_factory =
False if row_factory
is None else row_factory
379 def transaction(self, commit=True, exclusive=True, **kwargs):
381 Returns a transaction context manager.
383 Context is breakable by raising Rollback.
385 @param commit whether transaction commits automatically at exiting with-block
386 @param exclusive whether entering a with-block is exclusive
387 over other Transaction instances on this Database
388 @param kwargs engine-specific arguments, like `detect_types=sqlite3.PARSE_COLNAMES`
390 tx =
Transaction(self, commit, exclusive, **kwargs)
395 def _notify(self, tx):
396 """Notifies database of transaction closing."""
397 if tx
in self.
_txs: self.
_txs.remove(tx)
404 Transaction context manager, breakable by raising Rollback.
406 Note that in SQLite, a single connection has one shared transaction state,
407 so it is highly recommended to use exclusive Transaction instances for any action queries,
408 as concurrent transactions can interfere with one another otherwise.
411 def __init__(self, db, commit=True, exclusive=True, **__):
413 Creates a new transaction.
415 @param db Database instance
416 @param commit whether transaction commits automatically at exiting with-block
417 @param exclusive whether entering a with-block is exclusive over other
418 Transaction instances on this Database
423 self.
_exclusive =
True if exclusive
is None else exclusive
428 """Context manager entry, opens cursor, returns Transaction object."""
429 if self.
_closed:
raise RuntimeError(
"Transaction already closed")
439 def __exit__(self, exc_type, exc_val, exc_trace):
440 """Context manager exit, closes cursor, commits or rolls back as specified on creation."""
450 self.
_db._notify(self)
453 def close(self, commit=None):
455 Closes the transaction, performing commit or rollback as specified.
457 @param commit `True` for final commit, `False` for rollback,
458 `None` for auto-commit, if any
461 self.
_db._notify(self)
467 self.
_db._notify(self)
469 def execute(self, sql, args=()):
471 Executes the SQL statement and returns sqlite3.Cursor.
473 @param sql SQL statement to execute, with SQLite-specific parameter bindings, if any
474 @param args dictionary for :name placeholders,
475 or a sequence for positional ? placeholders
477 if self.
_closed:
raise RuntimeError(
"Transaction already closed")
483 Executes the SQL statement against all parameter sequences.
485 @param sql SQL statement to execute, with SQLite-specific parameter bindings
486 @param args iterable of query parameters, as dictionaries for :name placeholders
487 or sequences for positional ? placeholders
489 if self.
_closed:
raise RuntimeError(
"Transaction already closed")
495 Executes the SQL as script of any number of statements, outside of transaction.
497 Any pending transaction will be committed first.
499 @param sql script with one or more SQL statements
501 if self.
_closed:
raise RuntimeError(
"Transaction already closed")
502 with Database.MUTEX[self.
_db]:
507 """Commits pending actions, if any."""
509 with Database.MUTEX[self.
_db]:
513 """Rolls back pending actions, if any."""
515 with Database.MUTEX[self.
_db]:
520 """Whether transaction is currently not open."""
525 """Database engine cursor object, or `None` if closed."""
533 """Returns transaction Database instance."""
536 def _make_cursor(self):
537 """Opens the transaction cursor."""
538 self.
_db.connection.isolation_level =
"DEFERRED"
541 self.
_db.connection.isolation_level = self._isolevel0
544 def _reset(self, commit=False):
545 """Commits or rolls back ongoing transaction, if any, closes cursor, if any."""
546 if getattr(self.
_db.connection,
"in_transaction",
True):
547 self.
_db.connection.commit()
if commit
else self.
_db.connection.rollback()
556 Returns true if input is recognizable as SQLite connection options.
558 @param opts expected as a path string or path-like object
560 if isinstance(opts, string_types):
561 return opts.startswith(
"file:")
or not re.match(
r"^\w+\:\/\/", opts)
562 elif sys.version_info >= (3, 4):
564 return isinstance(opts, pathlib.Path)
568 def quote(value, force=False):
570 Returns identifier in quotes and proper-escaped for queries,
571 if value needs quoting (has non-alphanumerics, starts with number, or is reserved).
573 @param value the value to quote, returned as-is if not string
574 @param force whether to quote value even if not required
576 if not isinstance(value, string_types):
578 RGX_INVALID =
r"(^[\W\d])|(?=\W)"
579 result = value.decode()
if isinstance(value, binary_type)
else value
580 if force
or result.upper()
in RESERVED_KEYWORDS
or re.search(RGX_INVALID, result, re.U):
581 result =
u'"%s"' % result.replace(
'"',
'""')
586 """Registers function to auto-adapt given Python types to SQLite types in query parameters."""
587 for t
in typeclasses: sqlite3.register_adapter(t, transformer)
591 """Registers function to auto-convert given SQLite types to Python types in query results."""
592 for n
in typenames: sqlite3.register_converter(n, transformer)
596 """Registers custom row factory, as or `None` to reset to default."""
597 Database.ROW_FACTORY = row_factory
601 "RESERVED_KEYWORDS",
"Database",
"Transaction",
602 "autodetect",
"quote",
"register_adapter",
"register_converter",
"register_row_factory",