Skip to content

Commit d413ef3

Browse files
authored
Merge pull request #21 from LuckySting/declare-clause-compilation
Thanks for the work :)
2 parents cf9a436 + 00c618c commit d413ef3

File tree

9 files changed

+419
-274
lines changed

9 files changed

+419
-274
lines changed

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: "3.3"
22
services:
33
ydb:
4-
image: cr.yandex/yc/yandex-docker-local-ydb:latest
4+
image: cr.yandex/yc/yandex-docker-local-ydb:trunk
55
restart: always
66
ports:
77
- "2136:2136"

test/test_core.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ def test_sa_text(self, connection):
1818
assert rs.fetchone() == (1,)
1919

2020
rs = connection.execute(
21-
sa.text("SELECT x, y FROM AS_TABLE(:data)"), [{"data": [{"x": 2, "y": 1}, {"x": 3, "y": 2}]}]
21+
sa.text(
22+
"""
23+
DECLARE :data AS List<Struct<x:Int64, y:Int64>>;
24+
SELECT x, y FROM AS_TABLE(:data)
25+
"""
26+
),
27+
[{"data": [{"x": 2, "y": 1}, {"x": 3, "y": 2}]}],
2228
)
2329
assert set(rs.fetchall()) == {(2, 1), (3, 2)}
2430

@@ -184,7 +190,7 @@ def test_select_types(self, connection):
184190
id=1,
185191
# bin=b"abc",
186192
str="Hello World!",
187-
num=3.1415,
193+
num=3.5,
188194
bl=True,
189195
ts=now,
190196
date=today,
@@ -193,4 +199,4 @@ def test_select_types(self, connection):
193199
connection.execute(stm)
194200

195201
row = connection.execute(sa.select(tb)).fetchone()
196-
assert row == (1, "Hello World!", 3.1415, True, now, today)
202+
assert row == (1, "Hello World!", 3.5, True, now, today)

test/test_suite.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sqlalchemy.testing.suite import * # noqa: F401, F403
66

77
from sqlalchemy.testing import is_true, is_false
8-
from sqlalchemy.testing.suite import eq_, testing, inspect, provide_metadata, config, requirements
8+
from sqlalchemy.testing.suite import eq_, testing, inspect, provide_metadata, config, requirements, fixtures
99
from sqlalchemy.testing.suite import func, column, literal_column, select, exists
1010
from sqlalchemy.testing.suite import MetaData, Column, Table, Integer, String
1111

