ydb-sqlalchemy 0.1.15__tar.gz → 0.1.16__tar.gz

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 (35) hide show
  1. {ydb_sqlalchemy-0.1.15/ydb_sqlalchemy.egg-info → ydb_sqlalchemy-0.1.16}/PKG-INFO +1 -1
  2. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/setup.py +1 -1
  3. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/test/test_core.py +61 -14
  4. ydb_sqlalchemy-0.1.16/ydb_sqlalchemy/_version.py +1 -0
  5. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/__init__.py +1 -0
  6. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/compiler/base.py +15 -14
  7. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/types.py +38 -0
  8. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16/ydb_sqlalchemy.egg-info}/PKG-INFO +1 -1
  9. ydb_sqlalchemy-0.1.15/ydb_sqlalchemy/_version.py +0 -1
  10. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/LICENSE +0 -0
  11. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/MANIFEST.in +0 -0
  12. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/README.md +0 -0
  13. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/pyproject.toml +0 -0
  14. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/requirements.txt +0 -0
  15. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/setup.cfg +0 -0
  16. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/test/__init__.py +0 -0
  17. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/test/conftest.py +0 -0
  18. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/test/test_inspect.py +0 -0
  19. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/test/test_orm.py +0 -0
  20. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/test/test_suite.py +0 -0
  21. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/__init__.py +0 -0
  22. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/compiler/__init__.py +0 -0
  23. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/compiler/sa14.py +0 -0
  24. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/compiler/sa20.py +0 -0
  25. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/datetime_types.py +0 -0
  26. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py +0 -0
  27. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/dml.py +0 -0
  28. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/json.py +0 -0
  29. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/requirements.py +0 -0
  30. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py +0 -0
  31. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy.egg-info/SOURCES.txt +0 -0
  32. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy.egg-info/dependency_links.txt +0 -0
  33. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy.egg-info/entry_points.txt +0 -0
  34. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy.egg-info/requires.txt +0 -0
  35. {ydb_sqlalchemy-0.1.15 → ydb_sqlalchemy-0.1.16}/ydb_sqlalchemy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ydb-sqlalchemy
3
- Version: 0.1.15
3
+ Version: 0.1.16
4
4
  Summary: YDB Dialect for SQLAlchemy
5
5
  Home-page: http://github.com/ydb-platform/ydb-sqlalchemy
6
6
  Author: Yandex LLC
@@ -13,7 +13,7 @@ with open("requirements.txt") as f:
13
13
 
14
14
  setuptools.setup(
15
15
  name="ydb-sqlalchemy",
16
- version="0.1.15", # AUTOVERSION
16
+ version="0.1.16", # AUTOVERSION
17
17
  description="YDB Dialect for SQLAlchemy",
18
18
  author="Yandex LLC",
19
19
  author_email="ydb@yandex-team.ru",
@@ -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
 
@@ -0,0 +1 @@
1
+ VERSION = "0.1.16"
@@ -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(
@@ -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
 
@@ -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.16
4
4
  Summary: YDB Dialect for SQLAlchemy
5
5
  Home-page: http://github.com/ydb-platform/ydb-sqlalchemy
6
6
  Author: Yandex LLC
@@ -1 +0,0 @@
1
- VERSION = "0.1.15"
File without changes