ydb-sqlalchemy 0.1.15__py2.py3-none-any.whl → 0.1.17__py2.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.
test/test_core.py CHANGED
@@ -181,12 +181,19 @@ class TestSimpleSelect(TablesTest):
181
181
  rows = connection.execute(stm).fetchall()
182
182
  assert set(rows) == {(1,), (2,), (3,), (4,), (6,), (7,)}
183
183
 
184
+ # LIMIT
185
+ rows = connection.execute(tb.select().order_by(tb.c.id).limit(2)).fetchall()
186
+ assert rows == [
187
+ (1, "some text", Decimal("3.141592653")),
188
+ (2, "test text", Decimal("3.14159265")),
189
+ ]
190
+
184
191
  # LIMIT/OFFSET
185
- # rows = connection.execute(tb.select().order_by(tb.c.id).limit(2)).fetchall()
186
- # assert rows == [
187
- # (1, "some text", Decimal("3.141592653")),
188
- # (2, "test text", Decimal("3.14159265")),
189
- # ]
192
+ rows = connection.execute(tb.select().order_by(tb.c.id).limit(2).offset(1)).fetchall()
193
+ assert rows == [
194
+ (2, "test text", Decimal("3.14159265")),
195
+ (3, "test test", Decimal("3.1415926")),
196
+ ]
190
197
 
191
198
  # ORDER BY ASC
192
199
  rows = connection.execute(sa.select(tb.c.id).order_by(tb.c.id)).fetchall()
@@ -223,7 +230,7 @@ class TestTypes(TablesTest):
223
230
  "test_primitive_types",
224
231
  metadata,
225
232
  Column("int", sa.Integer, primary_key=True),
226
- # Column("bin", sa.BINARY),
233
+ Column("bin", sa.BINARY),
227
234
  Column("str", sa.String),
228
235
  Column("float", sa.Float),
229
236
  Column("bool", sa.Boolean),
@@ -253,7 +260,7 @@ class TestTypes(TablesTest):
253
260
 
254
261
  statement = sa.insert(table).values(
255
262
  int=42,
256
- # bin=b"abc",
263
+ bin=b"abc",
257
264
  str="Hello World!",
258
265
  float=3.5,
259
266
  bool=True,
@@ -262,7 +269,7 @@ class TestTypes(TablesTest):
262
269
  connection.execute(statement)
263
270
 
264
271
  row = connection.execute(sa.select(table)).fetchone()
265
- assert row == (42, "Hello World!", 3.5, True)
272
+ assert row == (42, b"abc", "Hello World!", 3.5, True)
266
273
 
267
274
  def test_all_binary_types(self, connection):
268
275
  table = self.tables.test_all_binary_types
@@ -1150,8 +1157,15 @@ class TestAsTable(TablesTest):
1150
1157
  Column("val_int", Integer, nullable=True),
1151
1158
  Column("val_str", String, nullable=True),
1152
1159
  )
1160
+ Table(
1161
+ "test_as_table_json",
1162
+ metadata,
1163
+ Column("id", Integer, primary_key=True),
1164
+ Column("data", sa.JSON, nullable=True),
1165
+ )
1153
1166
 
1154
- def test_upsert_as_table(self, connection):
1167
+ @pytest.mark.parametrize("list_cls", [types.ListType, sa.ARRAY])
1168
+ def test_upsert_as_table(self, connection, list_cls):
1155
1169
  table = self.tables.test_as_table
1156
1170
 
1157
1171
  input_data = [
@@ -1167,7 +1181,7 @@ class TestAsTable(TablesTest):
1167
1181
  "val_str": types.Optional(String),
1168
1182
  }
1169
1183
  )
1170
- list_type = types.ListType(struct_type)
1184
+ list_type = list_cls(struct_type)
1171
1185
 
1172
1186
  bind_param = sa.bindparam("data", type_=list_type)
1173
1187
 
@@ -1187,7 +1201,39 @@ class TestAsTable(TablesTest):
1187
1201
  (3, 30, None),
1188
1202
  ]
1189
1203
 