@@ -32,6 +32,10 @@
3232
NativeUUIDTest as _NativeUUIDTest,
3333
TimeMicrosecondsTest as _TimeMicrosecondsTest,
3434
DateTimeCoercedToDateTimeTest as _DateTimeCoercedToDateTimeTest,
35+
DateTest as _DateTest,
36+
DateTimeMicrosecondsTest as _DateTimeMicrosecondsTest,
37+
DateTimeTest as _DateTimeTest,
38+
TimestampMicrosecondsTest as _TimestampMicrosecondsTest,
3539
)
3640
from sqlalchemy.testing.suite.test_dialect import (
3741
EscapingTest as _EscapingTest,
@@ -47,6 +51,7 @@
4751
from sqlalchemy.testing.suite.test_results import RowFetchTest as _RowFetchTest
4852
from sqlalchemy.testing.suite.test_deprecations import DeprecatedCompoundSelectTest as _DeprecatedCompoundSelectTest
4953

54+
from ydb_sqlalchemy.sqlalchemy import types as ydb_sa_types
5055

5156
test_types_suite = sqlalchemy.testing.suite.test_types
5257
col_creator = test_types_suite.Column
@@ -259,6 +264,13 @@ def test_truediv_numeric(self):
259264
# SqlAlchemy maybe eat Decimal and throw Double
260265
pass
261266

267+
@testing.combinations(("6.25", "2.5", 2.5), argnames="left, right, expected")
268+
def test_truediv_float(self, connection, left, right, expected):
269+
eq_(
270+
connection.scalar(select(literal_column(left, type_=sa.Float()) / literal_column(right, type_=sa.Float()))),
271+
expected,
272+
)
273+
262274

263275
class ExistsTest(_ExistsTest):
264276
"""
@@ -402,6 +414,26 @@ def test_insert_from_select_autoinc(self, connection):
402414
def test_insert_from_select_autoinc_no_rows(self, connection):
403415
pass
404416

417+
@pytest.mark.skip("implicit PK values unsupported")
418+
def test_no_results_for_non_returning_insert(self, connection):
419+
pass
420+
421+
422+
class DateTest(_DateTest):
423+
run_dispose_bind = "once"
424+
425+
426+
class DateTimeMicrosecondsTest(_DateTimeMicrosecondsTest):
427+
run_dispose_bind = "once"
428+
429+
430+
class DateTimeTest(_DateTimeTest):
431+
run_dispose_bind = "once"
432+
433+
434+
class TimestampMicrosecondsTest(_TimestampMicrosecondsTest):
435+
run_dispose_bind = "once"
436+
405437

406438
@pytest.mark.skip("unsupported Time data type")
407439
class TimeTest(_TimeTest):
@@ -418,6 +450,71 @@ def test_nolength_string(self):
418450
foo.drop(config.db)
419451

420452

453+
class ContainerTypesTest(fixtures.TablesTest):
454+
@classmethod
455+
def define_tables(cls, metadata):
456+
Table(
457+
"container_types_test",
458+
metadata,
459+
Column("id", Integer),
460+
sa.PrimaryKeyConstraint("id"),
461+
schema=None,
462+
test_needs_fk=True,
463+
)
464+
465+
def test_ARRAY_bind_variable(self, connection):
466+
table = self.tables.container_types_test
467+
468+
connection.execute(sa.insert(table).values([{"id": 1}, {"id": 2}, {"id": 3}]))
469+
470+
stmt = select(table.c.id).where(table.c.id.in_(sa.bindparam("id", type_=sa.ARRAY(sa.Integer))))
471+
472+
eq_(connection.execute(stmt, {"id": [1, 2]}).fetchall(), [(1,), (2,)])
473+
474+
def test_list_type_bind_variable(self, connection):
475+
table = self.tables.container_types_test
476+
477+
connection.execute(sa.insert(table).values([{"id": 1}, {"id": 2}, {"id": 3}]))
478+
479+
stmt = select(table.c.id).where(table.c.id.in_(sa.bindparam("id", type_=ydb_sa_types.ListType(sa.Integer))))
480+
481+
eq_(connection.execute(stmt, {"id": [1, 2]}).fetchall(), [(1,), (2,)])
482+
483+
def test_struct_type_bind_variable(self, connection):
484+
table = self.tables.container_types_test
485+
486+
connection.execute(sa.insert(table).values([{"id": 1}, {"id": 2}, {"id": 3}]))
487+
488+
stmt = select(table.c.id).where(
489+
table.c.id
490+
== sa.text(":struct.id").bindparams(
491+
sa.bindparam("struct", type_=ydb_sa_types.StructType({"id": sa.Integer})),
492+
)
493+
)
494+
495+
eq_(connection.scalar(stmt, {"struct": {"id": 1}}), 1)
496+
497+
def test_from_as_table(self, connection):
498+
table = self.tables.container_types_test
499+
500+
connection.execute(
501+
sa.insert(table).from_select(
502+
["id"],
503+
sa.select(sa.column("id")).select_from(
504+
sa.func.as_table(
505+
sa.bindparam(
506+
"data",
507+
value=[{"id": 1}, {"id": 2}, {"id": 3}],
508+
type_=ydb_sa_types.ListType(ydb_sa_types.StructType({"id": sa.Integer})),
509+
)
510+
)
511+
),
512+
)
513+
)
514+
515+
eq_(connection.execute(sa.select(table)).fetchall(), [(1,), (2,), (3,)])
516+
517+
421518
@pytest.mark.skip("uuid unsupported for columns")
422519
class NativeUUIDTest(_NativeUUIDTest):
423520
pass

test_dbapi/test_dbapi.py

Lines changed: 31 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,101 +12,59 @@ def test_connection(connection):
1212

1313
cur = connection.cursor()
1414
with suppress(dbapi.DatabaseError):
15-
cur.execute("DROP TABLE foo", context={"isddl": True})
15+
cur.execute(dbapi.YdbQuery("DROP TABLE foo", is_ddl=True))
1616

1717
assert not connection.check_exists("/local/foo")
1818
with pytest.raises(dbapi.ProgrammingError):
1919
connection.describe("/local/foo")
2020

21-
cur.execute("CREATE TABLE foo(id Int64 NOT NULL, PRIMARY KEY (id))", context={"isddl": True})
21+
cur.execute(dbapi.YdbQuery("CREATE TABLE foo(id Int64 NOT NULL, PRIMARY KEY (id))", is_ddl=True))
2222

2323
assert connection.check_exists("/local/foo")
2424

2525
col = connection.describe("/local/foo").columns[0]
2626
assert col.name == "id"
2727
assert col.type == ydb.PrimitiveType.Int64
2828

29-
cur.execute("DROP TABLE foo", context={"isddl": True})
29+
cur.execute(dbapi.YdbQuery("DROP TABLE foo", is_ddl=True))
3030
cur.close()
3131

3232

33-
def test_cursor(connection):
33+
def test_cursor_raw_query(connection):
3434
cur = connection.cursor()
3535
assert cur
3636

3737
with suppress(dbapi.DatabaseError):
38-
cur.execute("DROP TABLE test", context={"isddl": True})
38+
cur.execute(dbapi.YdbQuery("DROP TABLE test", is_ddl=True))
3939

40-
cur.execute(
41-
"CREATE TABLE test(id Int64 NOT NULL, text Utf8, PRIMARY KEY (id))",
42-
context={"isddl": True},
43-
)
44-
45-
cur.execute('INSERT INTO test(id, text) VALUES (1, "foo")')
46-
47-
cur.execute("SELECT id, text FROM test")
48-
assert cur.fetchone() == (1, "foo"), "fetchone is ok"
49-
50-
cur.execute("SELECT id, text FROM test WHERE id = %(id)s", {"id": 1})
51-
assert cur.fetchone() == (1, "foo"), "parametrized query is ok"
40+
cur.execute(dbapi.YdbQuery("CREATE TABLE test(id Int64 NOT NULL, text Utf8, PRIMARY KEY (id))", is_ddl=True))
5241

5342
cur.execute(
54-
"INSERT INTO test(id, text) VALUES (%(id1)s, %(text1)s), (%(id2)s, %(text2)s)",
55-
{"id1": 2, "text1": "", "id2": 3, "text2": "bar"},
56-
)
57-
58-
cur.execute("UPDATE test SET text = %(t)s WHERE id = %(id)s", {"id": 2, "t": "foo2"})
59-
60-
cur.execute("SELECT id FROM test")
61-
assert set(cur.fetchall()) == {(1,), (2,), (3,)}, "fetchall is ok"
62-
63-
cur.execute("SELECT id FROM test ORDER BY id DESC")
64-
assert cur.fetchmany(2) == [(3,), (2,)], "fetchmany is ok"
65-
assert cur.fetchmany(1) == [(1,)]
66-
67-
cur.execute("SELECT id FROM test ORDER BY id LIMIT 2")
68-
assert cur.fetchall() == [(1,), (2,)], "limit clause without params is ok"
69-
70-
# TODO: Failed to convert type: Int64 to Uint64
71-
# cur.execute("SELECT id FROM test ORDER BY id LIMIT %(limit)s", {"limit": 2})
72-
# assert cur.fetchall() == [(1,), (2,)], "limit clause with params is ok"
73-
74-
cur2 = connection.cursor()
75-
cur2.execute("INSERT INTO test(id) VALUES (%(id1)s), (%(id2)s)", {"id1": 5, "id2": 6})
76-
77-
cur.execute("SELECT id FROM test ORDER BY id")
78-
assert cur.fetchall() == [(1,), (2,), (3,), (5,), (6,)], "cursor2 commit changes"
79-
80-
cur.execute("SELECT text FROM test WHERE id > %(min_id)s", {"min_id": 3})
81-
assert cur.fetchall() == [(None,), (None,)], "NULL returns as None"
82-
83-
cur.execute("SELECT id, text FROM test WHERE text LIKE %(p)s", {"p": "foo%"})
84-
assert set(cur.fetchall()) == {(1, "foo"), (2, "foo2")}, "like clause works"
85-
86-
cur.execute(
87-
# DECLARE statement (DECLARE $data AS List<Struct<id:Int64,text:Utf8>>)
88-
# will generate automatically
89-
"""INSERT INTO test SELECT id, text FROM AS_TABLE($data);""",
43+
dbapi.YdbQuery(
44+
"""
45+
DECLARE $data AS List<Struct<id:Int64, text: Utf8>>;
46+
47+
INSERT INTO test SELECT id, text FROM AS_TABLE($data);
48+
""",
49+
parameters_types={
50+
"$data": ydb.ListType(
51+
ydb.StructType()
52+
.add_member("id", ydb.PrimitiveType.Int64)
53+
.add_member("text", ydb.PrimitiveType.Utf8)
54+
)
55+
},
56+
),
9057
{
91-
"data": [
58+
"$data": [
9259
{"id": 17, "text": "seventeen"},
9360
{"id": 21, "text": "twenty one"},
9461
]
9562
},
9663
)
9764

98-
cur.execute("SELECT id FROM test ORDER BY id")
99-
assert cur.rowcount == 7, "rowcount ok"
100-
assert cur.fetchall() == [(1,), (2,), (3,), (5,), (6,), (17,), (21,)], "ok"
101-
102-
cur.execute("INSERT INTO test(id) VALUES (37)")
103-
cur.execute("SELECT id FROM test WHERE text IS %(p)s", {"p": None})
104-
assert not cur.fetchone() == (37,)
105-
106-
cur.execute("DROP TABLE test", context={"isddl": True})
65+
cur.execute(dbapi.YdbQuery("DROP TABLE test", is_ddl=True))
10766

10867
cur.close()
109-
cur2.close()
11068

11169

11270
def test_errors(connection):
@@ -116,25 +74,25 @@ def test_errors(connection):
11674
cur = connection.cursor()
11775

11876
with suppress(dbapi.DatabaseError):
119-
cur.execute("DROP TABLE test", context={"isddl": True})
77+
cur.execute(dbapi.YdbQuery("DROP TABLE test", is_ddl=True))
12078

12179
with pytest.raises(dbapi.DataError):
122-
cur.execute("SELECT 18446744073709551616")
80+
cur.execute(dbapi.YdbQuery("SELECT 18446744073709551616"))
12381

12482
with pytest.raises(dbapi.DataError):
125-
cur.execute("SELECT * FROM 拉屎")
83+
cur.execute(dbapi.YdbQuery("SELECT * FROM 拉屎"))
12684

12785
with pytest.raises(dbapi.DataError):
128-
cur.execute("SELECT floor(5 / 2)")
86+
cur.execute(dbapi.YdbQuery("SELECT floor(5 / 2)"))
12987

13088
with pytest.raises(dbapi.ProgrammingError):
131-
cur.execute("SELECT * FROM test")
89+
cur.execute(dbapi.YdbQuery("SELECT * FROM test"))
13290

133-
cur.execute("CREATE TABLE test(id Int64, PRIMARY KEY (id))", context={"isddl": True})
91+
cur.execute(dbapi.YdbQuery("CREATE TABLE test(id Int64, PRIMARY KEY (id))", is_ddl=True))
13492

135-
cur.execute("INSERT INTO test(id) VALUES(1)")
93+
cur.execute(dbapi.YdbQuery("INSERT INTO test(id) VALUES(1)"))
13694
with pytest.raises(dbapi.IntegrityError):
137-
cur.execute("INSERT INTO test(id) VALUES(1)")
95+
cur.execute(dbapi.YdbQuery("INSERT INTO test(id) VALUES(1)"))
13896

139-
cur.execute("DROP TABLE test", context={"isddl": True})
97+
cur.execute(dbapi.YdbQuery("DROP TABLE test", is_ddl=True))
14098
cur.close()

ydb_sqlalchemy/dbapi/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .connection import Connection
2+
from .cursor import Cursor, YdbQuery # noqa: F401
23
from .errors import (
34
Warning,
45
Error,

0 commit comments

Comments
 (0)