SQLAlchemy 2.0.42__py3-none-any.whl → 2.0.44__py3-none-any.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.
Files changed (70) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/connectors/asyncio.py +81 -3
  3. sqlalchemy/connectors/pyodbc.py +5 -0
  4. sqlalchemy/dialects/mssql/base.py +5 -0
  5. sqlalchemy/dialects/mysql/aiomysql.py +13 -4
  6. sqlalchemy/dialects/mysql/asyncmy.py +13 -4
  7. sqlalchemy/dialects/mysql/mariadbconnector.py +3 -0
  8. sqlalchemy/dialects/mysql/mysqlconnector.py +3 -0
  9. sqlalchemy/dialects/mysql/mysqldb.py +3 -0
  10. sqlalchemy/dialects/oracle/__init__.py +4 -0
  11. sqlalchemy/dialects/oracle/base.py +79 -24
  12. sqlalchemy/dialects/oracle/cx_oracle.py +3 -0
  13. sqlalchemy/dialects/oracle/oracledb.py +2 -4
  14. sqlalchemy/dialects/oracle/vector.py +104 -6
  15. sqlalchemy/dialects/postgresql/_psycopg_common.py +3 -0
  16. sqlalchemy/dialects/postgresql/array.py +15 -5
  17. sqlalchemy/dialects/postgresql/asyncpg.py +21 -27
  18. sqlalchemy/dialects/postgresql/json.py +28 -0
  19. sqlalchemy/dialects/postgresql/pg8000.py +3 -0
  20. sqlalchemy/dialects/postgresql/psycopg.py +3 -0
  21. sqlalchemy/dialects/sqlite/aiosqlite.py +3 -0
  22. sqlalchemy/dialects/sqlite/base.py +44 -13
  23. sqlalchemy/dialects/sqlite/pysqlite.py +69 -36
  24. sqlalchemy/engine/base.py +11 -5
  25. sqlalchemy/engine/create.py +15 -0
  26. sqlalchemy/engine/default.py +7 -0
  27. sqlalchemy/engine/interfaces.py +32 -0
  28. sqlalchemy/engine/result.py +1 -1
  29. sqlalchemy/event/__init__.py +1 -0
  30. sqlalchemy/event/attr.py +35 -14
  31. sqlalchemy/event/legacy.py +15 -3
  32. sqlalchemy/ext/asyncio/engine.py +4 -2
  33. sqlalchemy/ext/asyncio/result.py +3 -0
  34. sqlalchemy/ext/asyncio/scoping.py +0 -14
  35. sqlalchemy/ext/asyncio/session.py +0 -14
  36. sqlalchemy/ext/hybrid.py +8 -6
  37. sqlalchemy/ext/indexable.py +37 -19
  38. sqlalchemy/ext/mutable.py +26 -37
  39. sqlalchemy/orm/__init__.py +1 -0
  40. sqlalchemy/orm/decl_api.py +95 -11
  41. sqlalchemy/orm/descriptor_props.py +5 -1
  42. sqlalchemy/orm/events.py +60 -77
  43. sqlalchemy/orm/loading.py +13 -9
  44. sqlalchemy/orm/persistence.py +8 -2
  45. sqlalchemy/orm/properties.py +77 -26
  46. sqlalchemy/orm/query.py +16 -10
  47. sqlalchemy/orm/scoping.py +0 -14
  48. sqlalchemy/orm/session.py +0 -14
  49. sqlalchemy/orm/writeonly.py +2 -2
  50. sqlalchemy/sql/dml.py +19 -6
  51. sqlalchemy/sql/elements.py +36 -1
  52. sqlalchemy/sql/functions.py +2 -4
  53. sqlalchemy/sql/schema.py +7 -3
  54. sqlalchemy/sql/selectable.py +40 -24
  55. sqlalchemy/sql/sqltypes.py +3 -3
  56. sqlalchemy/testing/plugin/pytestplugin.py +9 -3
  57. sqlalchemy/testing/profiling.py +3 -0
  58. sqlalchemy/testing/requirements.py +19 -1
  59. sqlalchemy/testing/suite/test_dialect.py +37 -1
  60. sqlalchemy/testing/suite/test_reflection.py +20 -0
  61. sqlalchemy/util/__init__.py +2 -0
  62. sqlalchemy/util/compat.py +13 -0
  63. sqlalchemy/util/concurrency.py +2 -0
  64. sqlalchemy/util/langhelpers.py +8 -5
  65. sqlalchemy/util/typing.py +3 -2
  66. {sqlalchemy-2.0.42.dist-info → sqlalchemy-2.0.44.dist-info}/METADATA +2 -2
  67. {sqlalchemy-2.0.42.dist-info → sqlalchemy-2.0.44.dist-info}/RECORD +70 -70
  68. {sqlalchemy-2.0.42.dist-info → sqlalchemy-2.0.44.dist-info}/WHEEL +0 -0
  69. {sqlalchemy-2.0.42.dist-info → sqlalchemy-2.0.44.dist-info}/licenses/LICENSE +0 -0
  70. {sqlalchemy-2.0.42.dist-info → sqlalchemy-2.0.44.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -269,7 +269,7 @@ from .types import Uuid as Uuid
269
269
  from .types import VARBINARY as VARBINARY
270
270
  from .types import VARCHAR as VARCHAR
271
271
 
272
- __version__ = "2.0.42"
272
+ __version__ = "2.0.44"
273
273
 
274
274
 
275
275
  def __go(lcls: Any) -> None:
@@ -19,11 +19,15 @@ from typing import Iterator
19
19
  from typing import NoReturn
20
20
  from typing import Optional
21
21
  from typing import Sequence
22
+ from typing import Tuple
23
+ from typing import Type
22
24
  from typing import TYPE_CHECKING
23
25
 
24
26
  from ..engine import AdaptedConnection
27
+ from ..util import EMPTY_DICT
25
28
  from ..util.concurrency import await_fallback
26
29
  from ..util.concurrency import await_only
30
+ from ..util.concurrency import in_greenlet
27
31
  from ..util.typing import Protocol
28
32
 
29
33
  if TYPE_CHECKING:
@@ -129,8 +133,11 @@ class AsyncAdapt_dbapi_cursor:
129
133
  "await_",
130
134
  "_cursor",
131
135
  "_rows",
136
+ "_soft_closed_memoized",
132
137
  )