1190
- def test_insert_as_table(self, connection):
1204
+ @pytest.mark.parametrize("list_cls", [types.ListType, sa.ARRAY])
1205
+ def test_upsert_from_table_json(self, connection, list_cls):
1206
+ table = self.tables.test_as_table_json
1207
+
1208
+ input_data = [
1209
+ {"id": 1, "data": {"a": 1}},
1210
+ {"id": 2, "data": [1, 2, 3]},
1211
+ {"id": 3, "data": None},
1212
+ ]
1213
+
1214
+ struct_type = types.StructType.from_table(table)
1215
+ list_type = list_cls(struct_type)
1216
+
1217
+ bind_param = sa.bindparam("input_data", type_=list_type)
1218
+
1219
+ cols = [sa.column(c.name, type_=c.type) for c in table.columns]
1220
+ upsert_stm = ydb_sa.upsert(table).from_select(
1221
+ [c.name for c in table.columns],
1222
+ sa.select(*cols).select_from(sa.func.AS_TABLE(bind_param)),
1223
+ )
1224
+
1225
+ connection.execute(upsert_stm, {"input_data": input_data})
1226
+
1227
+ rows = connection.execute(sa.select(table).order_by(table.c.id)).fetchall()
1228
+
1229
+ assert rows == [
1230
+ (1, {"a": 1}),
1231
+ (2, [1, 2, 3]),
1232
+ (3, None),
1233
+ ]
1234
+
1235
+ @pytest.mark.parametrize("list_cls", [types.ListType, sa.ARRAY])
1236
+ def test_insert_as_table(self, connection, list_cls):
1191
1237
  table = self.tables.test_as_table
1192
1238
 
1193
1239
  input_data = [
@@ -1202,7 +1248,7 @@ class TestAsTable(TablesTest):
1202
1248
  "val_str": types.Optional(String),
1203
1249
  }
1204
1250
  )
1205
- list_type = types.ListType(struct_type)
1251
+ list_type = list_cls(struct_type)
1206
1252
 
1207
1253
  bind_param = sa.bindparam("data", type_=list_type)
1208
1254
 
@@ -1221,7 +1267,8 @@ class TestAsTable(TablesTest):
1221
1267
  (5, None, "e"),
1222
1268
  ]
1223
1269
 
1224
- def test_upsert_from_table_reflection(self, connection):
1270
+ @pytest.mark.parametrize("list_cls", [types.ListType, sa.ARRAY])
1271
+ def test_upsert_from_table_reflection(self, connection, list_cls):
1225
1272
  table = self.tables.test_as_table
1226
1273
 
1227
1274
  input_data = [
@@ -1230,7 +1277,7 @@ class TestAsTable(TablesTest):
1230
1277
  ]
1231
1278
 
1232
1279
  struct_type = types.StructType.from_table(table)
1233
- list_type = types.ListType(struct_type)
1280
+ list_type = list_cls(struct_type)
1234
1281
 
1235
1282
  bind_param = sa.bindparam("data", type_=list_type)
1236
1283
 
@@ -1 +1 @@
1
- VERSION = "0.1.15"
1
+ VERSION = "0.1.17"
@@ -160,6 +160,7 @@ class YqlDialect(StrCompileDialect):
160
160
  sa.types.BINARY: types.Binary,
161
161
  sa.types.LargeBinary: types.Binary,
162
162
  sa.types.BLOB: types.Binary,
163
+ sa.types.ARRAY: types.ListType,
163
164
  }
164
165
 
165
166
  connection_characteristics = util.immutabledict(
@@ -205,6 +206,7 @@ class YqlDialect(StrCompileDialect):
205
206
  json_serializer=None,
206
207
  json_deserializer=None,
207
208
  _add_declare_for_yql_stmt_vars=False,
209
+ _statement_prefixes_list=None,
208
210
  **kwargs,
209
211
  ):
210
212
  super().__init__(**kwargs)
@@ -214,6 +216,7 @@ class YqlDialect(StrCompileDialect):
214
216
  # NOTE: _add_declare_for_yql_stmt_vars is temporary and is soon to be removed.
215
217
  # no need in declare in yql statement here since ydb 24-1
216
218
  self._add_declare_for_yql_stmt_vars = _add_declare_for_yql_stmt_vars
219
+ self._statement_prefixes = tuple(_statement_prefixes_list) if _statement_prefixes_list else ()
217
220
 
218
221
  def _describe_table(self, connection, table_name, schema=None) -> ydb.TableDescription:
219
222
  if schema is not None:
@@ -404,6 +407,12 @@ class YqlDialect(StrCompileDialect):
404
407
  )
405
408
  return f"{declarations}\n{statement}"
406
409
 
