SQLAlchemy 2.0.36__cp313-cp313-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- SQLAlchemy-2.0.36.dist-info/LICENSE +19 -0
- SQLAlchemy-2.0.36.dist-info/METADATA +243 -0
- SQLAlchemy-2.0.36.dist-info/RECORD +273 -0
- SQLAlchemy-2.0.36.dist-info/WHEEL +5 -0
- SQLAlchemy-2.0.36.dist-info/top_level.txt +1 -0
- sqlalchemy/__init__.py +294 -0
- sqlalchemy/connectors/__init__.py +18 -0
- sqlalchemy/connectors/aioodbc.py +174 -0
- sqlalchemy/connectors/asyncio.py +213 -0
- sqlalchemy/connectors/pyodbc.py +249 -0
- sqlalchemy/cyextension/__init__.py +6 -0
- sqlalchemy/cyextension/collections.cp313-win32.pyd +0 -0
- sqlalchemy/cyextension/collections.pyx +409 -0
- sqlalchemy/cyextension/immutabledict.cp313-win32.pyd +0 -0
- sqlalchemy/cyextension/immutabledict.pxd +8 -0
- sqlalchemy/cyextension/immutabledict.pyx +133 -0
- sqlalchemy/cyextension/processors.cp313-win32.pyd +0 -0
- sqlalchemy/cyextension/processors.pyx +68 -0
- sqlalchemy/cyextension/resultproxy.cp313-win32.pyd +0 -0
- sqlalchemy/cyextension/resultproxy.pyx +102 -0
- sqlalchemy/cyextension/util.cp313-win32.pyd +0 -0
- sqlalchemy/cyextension/util.pyx +91 -0
- sqlalchemy/dialects/__init__.py +61 -0
- sqlalchemy/dialects/_typing.py +25 -0
- sqlalchemy/dialects/mssql/__init__.py +88 -0
- sqlalchemy/dialects/mssql/aioodbc.py +64 -0
- sqlalchemy/dialects/mssql/base.py +4010 -0
- sqlalchemy/dialects/mssql/information_schema.py +254 -0
- sqlalchemy/dialects/mssql/json.py +133 -0
- sqlalchemy/dialects/mssql/provision.py +162 -0
- sqlalchemy/dialects/mssql/pymssql.py +126 -0
- sqlalchemy/dialects/mssql/pyodbc.py +745 -0
- sqlalchemy/dialects/mysql/__init__.py +101 -0
- sqlalchemy/dialects/mysql/aiomysql.py +333 -0
- sqlalchemy/dialects/mysql/asyncmy.py +337 -0
- sqlalchemy/dialects/mysql/base.py +3494 -0
- sqlalchemy/dialects/mysql/cymysql.py +84 -0
- sqlalchemy/dialects/mysql/dml.py +219 -0
- sqlalchemy/dialects/mysql/enumerated.py +244 -0
- sqlalchemy/dialects/mysql/expression.py +141 -0
- sqlalchemy/dialects/mysql/json.py +81 -0
- sqlalchemy/dialects/mysql/mariadb.py +32 -0
- sqlalchemy/dialects/mysql/mariadbconnector.py +277 -0
- sqlalchemy/dialects/mysql/mysqlconnector.py +180 -0
- sqlalchemy/dialects/mysql/mysqldb.py +303 -0
- sqlalchemy/dialects/mysql/provision.py +110 -0
- sqlalchemy/dialects/mysql/pymysql.py +137 -0
- sqlalchemy/dialects/mysql/pyodbc.py +138 -0
- sqlalchemy/dialects/mysql/reflection.py +677 -0
- sqlalchemy/dialects/mysql/reserved_words.py +571 -0
- sqlalchemy/dialects/mysql/types.py +774 -0
- sqlalchemy/dialects/oracle/__init__.py +67 -0
- sqlalchemy/dialects/oracle/base.py +3271 -0
- sqlalchemy/dialects/oracle/cx_oracle.py +1483 -0
- sqlalchemy/dialects/oracle/dictionary.py +507 -0
- sqlalchemy/dialects/oracle/oracledb.py +431 -0
- sqlalchemy/dialects/oracle/provision.py +220 -0
- sqlalchemy/dialects/oracle/types.py +287 -0
- sqlalchemy/dialects/postgresql/__init__.py +167 -0
- sqlalchemy/dialects/postgresql/_psycopg_common.py +187 -0
- sqlalchemy/dialects/postgresql/array.py +425 -0
- sqlalchemy/dialects/postgresql/asyncpg.py +1274 -0
- sqlalchemy/dialects/postgresql/base.py +5008 -0
- sqlalchemy/dialects/postgresql/dml.py +310 -0
- sqlalchemy/dialects/postgresql/ext.py +496 -0
- sqlalchemy/dialects/postgresql/hstore.py +397 -0
- sqlalchemy/dialects/postgresql/json.py +333 -0
- sqlalchemy/dialects/postgresql/named_types.py +509 -0
- sqlalchemy/dialects/postgresql/operators.py +129 -0
- sqlalchemy/dialects/postgresql/pg8000.py +662 -0
- sqlalchemy/dialects/postgresql/pg_catalog.py +300 -0
- sqlalchemy/dialects/postgresql/provision.py +175 -0
- sqlalchemy/dialects/postgresql/psycopg.py +772 -0
- sqlalchemy/dialects/postgresql/psycopg2.py +886 -0
- sqlalchemy/dialects/postgresql/psycopg2cffi.py +61 -0
- sqlalchemy/dialects/postgresql/ranges.py +1029 -0
- sqlalchemy/dialects/postgresql/types.py +303 -0
- sqlalchemy/dialects/sqlite/__init__.py +57 -0
- sqlalchemy/dialects/sqlite/aiosqlite.py +396 -0
- sqlalchemy/dialects/sqlite/base.py +2805 -0
- sqlalchemy/dialects/sqlite/dml.py +240 -0
- sqlalchemy/dialects/sqlite/json.py +92 -0
- sqlalchemy/dialects/sqlite/provision.py +198 -0
- sqlalchemy/dialects/sqlite/pysqlcipher.py +155 -0
- sqlalchemy/dialects/sqlite/pysqlite.py +756 -0
- sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
- sqlalchemy/engine/__init__.py +62 -0
- sqlalchemy/engine/_py_processors.py +136 -0
- sqlalchemy/engine/_py_row.py +128 -0
- sqlalchemy/engine/_py_util.py +74 -0
- sqlalchemy/engine/base.py +3375 -0
- sqlalchemy/engine/characteristics.py +155 -0
- sqlalchemy/engine/create.py +875 -0
- sqlalchemy/engine/cursor.py +2181 -0
- sqlalchemy/engine/default.py +2365 -0
- sqlalchemy/engine/events.py +951 -0
- sqlalchemy/engine/interfaces.py +3403 -0
- sqlalchemy/engine/mock.py +131 -0
- sqlalchemy/engine/processors.py +61 -0
- sqlalchemy/engine/reflection.py +2098 -0
- sqlalchemy/engine/result.py +2382 -0
- sqlalchemy/engine/row.py +401 -0
- sqlalchemy/engine/strategies.py +19 -0
- sqlalchemy/engine/url.py +910 -0
- sqlalchemy/engine/util.py +167 -0
- sqlalchemy/event/__init__.py +25 -0
- sqlalchemy/event/api.py +225 -0
- sqlalchemy/event/attr.py +655 -0
- sqlalchemy/event/base.py +470 -0
- sqlalchemy/event/legacy.py +246 -0
- sqlalchemy/event/registry.py +386 -0
- sqlalchemy/events.py +17 -0
- sqlalchemy/exc.py +830 -0
- sqlalchemy/ext/__init__.py +11 -0
- sqlalchemy/ext/associationproxy.py +2013 -0
- sqlalchemy/ext/asyncio/__init__.py +25 -0
- sqlalchemy/ext/asyncio/base.py +279 -0
- sqlalchemy/ext/asyncio/engine.py +1466 -0
- sqlalchemy/ext/asyncio/exc.py +21 -0
- sqlalchemy/ext/asyncio/result.py +961 -0
- sqlalchemy/ext/asyncio/scoping.py +1614 -0
- sqlalchemy/ext/asyncio/session.py +1936 -0
- sqlalchemy/ext/automap.py +1691 -0
- sqlalchemy/ext/baked.py +574 -0
- sqlalchemy/ext/compiler.py +570 -0
- sqlalchemy/ext/declarative/__init__.py +65 -0
- sqlalchemy/ext/declarative/extensions.py +548 -0
- sqlalchemy/ext/horizontal_shard.py +481 -0
- sqlalchemy/ext/hybrid.py +1514 -0
- sqlalchemy/ext/indexable.py +341 -0
- sqlalchemy/ext/instrumentation.py +450 -0
- sqlalchemy/ext/mutable.py +1073 -0
- sqlalchemy/ext/mypy/__init__.py +6 -0
- sqlalchemy/ext/mypy/apply.py +320 -0
- sqlalchemy/ext/mypy/decl_class.py +515 -0
- sqlalchemy/ext/mypy/infer.py +590 -0
- sqlalchemy/ext/mypy/names.py +335 -0
- sqlalchemy/ext/mypy/plugin.py +303 -0
- sqlalchemy/ext/mypy/util.py +357 -0
- sqlalchemy/ext/orderinglist.py +416 -0
- sqlalchemy/ext/serializer.py +181 -0
- sqlalchemy/future/__init__.py +16 -0
- sqlalchemy/future/engine.py +15 -0
- sqlalchemy/inspection.py +174 -0
- sqlalchemy/log.py +288 -0
- sqlalchemy/orm/__init__.py +170 -0
- sqlalchemy/orm/_orm_constructors.py +2571 -0
- sqlalchemy/orm/_typing.py +179 -0
- sqlalchemy/orm/attributes.py +2835 -0
- sqlalchemy/orm/base.py +973 -0
- sqlalchemy/orm/bulk_persistence.py +2123 -0
- sqlalchemy/orm/clsregistry.py +571 -0
- sqlalchemy/orm/collections.py +1620 -0
- sqlalchemy/orm/context.py +3268 -0
- sqlalchemy/orm/decl_api.py +1883 -0
- sqlalchemy/orm/decl_base.py +2190 -0
- sqlalchemy/orm/dependency.py +1304 -0
- sqlalchemy/orm/descriptor_props.py +1076 -0
- sqlalchemy/orm/dynamic.py +300 -0
- sqlalchemy/orm/evaluator.py +379 -0
- sqlalchemy/orm/events.py +3261 -0
- sqlalchemy/orm/exc.py +228 -0
- sqlalchemy/orm/identity.py +302 -0
- sqlalchemy/orm/instrumentation.py +754 -0
- sqlalchemy/orm/interfaces.py +1474 -0
- sqlalchemy/orm/loading.py +1682 -0
- sqlalchemy/orm/mapped_collection.py +557 -0
- sqlalchemy/orm/mapper.py +4432 -0
- sqlalchemy/orm/path_registry.py +811 -0
- sqlalchemy/orm/persistence.py +1782 -0
- sqlalchemy/orm/properties.py +886 -0
- sqlalchemy/orm/query.py +3396 -0
- sqlalchemy/orm/relationships.py +3500 -0
- sqlalchemy/orm/scoping.py +2165 -0
- sqlalchemy/orm/session.py +5301 -0
- sqlalchemy/orm/state.py +1143 -0
- sqlalchemy/orm/state_changes.py +198 -0
- sqlalchemy/orm/strategies.py +3473 -0
- sqlalchemy/orm/strategy_options.py +2569 -0
- sqlalchemy/orm/sync.py +164 -0
- sqlalchemy/orm/unitofwork.py +796 -0
- sqlalchemy/orm/util.py +2424 -0
- sqlalchemy/orm/writeonly.py +678 -0
- sqlalchemy/pool/__init__.py +44 -0
- sqlalchemy/pool/base.py +1515 -0
- sqlalchemy/pool/events.py +370 -0
- sqlalchemy/pool/impl.py +581 -0
- sqlalchemy/py.typed +0 -0
- sqlalchemy/schema.py +70 -0
- sqlalchemy/sql/__init__.py +145 -0
- sqlalchemy/sql/_dml_constructors.py +140 -0
- sqlalchemy/sql/_elements_constructors.py +1850 -0
- sqlalchemy/sql/_orm_types.py +20 -0
- sqlalchemy/sql/_py_util.py +75 -0
- sqlalchemy/sql/_selectable_constructors.py +635 -0
- sqlalchemy/sql/_typing.py +460 -0
- sqlalchemy/sql/annotation.py +585 -0
- sqlalchemy/sql/base.py +2185 -0
- sqlalchemy/sql/cache_key.py +1057 -0
- sqlalchemy/sql/coercions.py +1405 -0
- sqlalchemy/sql/compiler.py +7818 -0
- sqlalchemy/sql/crud.py +1669 -0
- sqlalchemy/sql/ddl.py +1378 -0
- sqlalchemy/sql/default_comparator.py +552 -0
- sqlalchemy/sql/dml.py +1817 -0
- sqlalchemy/sql/elements.py +5499 -0
- sqlalchemy/sql/events.py +455 -0
- sqlalchemy/sql/expression.py +162 -0
- sqlalchemy/sql/functions.py +2055 -0
- sqlalchemy/sql/lambdas.py +1449 -0
- sqlalchemy/sql/naming.py +212 -0
- sqlalchemy/sql/operators.py +2579 -0
- sqlalchemy/sql/roles.py +323 -0
- sqlalchemy/sql/schema.py +6158 -0
- sqlalchemy/sql/selectable.py +7004 -0
- sqlalchemy/sql/sqltypes.py +3827 -0
- sqlalchemy/sql/traversals.py +1024 -0
- sqlalchemy/sql/type_api.py +2339 -0
- sqlalchemy/sql/util.py +1486 -0
- sqlalchemy/sql/visitors.py +1165 -0
- sqlalchemy/testing/__init__.py +96 -0
- sqlalchemy/testing/assertions.py +989 -0
- sqlalchemy/testing/assertsql.py +516 -0
- sqlalchemy/testing/asyncio.py +135 -0
- sqlalchemy/testing/config.py +427 -0
- sqlalchemy/testing/engines.py +472 -0
- sqlalchemy/testing/entities.py +117 -0
- sqlalchemy/testing/exclusions.py +435 -0
- sqlalchemy/testing/fixtures/__init__.py +28 -0
- sqlalchemy/testing/fixtures/base.py +366 -0
- sqlalchemy/testing/fixtures/mypy.py +312 -0
- sqlalchemy/testing/fixtures/orm.py +227 -0
- sqlalchemy/testing/fixtures/sql.py +503 -0
- sqlalchemy/testing/pickleable.py +155 -0
- sqlalchemy/testing/plugin/__init__.py +6 -0
- sqlalchemy/testing/plugin/bootstrap.py +51 -0
- sqlalchemy/testing/plugin/plugin_base.py +779 -0
- sqlalchemy/testing/plugin/pytestplugin.py +868 -0
- sqlalchemy/testing/profiling.py +324 -0
- sqlalchemy/testing/provision.py +496 -0
- sqlalchemy/testing/requirements.py +1818 -0
- sqlalchemy/testing/schema.py +224 -0
- sqlalchemy/testing/suite/__init__.py +19 -0
- sqlalchemy/testing/suite/test_cte.py +211 -0
- sqlalchemy/testing/suite/test_ddl.py +389 -0
- sqlalchemy/testing/suite/test_deprecations.py +153 -0
- sqlalchemy/testing/suite/test_dialect.py +740 -0
- sqlalchemy/testing/suite/test_insert.py +630 -0
- sqlalchemy/testing/suite/test_reflection.py +3225 -0
- sqlalchemy/testing/suite/test_results.py +502 -0
- sqlalchemy/testing/suite/test_rowcount.py +258 -0
- sqlalchemy/testing/suite/test_select.py +1999 -0
- sqlalchemy/testing/suite/test_sequence.py +317 -0
- sqlalchemy/testing/suite/test_types.py +2141 -0
- sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
- sqlalchemy/testing/suite/test_update_delete.py +139 -0
- sqlalchemy/testing/util.py +537 -0
- sqlalchemy/testing/warnings.py +52 -0
- sqlalchemy/types.py +76 -0
- sqlalchemy/util/__init__.py +160 -0
- sqlalchemy/util/_collections.py +715 -0
- sqlalchemy/util/_concurrency_py3k.py +288 -0
- sqlalchemy/util/_has_cy.py +40 -0
- sqlalchemy/util/_py_collections.py +541 -0
- sqlalchemy/util/compat.py +301 -0
- sqlalchemy/util/concurrency.py +108 -0
- sqlalchemy/util/deprecations.py +401 -0
- sqlalchemy/util/langhelpers.py +2218 -0
- sqlalchemy/util/preloaded.py +150 -0
- sqlalchemy/util/queue.py +322 -0
- sqlalchemy/util/tool_support.py +201 -0
- sqlalchemy/util/topological.py +120 -0
- sqlalchemy/util/typing.py +629 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# dialects/sqlite/dml.py
|
|
2
|
+
# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
|
|
3
|
+
# <see AUTHORS file>
|
|
4
|
+
#
|
|
5
|
+
# This module is part of SQLAlchemy and is released under
|
|
6
|
+
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .._typing import _OnConflictIndexElementsT
|
|
12
|
+
from .._typing import _OnConflictIndexWhereT
|
|
13
|
+
from .._typing import _OnConflictSetT
|
|
14
|
+
from .._typing import _OnConflictWhereT
|
|
15
|
+
from ... import util
|
|
16
|
+
from ...sql import coercions
|
|
17
|
+
from ...sql import roles
|
|
18
|
+
from ...sql._typing import _DMLTableArgument
|
|
19
|
+
from ...sql.base import _exclusive_against
|
|
20
|
+
from ...sql.base import _generative
|
|
21
|
+
from ...sql.base import ColumnCollection
|
|
22
|
+
from ...sql.base import ReadOnlyColumnCollection
|
|
23
|
+
from ...sql.dml import Insert as StandardInsert
|
|
24
|
+
from ...sql.elements import ClauseElement
|
|
25
|
+
from ...sql.elements import KeyedColumnElement
|
|
26
|
+
from ...sql.expression import alias
|
|
27
|
+
from ...util.typing import Self
|
|
28
|
+
|
|
29
|
+
__all__ = ("Insert", "insert")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def insert(table: _DMLTableArgument) -> Insert:
|
|
33
|
+
"""Construct a sqlite-specific variant :class:`_sqlite.Insert`
|
|
34
|
+
construct.
|
|
35
|
+
|
|
36
|
+
.. container:: inherited_member
|
|
37
|
+
|
|
38
|
+
The :func:`sqlalchemy.dialects.sqlite.insert` function creates
|
|
39
|
+
a :class:`sqlalchemy.dialects.sqlite.Insert`. This class is based
|
|
40
|
+
on the dialect-agnostic :class:`_sql.Insert` construct which may
|
|
41
|
+
be constructed using the :func:`_sql.insert` function in
|
|
42
|
+
SQLAlchemy Core.
|
|
43
|
+
|
|
44
|
+
The :class:`_sqlite.Insert` construct includes additional methods
|
|
45
|
+
:meth:`_sqlite.Insert.on_conflict_do_update`,
|
|
46
|
+
:meth:`_sqlite.Insert.on_conflict_do_nothing`.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
return Insert(table)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Insert(StandardInsert):
|
|
53
|
+
"""SQLite-specific implementation of INSERT.
|
|
54
|
+
|
|
55
|
+
Adds methods for SQLite-specific syntaxes such as ON CONFLICT.
|
|
56
|
+
|
|
57
|
+
The :class:`_sqlite.Insert` object is created using the
|
|
58
|
+
:func:`sqlalchemy.dialects.sqlite.insert` function.
|
|
59
|
+
|
|
60
|
+
.. versionadded:: 1.4
|
|
61
|
+
|
|
62
|
+
.. seealso::
|
|
63
|
+
|
|
64
|
+
:ref:`sqlite_on_conflict_insert`
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
stringify_dialect = "sqlite"
|
|
69
|
+
inherit_cache = False
|
|
70
|
+
|
|
71
|
+
@util.memoized_property
|
|
72
|
+
def excluded(
|
|
73
|
+
self,
|
|
74
|
+
) -> ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]:
|
|
75
|
+
"""Provide the ``excluded`` namespace for an ON CONFLICT statement
|
|
76
|
+
|
|
77
|
+
SQLite's ON CONFLICT clause allows reference to the row that would
|
|
78
|
+
be inserted, known as ``excluded``. This attribute provides
|
|
79
|
+
all columns in this row to be referenceable.
|
|
80
|
+
|
|
81
|
+
.. tip:: The :attr:`_sqlite.Insert.excluded` attribute is an instance
|
|
82
|
+
of :class:`_expression.ColumnCollection`, which provides an
|
|
83
|
+
interface the same as that of the :attr:`_schema.Table.c`
|
|
84
|
+
collection described at :ref:`metadata_tables_and_columns`.
|
|
85
|
+
With this collection, ordinary names are accessible like attributes
|
|
86
|
+
(e.g. ``stmt.excluded.some_column``), but special names and
|
|
87
|
+
dictionary method names should be accessed using indexed access,
|
|
88
|
+
such as ``stmt.excluded["column name"]`` or
|
|
89
|
+
``stmt.excluded["values"]``. See the docstring for
|
|
90
|
+
:class:`_expression.ColumnCollection` for further examples.
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
return alias(self.table, name="excluded").columns
|
|
94
|
+
|
|
95
|
+
_on_conflict_exclusive = _exclusive_against(
|
|
96
|
+
"_post_values_clause",
|
|
97
|
+
msgs={
|
|
98
|
+
"_post_values_clause": "This Insert construct already has "
|
|
99
|
+
"an ON CONFLICT clause established"
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@_generative
|
|
104
|
+
@_on_conflict_exclusive
|
|
105
|
+
def on_conflict_do_update(
|
|
106
|
+
self,
|
|
107
|
+
index_elements: _OnConflictIndexElementsT = None,
|
|
108
|
+
index_where: _OnConflictIndexWhereT = None,
|
|
109
|
+
set_: _OnConflictSetT = None,
|
|
110
|
+
where: _OnConflictWhereT = None,
|
|
111
|
+
) -> Self:
|
|
112
|
+
r"""
|
|
113
|
+
Specifies a DO UPDATE SET action for ON CONFLICT clause.
|
|
114
|
+
|
|
115
|
+
:param index_elements:
|
|
116
|
+
A sequence consisting of string column names, :class:`_schema.Column`
|
|
117
|
+
objects, or other column expression objects that will be used
|
|
118
|
+
to infer a target index or unique constraint.
|
|
119
|
+
|
|
120
|
+
:param index_where:
|
|
121
|
+
Additional WHERE criterion that can be used to infer a
|
|
122
|
+
conditional target index.
|
|
123
|
+
|
|
124
|
+
:param set\_:
|
|
125
|
+
A dictionary or other mapping object
|
|
126
|
+
where the keys are either names of columns in the target table,
|
|
127
|
+
or :class:`_schema.Column` objects or other ORM-mapped columns
|
|
128
|
+
matching that of the target table, and expressions or literals
|
|
129
|
+
as values, specifying the ``SET`` actions to take.
|
|
130
|
+
|
|
131
|
+
.. versionadded:: 1.4 The
|
|
132
|
+
:paramref:`_sqlite.Insert.on_conflict_do_update.set_`
|
|
133
|
+
parameter supports :class:`_schema.Column` objects from the target
|
|
134
|
+
:class:`_schema.Table` as keys.
|
|
135
|
+
|
|
136
|
+
.. warning:: This dictionary does **not** take into account
|
|
137
|
+
Python-specified default UPDATE values or generation functions,
|
|
138
|
+
e.g. those specified using :paramref:`_schema.Column.onupdate`.
|
|
139
|
+
These values will not be exercised for an ON CONFLICT style of
|
|
140
|
+
UPDATE, unless they are manually specified in the
|
|
141
|
+
:paramref:`.Insert.on_conflict_do_update.set_` dictionary.
|
|
142
|
+
|
|
143
|
+
:param where:
|
|
144
|
+
Optional argument. If present, can be a literal SQL
|
|
145
|
+
string or an acceptable expression for a ``WHERE`` clause
|
|
146
|
+
that restricts the rows affected by ``DO UPDATE SET``. Rows
|
|
147
|
+
not meeting the ``WHERE`` condition will not be updated
|
|
148
|
+
(effectively a ``DO NOTHING`` for those rows).
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
self._post_values_clause = OnConflictDoUpdate(
|
|
153
|
+
index_elements, index_where, set_, where
|
|
154
|
+
)
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
@_generative
|
|
158
|
+
@_on_conflict_exclusive
|
|
159
|
+
def on_conflict_do_nothing(
|
|
160
|
+
self,
|
|
161
|
+
index_elements: _OnConflictIndexElementsT = None,
|
|
162
|
+
index_where: _OnConflictIndexWhereT = None,
|
|
163
|
+
) -> Self:
|
|
164
|
+
"""
|
|
165
|
+
Specifies a DO NOTHING action for ON CONFLICT clause.
|
|
166
|
+
|
|
167
|
+
:param index_elements:
|
|
168
|
+
A sequence consisting of string column names, :class:`_schema.Column`
|
|
169
|
+
objects, or other column expression objects that will be used
|
|
170
|
+
to infer a target index or unique constraint.
|
|
171
|
+
|
|
172
|
+
:param index_where:
|
|
173
|
+
Additional WHERE criterion that can be used to infer a
|
|
174
|
+
conditional target index.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
self._post_values_clause = OnConflictDoNothing(
|
|
179
|
+
index_elements, index_where
|
|
180
|
+
)
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class OnConflictClause(ClauseElement):
|
|
185
|
+
stringify_dialect = "sqlite"
|
|
186
|
+
|
|
187
|
+
constraint_target: None
|
|
188
|
+
inferred_target_elements: _OnConflictIndexElementsT
|
|
189
|
+
inferred_target_whereclause: _OnConflictIndexWhereT
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
index_elements: _OnConflictIndexElementsT = None,
|
|
194
|
+
index_where: _OnConflictIndexWhereT = None,
|
|
195
|
+
):
|
|
196
|
+
if index_elements is not None:
|
|
197
|
+
self.constraint_target = None
|
|
198
|
+
self.inferred_target_elements = index_elements
|
|
199
|
+
self.inferred_target_whereclause = index_where
|
|
200
|
+
else:
|
|
201
|
+
self.constraint_target = self.inferred_target_elements = (
|
|
202
|
+
self.inferred_target_whereclause
|
|
203
|
+
) = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class OnConflictDoNothing(OnConflictClause):
|
|
207
|
+
__visit_name__ = "on_conflict_do_nothing"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class OnConflictDoUpdate(OnConflictClause):
|
|
211
|
+
__visit_name__ = "on_conflict_do_update"
|
|
212
|
+
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
index_elements: _OnConflictIndexElementsT = None,
|
|
216
|
+
index_where: _OnConflictIndexWhereT = None,
|
|
217
|
+
set_: _OnConflictSetT = None,
|
|
218
|
+
where: _OnConflictWhereT = None,
|
|
219
|
+
):
|
|
220
|
+
super().__init__(
|
|
221
|
+
index_elements=index_elements,
|
|
222
|
+
index_where=index_where,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if isinstance(set_, dict):
|
|
226
|
+
if not set_:
|
|
227
|
+
raise ValueError("set parameter dictionary must not be empty")
|
|
228
|
+
elif isinstance(set_, ColumnCollection):
|
|
229
|
+
set_ = dict(set_)
|
|
230
|
+
else:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
"set parameter must be a non-empty dictionary "
|
|
233
|
+
"or a ColumnCollection such as the `.c.` collection "
|
|
234
|
+
"of a Table object"
|
|
235
|
+
)
|
|
236
|
+
self.update_values_to_set = [
|
|
237
|
+
(coercions.expect(roles.DMLColumnRole, key), value)
|
|
238
|
+
for key, value in set_.items()
|
|
239
|
+
]
|
|
240
|
+
self.update_whereclause = where
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# dialects/sqlite/json.py
|
|
2
|
+
# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
|
|
3
|
+
# <see AUTHORS file>
|
|
4
|
+
#
|
|
5
|
+
# This module is part of SQLAlchemy and is released under
|
|
6
|
+
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
|
+
# mypy: ignore-errors
|
|
8
|
+
|
|
9
|
+
from ... import types as sqltypes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JSON(sqltypes.JSON):
|
|
13
|
+
"""SQLite JSON type.
|
|
14
|
+
|
|
15
|
+
SQLite supports JSON as of version 3.9 through its JSON1_ extension. Note
|
|
16
|
+
that JSON1_ is a
|
|
17
|
+
`loadable extension <https://www.sqlite.org/loadext.html>`_ and as such
|
|
18
|
+
may not be available, or may require run-time loading.
|
|
19
|
+
|
|
20
|
+
:class:`_sqlite.JSON` is used automatically whenever the base
|
|
21
|
+
:class:`_types.JSON` datatype is used against a SQLite backend.
|
|
22
|
+
|
|
23
|
+
.. seealso::
|
|
24
|
+
|
|
25
|
+
:class:`_types.JSON` - main documentation for the generic
|
|
26
|
+
cross-platform JSON datatype.
|
|
27
|
+
|
|
28
|
+
The :class:`_sqlite.JSON` type supports persistence of JSON values
|
|
29
|
+
as well as the core index operations provided by :class:`_types.JSON`
|
|
30
|
+
datatype, by adapting the operations to render the ``JSON_EXTRACT``
|
|
31
|
+
function wrapped in the ``JSON_QUOTE`` function at the database level.
|
|
32
|
+
Extracted values are quoted in order to ensure that the results are
|
|
33
|
+
always JSON string values.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
.. versionadded:: 1.3
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
.. _JSON1: https://www.sqlite.org/json1.html
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Note: these objects currently match exactly those of MySQL, however since
|
|
45
|
+
# these are not generalizable to all JSON implementations, remain separately
|
|
46
|
+
# implemented for each dialect.
|
|
47
|
+
class _FormatTypeMixin:
|
|
48
|
+
def _format_value(self, value):
|
|
49
|
+
raise NotImplementedError()
|
|
50
|
+
|
|
51
|
+
def bind_processor(self, dialect):
|
|
52
|
+
super_proc = self.string_bind_processor(dialect)
|
|
53
|
+
|
|
54
|
+
def process(value):
|
|
55
|
+
value = self._format_value(value)
|
|
56
|
+
if super_proc:
|
|
57
|
+
value = super_proc(value)
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
return process
|
|
61
|
+
|
|
62
|
+
def literal_processor(self, dialect):
|
|
63
|
+
super_proc = self.string_literal_processor(dialect)
|
|
64
|
+
|
|
65
|
+
def process(value):
|
|
66
|
+
value = self._format_value(value)
|
|
67
|
+
if super_proc:
|
|
68
|
+
value = super_proc(value)
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
return process
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class JSONIndexType(_FormatTypeMixin, sqltypes.JSON.JSONIndexType):
|
|
75
|
+
def _format_value(self, value):
|
|
76
|
+
if isinstance(value, int):
|
|
77
|
+
value = "$[%s]" % value
|
|
78
|
+
else:
|
|
79
|
+
value = '$."%s"' % value
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class JSONPathType(_FormatTypeMixin, sqltypes.JSON.JSONPathType):
|
|
84
|
+
def _format_value(self, value):
|
|
85
|
+
return "$%s" % (
|
|
86
|
+
"".join(
|
|
87
|
+
[
|
|
88
|
+
"[%s]" % elem if isinstance(elem, int) else '."%s"' % elem
|
|
89
|
+
for elem in value
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# dialects/sqlite/provision.py
|
|
2
|
+
# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
|
|
3
|
+
# <see AUTHORS file>
|
|
4
|
+
#
|
|
5
|
+
# This module is part of SQLAlchemy and is released under
|
|
6
|
+
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
|
+
# mypy: ignore-errors
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from ... import exc
|
|
13
|
+
from ...engine import url as sa_url
|
|
14
|
+
from ...testing.provision import create_db
|
|
15
|
+
from ...testing.provision import drop_db
|
|
16
|
+
from ...testing.provision import follower_url_from_main
|
|
17
|
+
from ...testing.provision import generate_driver_url
|
|
18
|
+
from ...testing.provision import log
|
|
19
|
+
from ...testing.provision import post_configure_engine
|
|
20
|
+
from ...testing.provision import run_reap_dbs
|
|
21
|
+
from ...testing.provision import stop_test_class_outside_fixtures
|
|
22
|
+
from ...testing.provision import temp_table_keyword_args
|
|
23
|
+
from ...testing.provision import upsert
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# TODO: I can't get this to build dynamically with pytest-xdist procs
|
|
27
|
+
_drivernames = {
|
|
28
|
+
"pysqlite",
|
|
29
|
+
"aiosqlite",
|
|
30
|
+
"pysqlcipher",
|
|
31
|
+
"pysqlite_numeric",
|
|
32
|
+
"pysqlite_dollar",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _format_url(url, driver, ident):
|
|
37
|
+
"""given a sqlite url + desired driver + ident, make a canonical
|
|
38
|
+
URL out of it
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
url = sa_url.make_url(url)
|
|
42
|
+
|
|
43
|
+
if driver is None:
|
|
44
|
+
driver = url.get_driver_name()
|
|
45
|
+
|
|
46
|
+
filename = url.database
|
|
47
|
+
|
|
48
|
+
needs_enc = driver == "pysqlcipher"
|
|
49
|
+
name_token = None
|
|
50
|
+
|
|
51
|
+
if filename and filename != ":memory:":
|
|
52
|
+
assert "test_schema" not in filename
|
|
53
|
+
tokens = re.split(r"[_\.]", filename)
|
|
54
|
+
|
|
55
|
+
new_filename = f"{driver}"
|
|
56
|
+
|
|
57
|
+
for token in tokens:
|
|
58
|
+
if token in _drivernames:
|
|
59
|
+
if driver is None:
|
|
60
|
+
driver = token
|
|
61
|
+
continue
|
|
62
|
+
elif token in ("db", "enc"):
|
|
63
|
+
continue
|
|
64
|
+
elif name_token is None:
|
|
65
|
+
name_token = token.strip("_")
|
|
66
|
+
|
|
67
|
+
assert name_token, f"sqlite filename has no name token: {url.database}"
|
|
68
|
+
|
|
69
|
+
new_filename = f"{name_token}_{driver}"
|
|
70
|
+
if ident:
|
|
71
|
+
new_filename += f"_{ident}"
|
|
72
|
+
new_filename += ".db"
|
|
73
|
+
if needs_enc:
|
|
74
|
+
new_filename += ".enc"
|
|
75
|
+
url = url.set(database=new_filename)
|
|
76
|
+
|
|
77
|
+
if needs_enc:
|
|
78
|
+
url = url.set(password="test")
|
|
79
|
+
|
|
80
|
+
url = url.set(drivername="sqlite+%s" % (driver,))
|
|
81
|
+
|
|
82
|
+
return url
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@generate_driver_url.for_db("sqlite")
|
|
86
|
+
def generate_driver_url(url, driver, query_str):
|
|
87
|
+
url = _format_url(url, driver, None)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
url.get_dialect()
|
|
91
|
+
except exc.NoSuchModuleError:
|
|
92
|
+
return None
|
|
93
|
+
else:
|
|
94
|
+
return url
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@follower_url_from_main.for_db("sqlite")
|
|
98
|
+
def _sqlite_follower_url_from_main(url, ident):
|
|
99
|
+
return _format_url(url, None, ident)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@post_configure_engine.for_db("sqlite")
|
|
103
|
+
def _sqlite_post_configure_engine(url, engine, follower_ident):
|
|
104
|
+
from sqlalchemy import event
|
|
105
|
+
|
|
106
|
+
if follower_ident:
|
|
107
|
+
attach_path = f"{follower_ident}_{engine.driver}_test_schema.db"
|
|
108
|
+
else:
|
|
109
|
+
attach_path = f"{engine.driver}_test_schema.db"
|
|
110
|
+
|
|
111
|
+
@event.listens_for(engine, "connect")
|
|
112
|
+
def connect(dbapi_connection, connection_record):
|
|
113
|
+
# use file DBs in all cases, memory acts kind of strangely
|
|
114
|
+
# as an attached
|
|
115
|
+
|
|
116
|
+
# NOTE! this has to be done *per connection*. New sqlite connection,
|
|
117
|
+
# as we get with say, QueuePool, the attaches are gone.
|
|
118
|
+
# so schemes to delete those attached files have to be done at the
|
|
119
|
+
# filesystem level and not rely upon what attachments are in a
|
|
120
|
+
# particular SQLite connection
|
|
121
|
+
dbapi_connection.execute(
|
|
122
|
+
f'ATTACH DATABASE "{attach_path}" AS test_schema'
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@event.listens_for(engine, "engine_disposed")
|
|
126
|
+
def dispose(engine):
|
|
127
|
+
"""most databases should be dropped using
|
|
128
|
+
stop_test_class_outside_fixtures
|
|
129
|
+
|
|
130
|
+
however a few tests like AttachedDBTest might not get triggered on
|
|
131
|
+
that main hook
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if os.path.exists(attach_path):
|
|
136
|
+
os.remove(attach_path)
|
|
137
|
+
|
|
138
|
+
filename = engine.url.database
|
|
139
|
+
|
|
140
|
+
if filename and filename != ":memory:" and os.path.exists(filename):
|
|
141
|
+
os.remove(filename)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@create_db.for_db("sqlite")
|
|
145
|
+
def _sqlite_create_db(cfg, eng, ident):
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@drop_db.for_db("sqlite")
|
|
150
|
+
def _sqlite_drop_db(cfg, eng, ident):
|
|
151
|
+
_drop_dbs_w_ident(eng.url.database, eng.driver, ident)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _drop_dbs_w_ident(databasename, driver, ident):
|
|
155
|
+
for path in os.listdir("."):
|
|
156
|
+
fname, ext = os.path.split(path)
|
|
157
|
+
if ident in fname and ext in [".db", ".db.enc"]:
|
|
158
|
+
log.info("deleting SQLite database file: %s", path)
|
|
159
|
+
os.remove(path)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@stop_test_class_outside_fixtures.for_db("sqlite")
|
|
163
|
+
def stop_test_class_outside_fixtures(config, db, cls):
|
|
164
|
+
db.dispose()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@temp_table_keyword_args.for_db("sqlite")
|
|
168
|
+
def _sqlite_temp_table_keyword_args(cfg, eng):
|
|
169
|
+
return {"prefixes": ["TEMPORARY"]}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@run_reap_dbs.for_db("sqlite")
|
|
173
|
+
def _reap_sqlite_dbs(url, idents):
|
|
174
|
+
log.info("db reaper connecting to %r", url)
|
|
175
|
+
log.info("identifiers in file: %s", ", ".join(idents))
|
|
176
|
+
url = sa_url.make_url(url)
|
|
177
|
+
for ident in idents:
|
|
178
|
+
for drivername in _drivernames:
|
|
179
|
+
_drop_dbs_w_ident(url.database, drivername, ident)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@upsert.for_db("sqlite")
|
|
183
|
+
def _upsert(
|
|
184
|
+
cfg, table, returning, *, set_lambda=None, sort_by_parameter_order=False
|
|
185
|
+
):
|
|
186
|
+
from sqlalchemy.dialects.sqlite import insert
|
|
187
|
+
|
|
188
|
+
stmt = insert(table)
|
|
189
|
+
|
|
190
|
+
if set_lambda:
|
|
191
|
+
stmt = stmt.on_conflict_do_update(set_=set_lambda(stmt.excluded))
|
|
192
|
+
else:
|
|
193
|
+
stmt = stmt.on_conflict_do_nothing()
|
|
194
|
+
|
|
195
|
+
stmt = stmt.returning(
|
|
196
|
+
*returning, sort_by_parameter_order=sort_by_parameter_order
|
|
197
|
+
)
|
|
198
|
+
return stmt
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# dialects/sqlite/pysqlcipher.py
|
|
2
|
+
# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
|
|
3
|
+
# <see AUTHORS file>
|
|
4
|
+
#
|
|
5
|
+
# This module is part of SQLAlchemy and is released under
|
|
6
|
+
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
|
+
# mypy: ignore-errors
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
.. dialect:: sqlite+pysqlcipher
|
|
12
|
+
:name: pysqlcipher
|
|
13
|
+
:dbapi: sqlcipher 3 or pysqlcipher
|
|
14
|
+
:connectstring: sqlite+pysqlcipher://:passphrase@/file_path[?kdf_iter=<iter>]
|
|
15
|
+
|
|
16
|
+
Dialect for support of DBAPIs that make use of the
|
|
17
|
+
`SQLCipher <https://www.zetetic.net/sqlcipher>`_ backend.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Driver
|
|
21
|
+
------
|
|
22
|
+
|
|
23
|
+
Current dialect selection logic is:
|
|
24
|
+
|
|
25
|
+
* If the :paramref:`_sa.create_engine.module` parameter supplies a DBAPI module,
|
|
26
|
+
that module is used.
|
|
27
|
+
* Otherwise for Python 3, choose https://pypi.org/project/sqlcipher3/
|
|
28
|
+
* If not available, fall back to https://pypi.org/project/pysqlcipher3/
|
|
29
|
+
* For Python 2, https://pypi.org/project/pysqlcipher/ is used.
|
|
30
|
+
|
|
31
|
+
.. warning:: The ``pysqlcipher3`` and ``pysqlcipher`` DBAPI drivers are no
|
|
32
|
+
longer maintained; the ``sqlcipher3`` driver as of this writing appears
|
|
33
|
+
to be current. For future compatibility, any pysqlcipher-compatible DBAPI
|
|
34
|
+
may be used as follows::
|
|
35
|
+
|
|
36
|
+
import sqlcipher_compatible_driver
|
|
37
|
+
|
|
38
|
+
from sqlalchemy import create_engine
|
|
39
|
+
|
|
40
|
+
e = create_engine(
|
|
41
|
+
"sqlite+pysqlcipher://:password@/dbname.db",
|
|
42
|
+
module=sqlcipher_compatible_driver
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
These drivers make use of the SQLCipher engine. This system essentially
|
|
46
|
+
introduces new PRAGMA commands to SQLite which allows the setting of a
|
|
47
|
+
passphrase and other encryption parameters, allowing the database file to be
|
|
48
|
+
encrypted.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Connect Strings
|
|
52
|
+
---------------
|
|
53
|
+
|
|
54
|
+
The format of the connect string is in every way the same as that
|
|
55
|
+
of the :mod:`~sqlalchemy.dialects.sqlite.pysqlite` driver, except that the
|
|
56
|
+
"password" field is now accepted, which should contain a passphrase::
|
|
57
|
+
|
|
58
|
+
e = create_engine('sqlite+pysqlcipher://:testing@/foo.db')
|
|
59
|
+
|
|
60
|
+
For an absolute file path, two leading slashes should be used for the
|
|
61
|
+
database name::
|
|
62
|
+
|
|
63
|
+
e = create_engine('sqlite+pysqlcipher://:testing@//path/to/foo.db')
|
|
64
|
+
|
|
65
|
+
A selection of additional encryption-related pragmas supported by SQLCipher
|
|
66
|
+
as documented at https://www.zetetic.net/sqlcipher/sqlcipher-api/ can be passed
|
|
67
|
+
in the query string, and will result in that PRAGMA being called for each
|
|
68
|
+
new connection. Currently, ``cipher``, ``kdf_iter``
|
|
69
|
+
``cipher_page_size`` and ``cipher_use_hmac`` are supported::
|
|
70
|
+
|
|
71
|
+
e = create_engine('sqlite+pysqlcipher://:testing@/foo.db?cipher=aes-256-cfb&kdf_iter=64000')
|
|
72
|
+
|
|
73
|
+
.. warning:: Previous versions of sqlalchemy did not take into consideration
|
|
74
|
+
the encryption-related pragmas passed in the url string, that were silently
|
|
75
|
+
ignored. This may cause errors when opening files saved by a
|
|
76
|
+
previous sqlalchemy version if the encryption options do not match.
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Pooling Behavior
|
|
80
|
+
----------------
|
|
81
|
+
|
|
82
|
+
The driver makes a change to the default pool behavior of pysqlite
|
|
83
|
+
as described in :ref:`pysqlite_threading_pooling`. The pysqlcipher driver
|
|
84
|
+
has been observed to be significantly slower on connection than the
|
|
85
|
+
pysqlite driver, most likely due to the encryption overhead, so the
|
|
86
|
+
dialect here defaults to using the :class:`.SingletonThreadPool`
|
|
87
|
+
implementation,
|
|
88
|
+
instead of the :class:`.NullPool` pool used by pysqlite. As always, the pool
|
|
89
|
+
implementation is entirely configurable using the
|
|
90
|
+
:paramref:`_sa.create_engine.poolclass` parameter; the :class:`.
|
|
91
|
+
StaticPool` may
|
|
92
|
+
be more feasible for single-threaded use, or :class:`.NullPool` may be used
|
|
93
|
+
to prevent unencrypted connections from being held open for long periods of
|
|
94
|
+
time, at the expense of slower startup time for new connections.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
""" # noqa
|
|
98
|
+
|
|
99
|
+
from .pysqlite import SQLiteDialect_pysqlite
|
|
100
|
+
from ... import pool
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SQLiteDialect_pysqlcipher(SQLiteDialect_pysqlite):
|
|
104
|
+
driver = "pysqlcipher"
|
|
105
|
+
supports_statement_cache = True
|
|
106
|
+
|
|
107
|
+
pragmas = ("kdf_iter", "cipher", "cipher_page_size", "cipher_use_hmac")
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def import_dbapi(cls):
|
|
111
|
+
try:
|
|
112
|
+
import sqlcipher3 as sqlcipher
|
|
113
|
+
except ImportError:
|
|
114
|
+
pass
|
|
115
|
+
else:
|
|
116
|
+
return sqlcipher
|
|
117
|
+
|
|
118
|
+
from pysqlcipher3 import dbapi2 as sqlcipher
|
|
119
|
+
|
|
120
|
+
return sqlcipher
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def get_pool_class(cls, url):
|
|
124
|
+
return pool.SingletonThreadPool
|
|
125
|
+
|
|
126
|
+
def on_connect_url(self, url):
|
|
127
|
+
super_on_connect = super().on_connect_url(url)
|
|
128
|
+
|
|
129
|
+
# pull the info we need from the URL early. Even though URL
|
|
130
|
+
# is immutable, we don't want any in-place changes to the URL
|
|
131
|
+
# to affect things
|
|
132
|
+
passphrase = url.password or ""
|
|
133
|
+
url_query = dict(url.query)
|
|
134
|
+
|
|
135
|
+
def on_connect(conn):
|
|
136
|
+
cursor = conn.cursor()
|
|
137
|
+
cursor.execute('pragma key="%s"' % passphrase)
|
|
138
|
+
for prag in self.pragmas:
|
|
139
|
+
value = url_query.get(prag, None)
|
|
140
|
+
if value is not None:
|
|
141
|
+
cursor.execute('pragma %s="%s"' % (prag, value))
|
|
142
|
+
cursor.close()
|
|
143
|
+
|
|
144
|
+
if super_on_connect:
|
|
145
|
+
super_on_connect(conn)
|
|
146
|
+
|
|
147
|
+
return on_connect
|
|
148
|
+
|
|
149
|
+
def create_connect_args(self, url):
|
|
150
|
+
plain_url = url._replace(password=None)
|
|
151
|
+
plain_url = plain_url.difference_update_query(self.pragmas)
|
|
152
|
+
return super().create_connect_args(plain_url)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
dialect = SQLiteDialect_pysqlcipher
|