133
138
 
139
+ _awaitable_cursor_close: bool = True
140
+
134
141
  _cursor: AsyncIODBAPICursor
135
142
  _adapt_connection: AsyncAdapt_dbapi_connection
136
143
  _connection: AsyncIODBAPIConnection
@@ -144,7 +151,7 @@ class AsyncAdapt_dbapi_cursor:
144
151
 
145
152
  cursor = self._make_new_cursor(self._connection)
146
153
  self._cursor = self._aenter_cursor(cursor)
147
-
154
+ self._soft_closed_memoized = EMPTY_DICT
148
155
  if not self.server_side:
149
156
  self._rows = collections.deque()
150
157
 
@@ -158,6 +165,8 @@ class AsyncAdapt_dbapi_cursor:
158
165
 
159
166
  @property
160
167
  def description(self) -> Optional[_DBAPICursorDescription]:
168
+ if "description" in self._soft_closed_memoized:
169
+ return self._soft_closed_memoized["description"] # type: ignore[no-any-return] # noqa: E501
161
170
  return self._cursor.description
162
171
 
163
172
  @property
@@ -176,11 +185,40 @@ class AsyncAdapt_dbapi_cursor:
176
185
  def lastrowid(self) -> int:
177
186
  return self._cursor.lastrowid
178
187
 
188
+ async def _async_soft_close(self) -> None:
189
+ """close the cursor but keep the results pending, and memoize the
190
+ description.
191
+
192
+ .. versionadded:: 2.0.44
193
+
194
+ """
195
+
196
+ if not self._awaitable_cursor_close or self.server_side:
197
+ return
198
+
199
+ self._soft_closed_memoized = self._soft_closed_memoized.union(
200
+ {
201
+ "description": self._cursor.description,
202
+ }
203
+ )
204
+ await self._cursor.close()
205
+
179
206
  def close(self) -> None:
180
- # note we aren't actually closing the cursor here,
181
- # we are just letting GC do it. see notes in aiomysql dialect
182
207
  self._rows.clear()
183
208
 
209
+ # updated as of 2.0.44
210
+ # try to "close" the cursor based on what we know about the driver
211
+ # and if we are able to. otherwise, hope that the asyncio
212
+ # extension called _async_soft_close() if the cursor is going into
213
+ # a sync context
214
+ if self._cursor is None or bool(self._soft_closed_memoized):
215
+ return
216
+
217
+ if not self._awaitable_cursor_close:
218
+ self._cursor.close() # type: ignore[unused-coroutine]
219
+ elif in_greenlet():
220
+ self.await_(self._cursor.close())
221
+
184
222
  def execute(
185
223
  self,
186
224
  operation: Any,
@@ -349,3 +387,43 @@ class AsyncAdaptFallback_dbapi_connection(AsyncAdapt_dbapi_connection):
349
387
  __slots__ = ()
350
388
 
351
389
  await_ = staticmethod(await_fallback)
390
+
391
+
392
+ class AsyncAdapt_terminate:
393
+ """Mixin for a AsyncAdapt_dbapi_connection to add terminate support."""
394
+
395
+ __slots__ = ()
396
+
397
+ def terminate(self) -> None:
398
+ if in_greenlet():
399
+ # in a greenlet; this is the connection was invalidated case.
400
+ try:
401
+ # try to gracefully close; see #10717
402
+ self.await_(asyncio.shield(self._terminate_graceful_close())) # type: ignore[attr-defined] # noqa: E501
403
+ except self._terminate_handled_exceptions() as e:
404
+ # in the case where we are recycling an old connection
405
+ # that may have already been disconnected, close() will
406
+ # fail. In this case, terminate
407
+ # the connection without any further waiting.
408
+ # see issue #8419
409
+ self._terminate_force_close()
410
+ if isinstance(e, asyncio.CancelledError):
411
+ # re-raise CancelledError if we were cancelled
412
+ raise
413
+ else:
414
+ # not in a greenlet; this is the gc cleanup case
415
+ self._terminate_force_close()
416
+
417
+ def _terminate_handled_exceptions(self) -> Tuple[Type[BaseException], ...]:
418
+ """Returns the exceptions that should be handled when
419
+ calling _graceful_close.
420
+ """
421
+ return (asyncio.TimeoutError, asyncio.CancelledError, OSError)
422
+
423
+ async def _terminate_graceful_close(self) -> None:
424
+ """Try to close connection gracefully"""
425
+ raise NotImplementedError
426
+
427
+ def _terminate_force_close(self) -> None:
428
+ """Terminate the connection"""
429
+ raise NotImplementedError
@@ -243,3 +243,8 @@ class PyODBCConnector(Connector):
243
243
  else:
244
244
  dbapi_connection.autocommit = False
245
245
  super().set_isolation_level(dbapi_connection, level)
246
+
247
+ def detect_autocommit_setting(
248
+ self, dbapi_conn: interfaces.DBAPIConnection
249
+ ) -> bool:
250
+ return bool(dbapi_conn.autocommit)
@@ -3503,6 +3503,9 @@ join sys.schemas as sch on
3503
3503
  where
3504
3504
  tab.name = :tabname
3505
3505
  and sch.name = :schname
3506
+ order by
3507
+ ind_col.index_id,
3508
+ ind_col.key_ordinal
3506
3509
  """
3507
3510
  )
3508
3511
  .bindparams(
@@ -3998,6 +4001,8 @@ index_info AS (
3998
4001
  index_info.index_schema = fk_info.unique_constraint_schema
3999
4002
  AND index_info.index_name = fk_info.unique_constraint_name
4000
4003
  AND index_info.ordinal_position = fk_info.ordinal_position
4004
+ AND NOT (index_info.table_schema = fk_info.table_schema
4005
+ AND index_info.table_name = fk_info.table_name)
4001
4006
 
4002
4007
  ORDER BY fk_info.constraint_schema, fk_info.constraint_name,
4003
4008
  fk_info.ordinal_position
@@ -45,6 +45,7 @@ from ...connectors.asyncio import AsyncAdapt_dbapi_connection
45
45
  from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
46
46
  from ...connectors.asyncio import AsyncAdapt_dbapi_module
47
47
  from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
48
+ from ...connectors.asyncio import AsyncAdapt_terminate
48
49
  from ...util.concurrency import await_fallback
49
50
  from ...util.concurrency import await_only
50
51
 
@@ -82,7 +83,9 @@ class AsyncAdapt_aiomysql_ss_cursor(
82
83
  )
83
84
 
84
85
 
85
- class AsyncAdapt_aiomysql_connection(AsyncAdapt_dbapi_connection):
86
+ class AsyncAdapt_aiomysql_connection(
87
+ AsyncAdapt_terminate, AsyncAdapt_dbapi_connection
88
+ ):
86
89
  __slots__ = ()
87
90
 
88
91
  _cursor_cls = AsyncAdapt_aiomysql_cursor
@@ -98,13 +101,19 @@ class AsyncAdapt_aiomysql_connection(AsyncAdapt_dbapi_connection):
98
101
  def autocommit(self, value: Any) -> None:
99
102
  self.await_(self._connection.autocommit(value))
100
103
 
101
- def terminate(self) -> None:
102
- # it's not awaitable.
103
- self._connection.close()
104
+ def get_autocommit(self) -> bool:
105
+ return self._connection.get_autocommit() # type: ignore
104
106
 
105
107
  def close(self) -> None:
106
108
  self.await_(self._connection.ensure_closed())
107
109
 
110
+ async def _terminate_graceful_close(self) -> None:
111
+ await self._connection.ensure_closed()
112
+
113
+ def _terminate_force_close(self) -> None:
114
+ # it's not awaitable.
115
+ self._connection.close()
116
+
108
117
 
109
118
  class AsyncAdaptFallback_aiomysql_connection(AsyncAdapt_aiomysql_connection):
110
119
  __slots__ = ()
@@ -42,6 +42,7 @@ from ...connectors.asyncio import AsyncAdapt_dbapi_connection
42
42
  from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
43
43
  from ...connectors.asyncio import AsyncAdapt_dbapi_module
44
44
  from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
45
+ from ...connectors.asyncio import AsyncAdapt_terminate
45
46
  from ...util.concurrency import await_fallback
46
47
  from ...util.concurrency import await_only
47
48
 
@@ -73,7 +74,9 @@ class AsyncAdapt_asyncmy_ss_cursor(
73
74
  )
74
75
 
75
76
 
76
- class AsyncAdapt_asyncmy_connection(AsyncAdapt_dbapi_connection):
77
+ class AsyncAdapt_asyncmy_connection(
78
+ AsyncAdapt_terminate, AsyncAdapt_dbapi_connection
79
+ ):
77
80
  __slots__ = ()
78
81
 
79
82
  _cursor_cls = AsyncAdapt_asyncmy_cursor
@@ -104,13 +107,19 @@ class AsyncAdapt_asyncmy_connection(AsyncAdapt_dbapi_connection):
104
107
  def autocommit(self, value: Any) -> None:
105
108
  self.await_(self._connection.autocommit(value))
106
109
 
107
- def terminate(self) -> None:
108
- # it's not awaitable.
109
- self._connection.close()
110
+ def get_autocommit(self) -> bool:
111
+ return self._connection.get_autocommit() # type: ignore
110
112
 
111
113
  def close(self) -> None:
112
114
  self.await_(self._connection.ensure_closed())
113
115
 
116
+ async def _terminate_graceful_close(self) -> None:
117
+ await self._connection.ensure_closed()
118
+
119
+ def _terminate_force_close(self) -> None:
120
+ # it's not awaitable.
121
+ self._connection.close()
122
+
114
123
 
115
124
  class AsyncAdaptFallback_asyncmy_connection(AsyncAdapt_asyncmy_connection):
116
125
  __slots__ = ()
@@ -253,6 +253,9 @@ class MySQLDialect_mariadbconnector(MySQLDialect):
253
253
  "AUTOCOMMIT",
254
254
  )
255
255
 
256
+ def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
257
+ return bool(dbapi_conn.autocommit)
258
+
256
259
  def set_isolation_level(
257
260
  self, dbapi_connection: DBAPIConnection, level: IsolationLevel
258
261
  ) -> None:
@@ -277,6 +277,9 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
277
277
  "AUTOCOMMIT",
278
278
  )
279
279
 
280
+ def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
281
+ return bool(dbapi_conn.autocommit)
282
+
280
283
  def set_isolation_level(
281
284
  self, dbapi_connection: DBAPIConnection, level: IsolationLevel
282
285
  ) -> None:
@@ -298,6 +298,9 @@ class MySQLDialect_mysqldb(MySQLDialect):
298
298
  "AUTOCOMMIT",
299
299
  )
300
300
 
301
+ def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
302
+ return dbapi_conn.get_autocommit() # type: ignore[no-any-return]
303
+
301
304
  def set_isolation_level(
302
305
  self, dbapi_connection: DBAPIConnection, level: IsolationLevel
303
306
  ) -> None:
@@ -35,8 +35,10 @@ from .base import VARCHAR2
35
35
  from .base import VECTOR
36
36
  from .base import VectorIndexConfig
37
37
  from .base import VectorIndexType
38
+ from .vector import SparseVector
38
39
  from .vector import VectorDistanceType
39
40
  from .vector import VectorStorageFormat
41
+ from .vector import VectorStorageType
40
42
 
41
43
  # Alias oracledb also as oracledb_async
42
44
  oracledb_async = type(
@@ -74,4 +76,6 @@ __all__ = (
74
76
  "VectorIndexType",
75
77
  "VectorIndexConfig",
76
78
  "VectorStorageFormat",
79
+ "VectorStorageType",
80
+ "SparseVector",
77
81
  )
@@ -729,8 +729,22 @@ VECTOR Datatype
729
729
 
730
730
  Oracle Database 23ai introduced a new VECTOR datatype for artificial intelligence
731
731
  and machine learning search operations. The VECTOR datatype is a homogeneous array
732
- of 8-bit signed integers, 8-bit unsigned integers (binary), 32-bit floating-point numbers,
733
- or 64-bit floating-point numbers.
732
+ of 8-bit signed integers, 8-bit unsigned integers (binary), 32-bit floating-point
733
+ numbers, or 64-bit floating-point numbers.
734
+
735
+ A vector's storage type can be either DENSE or SPARSE. A dense vector contains
736
+ meaningful values in most or all of its dimensions. In contrast, a sparse vector
737
+ has non-zero values in only a few dimensions, with the majority being zero.
738
+
739
+ Sparse vectors are represented by the total number of vector dimensions, an array
740
+ of indices, and an array of values where each value’s location in the vector is
741
+ indicated by the corresponding indices array position. All other vector values are
742
+ treated as zero.
743
+
744
+ The storage formats that can be used with sparse vectors are float32, float64, and
745
+ int8. Note that the binary storage format cannot be used with sparse vectors.
746
+
747
+ Sparse vectors are supported when you are using Oracle Database 23.7 or later.
734
748
 
735
749
  .. seealso::
736
750
 
@@ -738,17 +752,26 @@ or 64-bit floating-point numbers.
738
752
  <https://python-oracledb.readthedocs.io/en/latest/user_guide/vector_data_type.html>`_ - in the documentation
739
753
  for the :ref:`oracledb` driver.
740
754
 
741
- .. versionadded:: 2.0.41
755
+ .. versionadded:: 2.0.41 - Added VECTOR datatype
756
+
757
+ .. versionadded:: 2.0.43 - Added DENSE/SPARSE support
742
758
 
743
759
  CREATE TABLE support for VECTOR
744
760
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
745
761
 
746
- With the :class:`.VECTOR` datatype, you can specify the dimension for the data
747
- and the storage format. Valid values for storage format are enum values from
748
- :class:`.VectorStorageFormat`. To create a table that includes a
749
- :class:`.VECTOR` column::
762
+ With the :class:`.VECTOR` datatype, you can specify the number of dimensions,
763
+ the storage format, and the storage type for the data. Valid values for the
764
+ storage format are enum members of :class:`.VectorStorageFormat`. Valid values
765
+ for the storage type are enum members of :class:`.VectorStorageType`. If
766
+ storage type is not specified, a DENSE vector is created by default.
767
+
768
+ To create a table that includes a :class:`.VECTOR` column::
750
769
 
751
- from sqlalchemy.dialects.oracle import VECTOR, VectorStorageFormat
770
+ from sqlalchemy.dialects.oracle import (
771
+ VECTOR,
772
+ VectorStorageFormat,
773
+ VectorStorageType,
774
+ )
752
775
 
753
776
  t = Table(
754
777
  "t1",
@@ -756,7 +779,11 @@ and the storage format. Valid values for storage format are enum values from
756
779
  Column("id", Integer, primary_key=True),
757
780
  Column(
758
781
  "embedding",
759
- VECTOR(dim=3, storage_format=VectorStorageFormat.FLOAT32),
782
+ VECTOR(
783
+ dim=3,
784
+ storage_format=VectorStorageFormat.FLOAT32,
785
+ storage_type=VectorStorageType.SPARSE,
786
+ ),
760
787
  ),
761
788
  Column(...),
762
789
  ...,
@@ -764,31 +791,40 @@ and the storage format. Valid values for storage format are enum values from
764
791
 
765
792
  Vectors can also be defined with an arbitrary number of dimensions and formats.
766
793
  This allows you to specify vectors of different dimensions with the various
767
- storage formats mentioned above.
794
+ storage formats mentioned below.
768
795
 
769
796
  **Examples**
770
797
 
771
- * In this case, the storage format is flexible, allowing any vector type data to be inserted,
772
- such as INT8 or BINARY etc::
798
+ * In this case, the storage format is flexible, allowing any vector type data to be
799
+ inserted, such as INT8 or BINARY etc::
773
800
 
774
801
  vector_col: Mapped[array.array] = mapped_column(VECTOR(dim=3))
775
802
 
776
- * The dimension is flexible in this case, meaning that any dimension vector can be used::
803
+ * The dimension is flexible in this case, meaning that any dimension vector can
804
+ be used::
777
805
 
778
806
  vector_col: Mapped[array.array] = mapped_column(
779
807
  VECTOR(storage_format=VectorStorageType.INT8)
780
808
  )
781
809
 
782
- * Both the dimensions and the storage format are flexible::
810
+ * Both the dimensions and the storage format are flexible. It creates a DENSE vector::
783
811
 
784
812
  vector_col: Mapped[array.array] = mapped_column(VECTOR)
785
813
 
814
+ * To create a SPARSE vector with both dimensions and the storage format as flexible,
815
+ use the :attr:`.VectorStorageType.SPARSE` storage type::
816
+
817
+ vector_col: Mapped[array.array] = mapped_column(
818
+ VECTOR(storage_type=VectorStorageType.SPARSE)
819
+ )
820
+
786
821
  Python Datatypes for VECTOR
787
822
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~
788
823
 
789
824
  VECTOR data can be inserted using Python list or Python ``array.array()`` objects.
790
- Python arrays of type FLOAT (32-bit), DOUBLE (64-bit), or INT (8-bit signed integer)
791
- are used as bind values when inserting VECTOR columns::
825
+ Python arrays of type FLOAT (32-bit), DOUBLE (64-bit), INT (8-bit signed integers),
826
+ or BINARY (8-bit unsigned integers) are used as bind values when inserting
827
+ VECTOR columns::
792
828
 
793
829
  from sqlalchemy import insert, select
794
830
 
@@ -798,6 +834,21 @@ are used as bind values when inserting VECTOR columns::
798
834
  {"id": 1, "embedding": [1, 2, 3]},
799
835
  )
800
836
 
837
+ Data can be inserted into a sparse vector using the :class:`_oracle.SparseVector`
838
+ class, creating an object consisting of the number of dimensions, an array of indices, and a
839
+ corresponding array of values::
840
+
841
+ from sqlalchemy import insert, select
842
+ from sqlalchemy.dialects.oracle import SparseVector
843
+
844
+ sparse_val = SparseVector(10, [1, 2], array.array("d", [23.45, 221.22]))
845
+
846
+ with engine.begin() as conn:
847
+ conn.execute(
848
+ insert(t1),
849
+ {"id": 1, "embedding": sparse_val},
850
+ )
851
+
801
852
  VECTOR Indexes
802
853
  ~~~~~~~~~~~~~~
803
854
 
@@ -805,6 +856,8 @@ The VECTOR feature supports an Oracle-specific parameter ``oracle_vector``
805
856
  on the :class:`.Index` construct, which allows the construction of VECTOR
806
857
  indexes.
807
858
 
859
+ SPARSE vectors cannot be used in the creation of vector indexes.
860
+
808
861
  To utilize VECTOR indexing, set the ``oracle_vector`` parameter to True to use
809
862
  the default values provided by Oracle. HNSW is the default indexing method::
810
863
 
@@ -1157,14 +1210,16 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler):
1157
1210
  return "ROWID"
1158
1211
 
1159
1212
  def visit_VECTOR(self, type_, **kw):
1160
- if type_.dim is None and type_.storage_format is None:
1161
- return "VECTOR(*,*)"
1162
- elif type_.storage_format is None:
1163
- return f"VECTOR({type_.dim},*)"
1164
- elif type_.dim is None:
1165
- return f"VECTOR(*,{type_.storage_format.value})"
1166
- else:
1167
- return f"VECTOR({type_.dim},{type_.storage_format.value})"
1213
+ dim = type_.dim if type_.dim is not None else "*"
1214
+ storage_format = (
1215
+ type_.storage_format.value
1216
+ if type_.storage_format is not None
1217
+ else "*"
1218
+ )
1219
+ storage_type = (
1220
+ type_.storage_type.value if type_.storage_type is not None else "*"
1221
+ )
1222
+ return f"VECTOR({dim},{storage_format},{storage_type})"
1168
1223
 
1169
1224
 
1170
1225
  class OracleCompiler(compiler.SQLCompiler):
@@ -1234,6 +1234,9 @@ class OracleDialect_cx_oracle(OracleDialect):
1234
1234
  with dbapi_connection.cursor() as cursor:
1235
1235
  cursor.execute(f"ALTER SESSION SET ISOLATION_LEVEL={level}")
1236
1236
 
1237
+ def detect_autocommit_setting(self, dbapi_conn) -> bool:
1238
+ return bool(dbapi_conn.autocommit)
1239
+
1237
1240
  def _detect_decimal_char(self, connection):
1238
1241
  # we have the option to change this setting upon connect,
1239
1242
  # or just look at what it is upon connect and convert.
@@ -736,6 +736,8 @@ class OracleDialect_oracledb(_cx_oracle.OracleDialect_cx_oracle):
736
736
 
737
737
  class AsyncAdapt_oracledb_cursor(AsyncAdapt_dbapi_cursor):
738
738
  _cursor: AsyncCursor
739
+ _awaitable_cursor_close: bool = False
740
+
739
741
  __slots__ = ()
740
742
 
741
743
  @property
@@ -749,10 +751,6 @@ class AsyncAdapt_oracledb_cursor(AsyncAdapt_dbapi_cursor):
749
751
  def var(self, *args, **kwargs):
750
752
  return self._cursor.var(*args, **kwargs)
751
753
 
752
- def close(self):
753
- self._rows.clear()
754
- self._cursor.close()
755
-
756
754
  def setinputsizes(self, *args: Any, **kwargs: Any) -> Any:
757
755
  return self._cursor.setinputsizes(*args, **kwargs)
758
756