410
+ def _apply_statement_prefixes_impl(self, statement: str) -> str:
411
+ if not self._statement_prefixes:
412
+ return statement
413
+ prefixes = "\n".join(self._statement_prefixes) + "\n"
414
+ return f"{prefixes}{statement}"
415
+
407
416
  def __merge_parameters_values_and_types(
408
417
  self, values: Mapping[str, Any], types: Mapping[str, Any], execute_many: bool
409
418
  ) -> Sequence[Mapping[str, ydb.TypedValue]]:
@@ -437,9 +446,12 @@ class YqlDialect(StrCompileDialect):
437
446
  statement, parameters = self._format_variables(statement, parameters, execute_many)
438
447
  if self._add_declare_for_yql_stmt_vars:
439
448
  statement = self._add_declare_for_yql_stmt_vars_impl(statement, parameters_types)
449
+ statement = self._apply_statement_prefixes_impl(statement)
440
450
  return statement, parameters
441
451
 
442
452
  statement, parameters = self._format_variables(statement, parameters, execute_many)
453
+ if not is_ddl:
454
+ statement = self._apply_statement_prefixes_impl(statement)
443
455
  return statement, parameters
444
456
 
445
457
  def do_ping(self, dbapi_connection: ydb_dbapi.Connection) -> bool:
@@ -44,19 +44,6 @@ else:
44
44
  from sqlalchemy import Cast as _cast
45
45
 
46
46
 
