sqlalchemy-cubrid 0.7.1__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.
@@ -0,0 +1,318 @@
1
+ # sqlalchemy_cubrid/dml.py
2
+ # Copyright (C) 2021-2026 by sqlalchemy-cubrid authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of sqlalchemy-cubrid and is released under
6
+ # the MIT License: http://www.opensource.org/licenses/mit-license.php
7
+
8
+ """CUBRID DML constructs."""
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
13
+
14
+ from sqlalchemy import exc, util
15
+ from sqlalchemy.sql._typing import _DMLTableArgument
16
+ from sqlalchemy.sql.base import (
17
+ _exclusive_against,
18
+ _generative,
19
+ ColumnCollection,
20
+ Generative,
21
+ ReadOnlyColumnCollection,
22
+ )
23
+ from sqlalchemy.sql.dml import Insert as StandardInsert
24
+ from sqlalchemy.sql.elements import ClauseElement, KeyedColumnElement
25
+ from sqlalchemy.sql.expression import Executable, alias
26
+ from sqlalchemy.sql.selectable import NamedFromClause
27
+ from sqlalchemy.util.typing import Self
28
+
29
+ __all__ = ("Insert", "Merge", "Replace", "insert", "merge", "replace")
30
+
31
+ _UpdateArg = Union[
32
+ Mapping[str, Any],
33
+ List[Tuple[str, Any]],
34
+ ]
35
+
36
+
37
+ def insert(table: _DMLTableArgument) -> Insert:
38
+ """Construct a CUBRID-specific variant :class:`Insert` construct.
39
+
40
+ Includes :meth:`Insert.on_duplicate_key_update`.
41
+ """
42
+ return Insert(table)
43
+
44
+
45
+ class Insert(StandardInsert):
46
+ """CUBRID-specific implementation of INSERT.
47
+
48
+ Adds methods for CUBRID-specific syntaxes such as ON DUPLICATE KEY UPDATE.
49
+ """
50
+
51
+ stringify_dialect = "cubrid"
52
+ inherit_cache = True
53
+
54
+ @property
55
+ def inserted(self) -> ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]:
56
+ """Provide the "inserted" namespace for an ON DUPLICATE KEY UPDATE statement.
57
+
58
+ CUBRID uses VALUES() to reference the row being inserted,
59
+ identical to MySQL's pre-8.0 syntax.
60
+ """
61
+ return self.inserted_alias.columns
62
+
63
+ @util.memoized_property
64
+ def inserted_alias(self) -> NamedFromClause:
65
+ return alias(self.table, name="inserted")
66
+
67
+ @_generative
68
+ @_exclusive_against(
69
+ "_post_values_clause",
70
+ msgs={
71
+ "_post_values_clause": "This Insert construct already "
72
+ "has an ON DUPLICATE KEY clause present"
73
+ },
74
+ )
75
+ def on_duplicate_key_update(self, *args: _UpdateArg, **kw: Any) -> Self:
76
+ """Specifies the ON DUPLICATE KEY UPDATE clause.
77
+
78
+ :param **kw: Column keys linked to UPDATE values.
79
+ :param *args: A dictionary or list of 2-tuples as a single positional argument.
80
+ """
81
+ arg_values = list(args)
82
+ values = kw
83
+
84
+ if arg_values and kw:
85
+ raise exc.ArgumentError("Can't pass kwargs and positional arguments simultaneously")
86
+ if arg_values:
87
+ if len(arg_values) > 1:
88
+ raise exc.ArgumentError(
89
+ "Only a single dictionary or list of tuples is accepted positionally."
90
+ )
91
+ values = next(iter(arg_values), kw)
92
+
93
+ self._post_values_clause = OnDuplicateClause(self.inserted_alias, values)
94
+ return self
95
+
96
+
97
+ def replace(table: _DMLTableArgument) -> Replace:
98
+ """Construct a CUBRID REPLACE INTO statement.
99
+
100
+ CUBRID's REPLACE works like MySQL's REPLACE - it inserts a new row,
101
+ or if a duplicate key violation occurs, deletes the old row and inserts
102
+ the new one.
103
+
104
+ Usage::
105
+
106
+ from sqlalchemy_cubrid import replace
107
+
108
+ stmt = replace(users).values(id=1, name="updated")
109
+ """
110
+ return Replace(table)
111
+
112
+
113
+ class Replace(StandardInsert):
114
+ """CUBRID-specific REPLACE INTO statement.
115
+
116
+ Extends StandardInsert to generate REPLACE INTO instead of INSERT INTO.
117
+ Supports all standard INSERT syntax (values, from_select, etc.)
118
+ but does NOT support ON DUPLICATE KEY UPDATE (use Insert for that).
119
+ """
120
+
121
+ stringify_dialect = "cubrid"
122
+ inherit_cache = True
123
+ __visit_name__ = "replace"
124
+
125
+ @property
126
+ def _effective_plugin_target(self) -> str:
127
+ return "insert"
128
+
129
+
130
+ class OnDuplicateClause(ClauseElement):
131
+ __visit_name__ = "on_duplicate_key_update"
132
+
133
+ _parameter_ordering: Optional[List[str]] = None
134
+ update: Dict[str, Any]
135
+ stringify_dialect = "cubrid"
136
+
137
+ def __init__(self, inserted_alias: NamedFromClause, update: _UpdateArg) -> None:
138
+ self.inserted_alias = inserted_alias
139
+ if isinstance(update, list) and (update and isinstance(update[0], tuple)):
140
+ self._parameter_ordering = [key for key, _ in update]
141
+ update = dict(update)
142
+ if isinstance(update, dict):
143
+ if not update:
144
+ raise ValueError("update parameter dictionary must not be empty")
145
+ elif isinstance(update, ColumnCollection):
146
+ update = dict(update)
147
+ else:
148
+ raise ValueError(
149
+ "update parameter must be a non-empty dictionary "
150
+ "or a ColumnCollection such as the `.c.` collection "
151
+ "of a Table object"
152
+ )
153
+ self.update = update
154
+
155
+
156
+ def merge(target: _DMLTableArgument) -> Merge:
157
+ """Construct a CUBRID-specific MERGE statement.
158
+
159
+ CUBRID's MERGE matches rows from a source against a target table
160
+ using an ON condition, then executes UPDATE for matched rows and/or
161
+ INSERT for unmatched rows.
162
+
163
+ Usage::
164
+
165
+ from sqlalchemy_cubrid import merge
166
+
167
+ stmt = (
168
+ merge(target_table)
169
+ .using(source_table)
170
+ .on(target_table.c.id == source_table.c.id)
171
+ .when_matched_then_update({"name": source_table.c.name})
172
+ .when_not_matched_then_insert({"id": source_table.c.id, "name": source_table.c.name})
173
+ )
174
+ """
175
+ return Merge(target)
176
+
177
+
178
+ class Merge(Executable, ClauseElement, Generative):
179
+ __visit_name__ = "merge"
180
+ stringify_dialect = "cubrid"
181
+
182
+ _target: _DMLTableArgument
183
+ _using_source: Optional[Any]
184
+ _on_condition: Optional[ClauseElement]
185
+ _when_matched: Optional[Dict[str, Any]]
186
+ _when_not_matched: Optional[Dict[str, Any]]
187
+
188
+ def __init__(self, target: _DMLTableArgument) -> None:
189
+ self._target = target
190
+ self._using_source = None
191
+ self._on_condition = None
192
+ self._when_matched = None
193
+ self._when_not_matched = None
194
+
195
+ @_generative
196
+ def into(self, target_table: _DMLTableArgument) -> Self:
197
+ self._target = target_table
198
+ return self
199
+
200
+ @_generative
201
+ def using(self, source: Any) -> Self:
202
+ self._using_source = source
203
+ return self
204
+
205
+ @_generative
206
+ def on(self, condition: ClauseElement) -> Self:
207
+ self._on_condition = condition
208
+ return self
209
+
210
+ @_generative
211
+ def when_matched_then_update(
212
+ self,
213
+ values_dict: Union[Mapping[Any, Any], List[Tuple[Any, Any]], Tuple[Tuple[Any, Any], ...]],
214
+ where: Optional[ClauseElement] = None,
215
+ delete_where: Optional[ClauseElement] = None,
216
+ ) -> Self:
217
+ values = self._normalize_key_value_pairs(values_dict, argument_name="values_dict")
218
+ existing_delete_where = (
219
+ self._when_matched.get("delete_where") if self._when_matched is not None else None
220
+ )
221
+ self._when_matched = {
222
+ "values": dict(values),
223
+ "where": where,
224
+ "delete_where": (delete_where if delete_where is not None else existing_delete_where),
225
+ }
226
+ return self
227
+
228
+ @_generative
229
+ def when_matched_then_delete(self, where: Optional[ClauseElement] = None) -> Self:
230
+ if self._when_matched is None:
231
+ raise ValueError("WHEN MATCHED UPDATE clause must be specified before DELETE WHERE")
232
+
233
+ self._when_matched = {
234
+ "values": dict(self._when_matched["values"]),
235
+ "where": self._when_matched.get("where"),
236
+ "delete_where": where,
237
+ }
238
+ return self
239
+
240
+ @_generative
241
+ def when_not_matched_then_insert(
242
+ self,
243
+ values_dict_or_column_list: Union[
244
+ Mapping[Any, Any],
245
+ List[Tuple[Any, Any]],
246
+ Tuple[Tuple[Any, Any], ...],
247
+ List[Any],
248
+ Tuple[Any, ...],
249
+ ],
250
+ where: Optional[ClauseElement] = None,
251
+ ) -> Self:
252
+ columns: List[Any]
253
+ values: List[Any]
254
+
255
+ if isinstance(values_dict_or_column_list, Mapping):
256
+ pairs = self._normalize_key_value_pairs(
257
+ values_dict_or_column_list,
258
+ argument_name="values_dict_or_column_list",
259
+ )
260
+ columns = [column for column, _ in pairs]
261
+ values = [value for _, value in pairs]
262
+ elif isinstance(values_dict_or_column_list, ColumnCollection):
263
+ pairs = self._normalize_key_value_pairs(
264
+ dict(values_dict_or_column_list),
265
+ argument_name="values_dict_or_column_list",
266
+ )
267
+ columns = [column for column, _ in pairs]
268
+ values = [value for _, value in pairs]
269
+ elif isinstance(values_dict_or_column_list, (list, tuple)):
270
+ if values_dict_or_column_list and isinstance(values_dict_or_column_list[0], tuple):
271
+ pairs = self._normalize_key_value_pairs(
272
+ list(values_dict_or_column_list),
273
+ argument_name="values_dict_or_column_list",
274
+ )
275
+ columns = [column for column, _ in pairs]
276
+ values = [value for _, value in pairs]
277
+ else:
278
+ columns = list(values_dict_or_column_list)
279
+ if not columns:
280
+ raise ValueError(
281
+ "values_dict_or_column_list must be a non-empty dictionary, "
282
+ "a list of tuples, or a non-empty column list"
283
+ )
284
+ values = list(columns)
285
+ else:
286
+ raise ValueError(
287
+ "values_dict_or_column_list must be a non-empty dictionary, "
288
+ "a list of tuples, or a non-empty column list"
289
+ )
290
+
291
+ self._when_not_matched = {
292
+ "columns": columns,
293
+ "values": values,
294
+ "where": where,
295
+ }
296
+ return self
297
+
298
+ def _normalize_key_value_pairs(
299
+ self,
300
+ values: Union[Mapping[Any, Any], List[Tuple[Any, Any]], Tuple[Tuple[Any, Any], ...]],
301
+ argument_name: str,
302
+ ) -> List[Tuple[Any, Any]]:
303
+ if isinstance(values, Mapping):
304
+ pairs = list(values.items())
305
+ elif isinstance(values, list):
306
+ pairs = list(values)
307
+ elif isinstance(values, tuple):
308
+ pairs = list(values)
309
+ else:
310
+ raise ValueError(f"{argument_name} must be a non-empty dictionary or a list of tuples")
311
+
312
+ if not pairs:
313
+ raise ValueError(f"{argument_name} must be a non-empty dictionary or a list of tuples")
314
+
315
+ if not all(isinstance(pair, tuple) and len(pair) == 2 for pair in pairs):
316
+ raise ValueError(f"{argument_name} must be a non-empty dictionary or a list of tuples")
317
+
318
+ return pairs
File without changes
@@ -0,0 +1,124 @@
1
+ # sqlalchemy_cubrid/pycubrid_dialect.py
2
+ # Copyright (C) 2021-2026 by sqlalchemy-cubrid authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of sqlalchemy-cubrid and is released under
6
+ # the MIT License: http://www.opensource.org/licenses/mit-license.php
7
+
8
+ """CUBRID dialect variant using the pycubrid pure-Python DB-API 2.0 driver."""
9
+
10
+ from __future__ import annotations
11
+
12
+
13
+ from sqlalchemy_cubrid.base import CubridExecutionContext
14
+ from sqlalchemy_cubrid.dialect import CubridDialect
15
+
16
+
17
+ class PyCubridExecutionContext(CubridExecutionContext):
18
+ """Execution context for pycubrid connections.
19
+
20
+ pycubrid exposes ``cursor.lastrowid`` as a proper ``int | None``,
21
+ so we use it directly instead of the CUBRIDdb workaround.
22
+ """
23
+
24
+ def get_lastrowid(self):
25
+ """Return the last inserted row ID from pycubrid's cursor."""
26
+ try:
27
+ return self.cursor.lastrowid
28
+ except AttributeError:
29
+ pass
30
+
31
+ # Fallback: use SQL function
32
+ cursor = self.create_server_side_cursor()
33
+ try:
34
+ cursor.execute("SELECT LAST_INSERT_ID()")
35
+ row = cursor.fetchone()
36
+ if row:
37
+ return int(row[0])
38
+ finally:
39
+ cursor.close()
40
+ return None
41
+
42
+
43
+ class PyCubridDialect(CubridDialect):
44
+ """SQLAlchemy dialect for CUBRID using the pycubrid pure-Python driver.
45
+
46
+ Connection URL: ``cubrid+pycubrid://user:password@host:port/dbname``
47
+
48
+ This dialect subclasses :class:`CubridDialect` and overrides only
49
+ the driver-specific methods: ``import_dbapi``, ``create_connect_args``,
50
+ ``on_connect``, and ``do_ping``. All SQL compilation, type mapping,
51
+ and schema reflection is inherited unchanged.
52
+ """
53
+
54
+ driver = "pycubrid"
55
+ supports_statement_cache = True
56
+ execution_ctx_cls = PyCubridExecutionContext
57
+
58
+ # pycubrid uses qmark paramstyle natively
59
+ default_paramstyle = "qmark"
60
+
61
+ @classmethod
62
+ def import_dbapi(cls):
63
+ """Import and return the pycubrid DBAPI module."""
64
+ try:
65
+ import pycubrid as dbapi_module
66
+ except ImportError as e:
67
+ raise e
68
+ return dbapi_module
69
+
70
+ # Keep legacy dbapi() for SA 1.x compat
71
+ @classmethod
72
+ def dbapi(cls):
73
+ return cls.import_dbapi()
74
+
75
+ def create_connect_args(self, url):
76
+ """Build DB-API connection arguments for pycubrid.
77
+
78
+ pycubrid accepts keyword arguments directly::
79
+
80
+ pycubrid.connect(host=..., port=..., database=..., user=..., password=...)
81
+ """
82
+ if url is None:
83
+ raise ValueError("Unexpected database URL format")
84
+
85
+ opts = url.translate_connect_args(username="user", database="database")
86
+ return (), {
87
+ "host": opts.get("host", "localhost"),
88
+ "port": opts.get("port", 33000),
89
+ "database": opts.get("database", ""),
90
+ "user": opts.get("user", "dba"),
91
+ "password": opts.get("password", ""),
92
+ }
93
+
94
+ def on_connect(self):
95
+ """Return a callable to set up a new pycubrid connection.
96
+
97
+ Disables autocommit so that SQLAlchemy manages transactions.
98
+ """
99
+ isolation_level = self.isolation_level
100
+
101
+ def connect(conn):
102
+ # pycubrid uses a property setter for autocommit
103
+ conn.autocommit = False
104
+ if isolation_level is not None:
105
+ self.set_isolation_level(conn, isolation_level)
106
+
107
+ return connect
108
+
109
+ def do_ping(self, dbapi_connection):
110
+ """Ping the server to check connection liveness.
111
+
112
+ pycubrid does not expose a native ``ping()`` method, so we
113
+ execute a lightweight query instead.
114
+ """
115
+ cursor = dbapi_connection.cursor()
116
+ try:
117
+ cursor.execute("SELECT 1")
118
+ cursor.fetchone()
119
+ finally:
120
+ cursor.close()
121
+ return True
122
+
123
+
124
+ dialect = PyCubridDialect
@@ -0,0 +1,270 @@
1
+ # sqlalchemy_cubrid/requirements.py
2
+ # Copyright (C) 2021-2026 by sqlalchemy-cubrid authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of sqlalchemy-cubrid and is released under
6
+ # the MIT License: http://www.opensource.org/licenses/mit-license.php
7
+
8
+ """CUBRID test-suite requirement flags.
9
+
10
+ These tell SQLAlchemy's built-in test suite which features this dialect
11
+ supports so tests are automatically skipped when the feature is absent.
12
+
13
+ Reference: https://github.com/sqlalchemy/sqlalchemy/blob/main/README.dialects.rst
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from sqlalchemy.testing import exclusions
19
+ from sqlalchemy.testing.requirements import SuiteRequirements
20
+
21
+
22
+ class Requirements(SuiteRequirements):
23
+ """CUBRID-specific requirement flags for the SA test suite."""
24
+
25
+ # ----- RETURNING -----
26
+
27
+ @property
28
+ def returning(self):
29
+ """CUBRID does not support INSERT/UPDATE/DELETE … RETURNING."""
30
+ return exclusions.closed()
31
+
32
+ @property
33
+ def insert_returning(self):
34
+ return exclusions.closed()
35
+
36
+ @property
37
+ def update_returning(self):
38
+ return exclusions.closed()
39
+
40
+ @property
41
+ def delete_returning(self):
42
+ return exclusions.closed()
43
+
44
+ # ----- Booleans -----
45
+
46
+ @property
47
+ def nullable_booleans(self):
48
+ """CUBRID maps BOOLEAN to SMALLINT which is nullable."""
49
+ return exclusions.open()
50
+
51
+ @property
52
+ def non_native_boolean_unconstrained(self):
53
+ """The SMALLINT emulation has no CHECK constraint."""
54
+ return exclusions.open()
55
+
56
+ # ----- Sequences -----
57
+
58
+ @property
59
+ def sequences(self):
60
+ """CUBRID does not support sequences."""
61
+ return exclusions.closed()
62
+
63
+ @property
64
+ def sequences_optional(self):
65
+ return exclusions.closed()
66
+
67
+ # ----- Schema / DDL -----
68
+
69
+ @property
70
+ def schemas(self):
71
+ """CUBRID does not support multiple schemas."""
72
+ return exclusions.closed()
73
+
74
+ @property
75
+ def temp_table_names(self):
76
+ return exclusions.closed()
77
+
78
+ @property
79
+ def temporary_tables(self):
80
+ return exclusions.closed()
81
+
82
+ @property
83
+ def temporary_views(self):
84
+ return exclusions.closed()
85
+
86
+ @property
87
+ def table_ddl_if_exists(self):
88
+ """CUBRID supports IF NOT EXISTS / IF EXISTS in DDL."""
89
+ return exclusions.open()
90
+
91
+ @property
92
+ def comment_reflection(self):
93
+ """CUBRID supports table and column comments."""
94
+ return exclusions.open()
95
+
96
+ @property
97
+ def check_constraint_reflection(self):
98
+ return exclusions.closed()
99
+
100
+ # ----- DML -----
101
+
102
+ @property
103
+ def empty_inserts(self):
104
+ """CUBRID supports INSERT INTO t DEFAULT VALUES."""
105
+ return exclusions.open()
106
+
107
+ @property
108
+ def insert_from_select(self):
109
+ return exclusions.open()
110
+
111
+ @property
112
+ def ctes(self):
113
+ """CUBRID 11 supports CTEs."""
114
+ return exclusions.open()
115
+
116
+ @property
117
+ def ctes_on_dml(self):
118
+ return exclusions.closed()
119
+
120
+ # ----- SELECT features -----
121
+
122
+ @property
123
+ def window_functions(self):
124
+ """CUBRID supports window functions (ROW_NUMBER, RANK, etc.) with OVER()."""
125
+ return exclusions.open()
126
+
127
+ @property
128
+ def intersect(self):
129
+ return exclusions.open()
130
+
131
+ @property
132
+ def except_(self):
133
+ return exclusions.open()
134
+
135
+ @property
136
+ def fetch_no_order(self):
137
+ return exclusions.open()
138
+
139
+ @property
140
+ def order_by_col_from_union(self):
141
+ return exclusions.open()
142
+
143
+ # ----- Type support -----
144
+
145
+ @property
146
+ def unicode_ddl(self):
147
+ return exclusions.open()
148
+
149
+ @property
150
+ def datetime_literals(self):
151
+ return exclusions.closed()
152
+
153
+ @property
154
+ def date(self):
155
+ return exclusions.open()
156
+
157
+ @property
158
+ def time(self):
159
+ return exclusions.open()
160
+
161
+ @property
162
+ def datetime(self):
163
+ return exclusions.open()
164
+
165
+ @property
166
+ def timestamp(self):
167
+ return exclusions.open()
168
+
169
+ @property
170
+ def text_type(self):
171
+ return exclusions.open()
172
+
173
+ @property
174
+ def json_type(self):
175
+ """CUBRID does not support JSON type."""
176
+ return exclusions.closed()
177
+
178
+ @property
179
+ def array_type(self):
180
+ return exclusions.closed()
181
+
182
+ @property
183
+ def uuid_data_type(self):
184
+ return exclusions.closed()
185
+
186
+ # ----- Misc -----
187
+
188
+ @property
189
+ def views(self):
190
+ return exclusions.open()
191
+
192
+ @property
193
+ def savepoints(self):
194
+ return exclusions.open()
195
+
196
+ @property
197
+ def foreign_keys(self):
198
+ return exclusions.open()
199
+
200
+ @property
201
+ def self_referential_foreign_keys(self):
202
+ return exclusions.open()
203
+
204
+ @property
205
+ def unique_constraint_reflection(self):
206
+ return exclusions.open()
207
+
208
+ @property
209
+ def foreign_key_constraint_reflection(self):
210
+ return exclusions.open()
211
+
212
+ @property
213
+ def index_reflection(self):
214
+ return exclusions.open()
215
+
216
+ @property
217
+ def primary_key_constraint_reflection(self):
218
+ return exclusions.open()
219
+
220
+ @property
221
+ def on_update_cascade(self):
222
+ return exclusions.open()
223
+
224
+ @property
225
+ def on_delete_cascade(self):
226
+ return exclusions.open()
227
+
228
+ @property
229
+ def server_side_cursors(self):
230
+ return exclusions.closed()
231
+
232
+ @property
233
+ def independent_connections(self):
234
+ return exclusions.open()
235
+
236
+ # ----- Binary / LOB -----
237
+
238
+ @property
239
+ def binary_comparisons(self):
240
+ """CUBRID BLOB roundtrip has driver-level issues."""
241
+ return exclusions.closed()
242
+
243
+ @property
244
+ def binary_literals(self):
245
+ """CUBRID does not support binary literal syntax."""
246
+ return exclusions.closed()
247
+
248
+ # ----- Identifier quoting -----
249
+
250
+ @property
251
+ def unusual_column_name_characters(self):
252
+ """CUBRID has limited support for special characters in identifiers."""
253
+ return exclusions.closed()
254
+
255
+ @property
256
+ def implicitly_named_constraints(self):
257
+ """CUBRID FK reflection has issues with special-character table names."""
258
+ return exclusions.closed()
259
+
260
+ # ----- SELECT FOR UPDATE -----
261
+
262
+ @property
263
+ def update_nowait(self):
264
+ """CUBRID does not support SELECT ... FOR UPDATE NOWAIT."""
265
+ return exclusions.closed()
266
+
267
+ @property
268
+ def for_update(self):
269
+ """CUBRID supports SELECT ... FOR UPDATE [OF col1, col2]."""
270
+ return exclusions.open()