47
- STR_QUOTE_MAP = {
48
- "'": "\\'",
49
- "\\": "\\\\",
50
- "\0": "\\0",
51
- "\b": "\\b",
52
- "\f": "\\f",
53
- "\r": "\\r",
54
- "\n": "\\n",
55
- "\t": "\\t",
56
- "%": "%%",
57
- }
58
-
59
-
60
47
  COMPOUND_KEYWORDS = {
61
48
  selectable.CompoundSelect.UNION: "UNION ALL",
62
49
  selectable.CompoundSelect.UNION_ALL: "UNION ALL",
@@ -67,6 +54,19 @@ COMPOUND_KEYWORDS = {
67
54
  }
68
55
 
69
56
 
57
+ ESCAPE_RULES = [
58
+ ("\\", "\\\\"), # Must be first to avoid double escaping
59
+ ("'", "\\'"),
60
+ ("\0", "\\0"),
61
+ ("\b", "\\b"),
62
+ ("\f", "\\f"),
63
+ ("\r", "\\r"),
64
+ ("\n", "\\n"),
65
+ ("\t", "\\t"),
66
+ ("%", "%%"),
67
+ ]
68
+
69
+
70
70
  class BaseYqlTypeCompiler(StrSQLTypeCompiler):
71
71
  def visit_JSON(self, type_: Union[sa.JSON, types.YqlJSON], **kw):
72
72
  return "JSON"
@@ -293,7 +293,8 @@ class BaseYqlCompiler(StrSQLCompiler):
293
293
 
294
294
  def render_literal_value(self, value, type_):
295
295
  if isinstance(value, str):
296
- value = "".join(STR_QUOTE_MAP.get(x, x) for x in value)
296
+ for pattern, replacement in ESCAPE_RULES:
297
+ value = value.replace(pattern, replacement)
297
298
  return f"'{value}'"
298
299
  return super().render_literal_value(value, type_)
299
300
 
@@ -114,6 +114,30 @@ def test_types_compilation():
114
114
  assert compile_type(struct) == "Struct<a:Int32,b:List<Int32>>"
115
115
 
116
116
 
117
+ def test_statement_prefixes_prepended_to_query():
118
+ dialect = YqlDialect(_statement_prefixes_list=["PRAGMA DistinctOverKeys;"])
119
+ result = dialect._apply_statement_prefixes_impl("SELECT 1")
120
+ assert result == "PRAGMA DistinctOverKeys;\nSELECT 1"
121
+
122
+
123
+ def test_statement_prefixes_empty_list_unchanged():
124
+ dialect = YqlDialect(_statement_prefixes_list=[])
125
+ result = dialect._apply_statement_prefixes_impl("SELECT 1")
126
+ assert result == "SELECT 1"
127
+
128
+
129
+ def test_statement_prefixes_none_unchanged():
130
+ dialect = YqlDialect()
131
+ result = dialect._apply_statement_prefixes_impl("SELECT 1")
132
+ assert result == "SELECT 1"
133
+
134
+
135
+ def test_statement_prefixes_multiple():
136
+ dialect = YqlDialect(_statement_prefixes_list=["PRAGMA Foo;", "PRAGMA Bar;"])
137
+ result = dialect._apply_statement_prefixes_impl("SELECT 1")
138
+ assert result == "PRAGMA Foo;\nPRAGMA Bar;\nSELECT 1"
139
+
140
+
117
141
  def test_optional_type_compilation():
118
142
  dialect = YqlDialect()
119
143
  type_compiler = dialect.type_compiler
@@ -110,6 +110,18 @@ class Decimal(types.DECIMAL):
110
110
  class ListType(ARRAY):
111
111
  __visit_name__ = "list_type"
112
112
 
113
+ def bind_processor(self, dialect):
114
+ item_proc = self.item_type.bind_processor(dialect)
115
+
116
+ def process(value):
117
+ if value is None:
118
+ return None
119
+ return [item_proc(v) if v is not None else None for v in value]
120
+
121
+ if item_proc:
122
+ return process
123
+ return None
124
+
113
125
 
114
126
  class HashableDict(dict):
115
127
  def __hash__(self):
@@ -173,6 +185,32 @@ class StructType(types.TypeEngine[Mapping[str, Any]]):
173
185
  def compare_values(self, x, y):
174
186
  return x == y
175
187
 
188
+ def bind_processor(self, dialect):
189
+ processors = {}
190
+ for name, type_ in self.fields_types.items():
191
+ if isinstance(type_, Optional):
192
+ type_ = type_.element_type
193
+
194
+ type_ = type_api.to_instance(type_)
195
+ proc = type_.bind_processor(dialect)
196
+ if proc:
197
+ processors[name] = proc
198
+
199
+ if not processors:
200
+ return None
201
+
202
+ def process(value):
203
+ if value is None:
204
+ return None
205
+ new_value = value.copy()
206
+ for name, proc in processors.items():
207
+ if name in new_value:
208
+ if new_value[name] is not None:
209
+ new_value[name] = proc(new_value[name])
210
+ return new_value
211
+
212
+ return process
213
+
176
214
 
177
215
  class Lambda(ColumnElement):
178
216
  __visit_name__ = "lambda"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ydb-sqlalchemy
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: YDB Dialect for SQLAlchemy
5
5
  Home-page: http://github.com/ydb-platform/ydb-sqlalchemy
6
6
  Author: Yandex LLC
@@ -1,26 +1,26 @@
1
1
  test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  test/conftest.py,sha256=rhWa0EQB9EwO_wAwxPdK17Qi582DdbBE8p5Gv4180Ds,570
3
- test/test_core.py,sha256=5ijngbM6b_8avees4CqCv0FgzAza2RRPDwgT2saU99k,47254
3
+ test/test_core.py,sha256=cp49GDKgpsxfCBG9EjSwjOlPSchKW68nn8CnMeuxy8k,48929
4
4
  test/test_inspect.py,sha256=c4kc3jc48MCOfllO-ciiYf1vO-HOfuv0xVoXYT1Jxro,1106
5
5
  test/test_orm.py,sha256=jQVVld50zbUwxwgW9ySIWGaNDEOLzHKXjTkdpsG9TpA,1825
6
6
  test/test_suite.py,sha256=JYBGZjaRbg_ZiAqTHeCfL7DLnB6N6xkXN82gnooCyd8,31063
7
7
  ydb_sqlalchemy/__init__.py,sha256=hX7Gy-KOiHk7B5-0wj3ZmLjk4YDJnSMHIAqxVGn_PJY,181
8
- ydb_sqlalchemy/_version.py,sha256=f1tdrTNNxSCrK-kaNyZL5_MXChoOPzxvT7JLn3_KW6k,19
9
- ydb_sqlalchemy/sqlalchemy/__init__.py,sha256=Z5aprCPByfUmH2DvybA0o7W0lQ8YDCR1XSyyfGmBdoE,17911
8
+ ydb_sqlalchemy/_version.py,sha256=CQKgFdKOd8j2eckBUFbwPNJt1oASUfatjq5eO0eF8cA,19
9
+ ydb_sqlalchemy/sqlalchemy/__init__.py,sha256=8_wPXgqlhyIW0CGmG1vIsjuepGyCOl8rAm7E0Hv90DY,18500
10
10
  ydb_sqlalchemy/sqlalchemy/datetime_types.py,sha256=wrI9kpsI_f7Jhbm7Fu0o_S1QoGCLIe6A9jfUwb41aMM,1929
11
11
  ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py,sha256=7FDjganh9QStIkoXYPFfcRRhd07YCX63_8OmMnge1FI,3542
12
12
  ydb_sqlalchemy/sqlalchemy/dml.py,sha256=k_m6PLOAY7dVzG1gsyo2bB3Lp-o3rhzN0oSX_nfkbFU,310
13
13
  ydb_sqlalchemy/sqlalchemy/json.py,sha256=b4ydjlQjBhlhqGP_Sy2uZVKmt__D-9M7-YLGQMdYGME,1043
14
14
  ydb_sqlalchemy/sqlalchemy/requirements.py,sha256=zm6fcLormtk3KHnbtrBvxfkbG9ZyzNan38HrRB6vC3c,2505
15
- ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py,sha256=4wyRHmE8YQaMElQPHX6ToEj7A9F8Mvv909aaz_0wRnA,4535
16
- ydb_sqlalchemy/sqlalchemy/types.py,sha256=_PxK76x6BJZv-7gVdXL-XJtaOFjiYZY4f-_WBRWH8x0,4996
15
+ ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py,sha256=kSQ2sg90X5QoEAKfJSj3CIqI6j95tLUoUW8Fa197Um0,5410
16
+ ydb_sqlalchemy/sqlalchemy/types.py,sha256=lFopPGW8WLPIrhgXEcnSAqzJMyrV74ryLSaeTnaKBzA,6125
17
17
  ydb_sqlalchemy/sqlalchemy/compiler/__init__.py,sha256=QqA6r-_bw1R97nQZy5ZSJN724znXg88l4mi5PpqAOxI,492
18
- ydb_sqlalchemy/sqlalchemy/compiler/base.py,sha256=zKOf4SrcAjMKf1LjRvQA7IzMRic0EIznHuZTHySvc_k,20143
18
+ ydb_sqlalchemy/sqlalchemy/compiler/base.py,sha256=JyDIEk3CvJggidFLSNoeI4Khjk3a-b0SjS80GrjgSIk,20248
19
19
  ydb_sqlalchemy/sqlalchemy/compiler/sa14.py,sha256=LanxAnwOiMnsnrY05B0jpmvGn5NXuOKMcxi_6N3obVM,1186
20
20
  ydb_sqlalchemy/sqlalchemy/compiler/sa20.py,sha256=rvVhe-pq5bOyuW4KMMMAD7JIWMzy355eijymBvuPwKw,3421
21
- ydb_sqlalchemy-0.1.15.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
22
- ydb_sqlalchemy-0.1.15.dist-info/METADATA,sha256=iOoVXzaJ3tJ7ct7jssAXLSUwptb9C4e7Yx3dPp8VzdA,5395
23
- ydb_sqlalchemy-0.1.15.dist-info/WHEEL,sha256=Ll72iyqtt6Rbxp-Q7FSafYA1LeRv98X15xcZWRsFEmY,109
24
- ydb_sqlalchemy-0.1.15.dist-info/entry_points.txt,sha256=iJxbKYuliWNBmL0iIiw8MxvOXrSEz5xe5fuEBqMRwCE,267
25
- ydb_sqlalchemy-0.1.15.dist-info/top_level.txt,sha256=iS69Y1GTAcTok0u0oQdxP-Q5iVgUGI71XBsaEUrWhMg,20
26
- ydb_sqlalchemy-0.1.15.dist-info/RECORD,,
21
+ ydb_sqlalchemy-0.1.17.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
22
+ ydb_sqlalchemy-0.1.17.dist-info/METADATA,sha256=fhxTYNZXhbJs_hoyerJNTPmdqjVXSDbUa84YmsacZts,5395
23
+ ydb_sqlalchemy-0.1.17.dist-info/WHEEL,sha256=I3glN-nznogni2CWkAvi6vPRkBhTbVeYvJZnIsln6uc,109
24
+ ydb_sqlalchemy-0.1.17.dist-info/entry_points.txt,sha256=iJxbKYuliWNBmL0iIiw8MxvOXrSEz5xe5fuEBqMRwCE,267
25
+ ydb_sqlalchemy-0.1.17.dist-info/top_level.txt,sha256=iS69Y1GTAcTok0u0oQdxP-Q5iVgUGI71XBsaEUrWhMg,20
26
+ ydb_sqlalchemy-0.1.17.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.2)
2
+ Generator: setuptools (75.3.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any