ydb-sqlalchemy 0.1.8__tar.gz → 0.1.10__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 (39) hide show
  1. ydb_sqlalchemy-0.1.10/PKG-INFO +191 -0
  2. ydb_sqlalchemy-0.1.10/README.md +171 -0
  3. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/setup.py +1 -1
  4. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/test_suite.py +278 -1
  5. ydb_sqlalchemy-0.1.10/ydb_sqlalchemy/_version.py +1 -0
  6. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/__init__.py +2 -0
  7. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/compiler/base.py +12 -2
  8. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/datetime_types.py +10 -0
  9. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py +10 -0
  10. ydb_sqlalchemy-0.1.10/ydb_sqlalchemy/sqlalchemy/types.py +141 -0
  11. ydb_sqlalchemy-0.1.10/ydb_sqlalchemy.egg-info/PKG-INFO +191 -0
  12. ydb_sqlalchemy-0.1.8/PKG-INFO +0 -94
  13. ydb_sqlalchemy-0.1.8/README.md +0 -74
  14. ydb_sqlalchemy-0.1.8/ydb_sqlalchemy/_version.py +0 -1
  15. ydb_sqlalchemy-0.1.8/ydb_sqlalchemy/sqlalchemy/types.py +0 -80
  16. ydb_sqlalchemy-0.1.8/ydb_sqlalchemy.egg-info/PKG-INFO +0 -94
  17. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/LICENSE +0 -0
  18. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/MANIFEST.in +0 -0
  19. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/pyproject.toml +0 -0
  20. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/requirements.txt +0 -0
  21. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/setup.cfg +0 -0
  22. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/__init__.py +0 -0
  23. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/conftest.py +0 -0
  24. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/test_core.py +0 -0
  25. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/test_inspect.py +0 -0
  26. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/test_orm.py +0 -0
  27. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/__init__.py +0 -0
  28. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/compiler/__init__.py +0 -0
  29. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/compiler/sa14.py +0 -0
  30. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/compiler/sa20.py +0 -0
  31. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py +0 -0
  32. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/dml.py +0 -0
  33. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/json.py +0 -0
  34. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/requirements.py +0 -0
  35. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/SOURCES.txt +0 -0
  36. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/dependency_links.txt +0 -0
  37. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/entry_points.txt +0 -0
  38. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/requires.txt +0 -0
  39. {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/top_level.txt +0 -0
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.1
2
+ Name: ydb-sqlalchemy
3
+ Version: 0.1.10
4
+ Summary: YDB Dialect for SQLAlchemy
5
+ Home-page: http://github.com/ydb-platform/ydb-sqlalchemy
6
+ Author: Yandex LLC
7
+ Author-email: ydb@yandex-team.ru
8
+ License: Apache 2.0
9
+ Keywords: SQLAlchemy YDB YQL
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: sqlalchemy<3.0.0,>=1.4.0
16
+ Requires-Dist: ydb>=3.18.8
17
+ Requires-Dist: ydb-dbapi>=0.1.10
18
+ Provides-Extra: yc
19
+ Requires-Dist: yandexcloud; extra == "yc"
20
+
21
+ # YDB Dialect for SQLAlchemy
22
+ ---
23
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ydb-platform/ydb-sqlalchemy/blob/main/LICENSE)
24
+ [![PyPI version](https://badge.fury.io/py/ydb-sqlalchemy.svg)](https://badge.fury.io/py/ydb-sqlalchemy)
25
+ [![Functional tests](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/tests.yml/badge.svg)](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/tests.yml)
26
+ [![Style checks](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/style.yml/badge.svg)](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/style.yml)
27
+
28
+ This repository contains YQL dialect for SqlAlchemy 2.0.
29
+
30
+ **Note**: Dialect also works with SqlAlchemy 1.4, but it is not fully tested.
31
+
32
+
33
+ ## Installation
34
+
35
+ ### Via PyPI
36
+ To install ydb-sqlalchemy from PyPI:
37
+
38
+ ```bash
39
+ $ pip install ydb-sqlalchemy
40
+ ```
41
+
42
+ ### Installation from source code
43
+ To work with current ydb-sqlalchemy version clone this repo and run from source root:
44
+
45
+ ```bash
46
+ $ pip install -U .
47
+ ```
48
+
49
+ ## Getting started
50
+
51
+ Connect to local YDB using SqlAlchemy:
52
+
53
+ ```python3
54
+ import sqlalchemy as sa
55
+
56
+
57
+ engine = sa.create_engine("yql+ydb://localhost:2136/local")
58
+
59
+ with engine.connect() as conn:
60
+ rs = conn.execute(sa.text("SELECT 1 AS value"))
61
+ print(rs.fetchone())
62
+
63
+ ```
64
+
65
+ ## Authentication
66
+
67
+ To specify credentials, you should pass `credentials` object to `connect_args` argument of `create_engine` method.
68
+
69
+ ### Static Credentials
70
+
71
+ To use static credentials you should specify `username` and `password` as follows:
72
+
73
+ ```python3
74
+ engine = sa.create_engine(
75
+ "yql+ydb://localhost:2136/local",
76
+ connect_args = {
77
+ "credentials": {
78
+ "username": "...",
79
+ "password": "..."
80
+ }
81
+ }
82
+ )
83
+ ```
84
+
85
+ ### Token Credentials
86
+
87
+ To use access token credentials you should specify `token` as follows:
88
+
89
+ ```python3
90
+ engine = sa.create_engine(
91
+ "yql+ydb://localhost:2136/local",
92
+ connect_args = {
93
+ "credentials": {
94
+ "token": "..."
95
+ }
96
+ }
97
+ )
98
+ ```
99
+
100
+ ### Service Account Credentials
101
+
102
+ To use service account credentials you should specify `service_account_json` as follows:
103
+
104
+ ```python3
105
+ engine = sa.create_engine(
106
+ "yql+ydb://localhost:2136/local",
107
+ connect_args = {
108
+ "credentials": {
109
+ "service_account_json": {
110
+ "id": "...",
111
+ "service_account_id": "...",
112
+ "created_at": "...",
113
+ "key_algorithm": "...",
114
+ "public_key": "...",
115
+ "private_key": "..."
116
+ }
117
+ }
118
+ }
119
+ )
120
+ ```
121
+
122
+ ### Credentials from YDB SDK
123
+
124
+ To use any credentials that comes with `ydb` package, just pass credentials object as follows:
125
+
126
+ ```python3
127
+ import ydb.iam
128
+
129
+ engine = sa.create_engine(
130
+ "yql+ydb://localhost:2136/local",
131
+ connect_args = {
132
+ "credentials": ydb.iam.MetadataUrlCredentials()
133
+ }
134
+ )
135
+
136
+ ```
137
+
138
+
139
+ ## Migrations
140
+
141
+ To setup `alembic` to work with `YDB` please check [this example](https://github.com/ydb-platform/ydb-sqlalchemy/tree/main/examples/alembic).
142
+
143
+ ## Development
144
+
145
+ ### Run Tests:
146
+
147
+ Run the command from the root directory of the repository to start YDB in a local docker container.
148
+ ```bash
149
+ $ docker-compose up
150
+ ```
151
+
152
+ To run all tests execute the command from the root directory of the repository:
153
+ ```bash
154
+ $ tox -e test-all
155
+ ```
156
+
157
+ Run specific test:
158
+ ```bash
159
+ $ tox -e test -- test/test_core.py
160
+ ```
161
+
162
+ Check code style:
163
+ ```bash
164
+ $ tox -e style
165
+ ```
166
+
167
+ Reformat code:
168
+ ```bash
169
+ $ tox -e isort
170
+ $ tox -e black-format
171
+ ```
172
+
173
+ Run example (needs running local YDB):
174
+ ```bash
175
+ $ python -m pip install virtualenv
176
+ $ virtualenv venv
177
+ $ source venv/bin/activate
178
+ $ pip install -r requirements.txt
179
+ $ python examples/example.py
180
+ ```
181
+
182
+ ## Additional Notes
183
+
184
+ ### Pandas
185
+ It is possible to use YDB SA engine with `pandas` fuctions [to_sql()](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_sql.html) and [read_sql](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_sql.html). However, there are some limitations:
186
+
187
+ * `to_sql` method can not be used with column tables, since it is impossible to specify `NOT NULL` columns with current `to_sql` arguments. YDB requires column tables to have `NOT NULL` attribute on `PK` columns.
188
+
189
+ * `to_sql` is not fully optimized to load huge datasets. It is recommended to use `method="multi"` and avoid setting a very large `chunksize`.
190
+
191
+ * `read_sql` is not fully optimized to load huge datasets and could lead to significant memory consumptions.
@@ -0,0 +1,171 @@
1
+ # YDB Dialect for SQLAlchemy
2
+ ---
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ydb-platform/ydb-sqlalchemy/blob/main/LICENSE)
4
+ [![PyPI version](https://badge.fury.io/py/ydb-sqlalchemy.svg)](https://badge.fury.io/py/ydb-sqlalchemy)
5
+ [![Functional tests](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/tests.yml/badge.svg)](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/tests.yml)
6
+ [![Style checks](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/style.yml/badge.svg)](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/style.yml)
7
+
8
+ This repository contains YQL dialect for SqlAlchemy 2.0.
9
+
10
+ **Note**: Dialect also works with SqlAlchemy 1.4, but it is not fully tested.
11
+
12
+
13
+ ## Installation
14
+
15
+ ### Via PyPI
16
+ To install ydb-sqlalchemy from PyPI:
17
+
18
+ ```bash
19
+ $ pip install ydb-sqlalchemy
20
+ ```
21
+
22
+ ### Installation from source code
23
+ To work with current ydb-sqlalchemy version clone this repo and run from source root:
24
+
25
+ ```bash
26
+ $ pip install -U .
27
+ ```
28
+
29
+ ## Getting started
30
+
31
+ Connect to local YDB using SqlAlchemy:
32
+
33
+ ```python3
34
+ import sqlalchemy as sa
35
+
36
+
37
+ engine = sa.create_engine("yql+ydb://localhost:2136/local")
38
+
39
+ with engine.connect() as conn:
40
+ rs = conn.execute(sa.text("SELECT 1 AS value"))
41
+ print(rs.fetchone())
42
+
43
+ ```
44
+
45
+ ## Authentication
46
+
47
+ To specify credentials, you should pass `credentials` object to `connect_args` argument of `create_engine` method.
48
+
49
+ ### Static Credentials
50
+
51
+ To use static credentials you should specify `username` and `password` as follows:
52
+
53
+ ```python3
54
+ engine = sa.create_engine(
55
+ "yql+ydb://localhost:2136/local",
56
+ connect_args = {
57
+ "credentials": {
58
+ "username": "...",
59
+ "password": "..."
60
+ }
61
+ }
62
+ )
63
+ ```
64
+
65
+ ### Token Credentials
66
+
67
+ To use access token credentials you should specify `token` as follows:
68
+
69
+ ```python3
70
+ engine = sa.create_engine(
71
+ "yql+ydb://localhost:2136/local",
72
+ connect_args = {
73
+ "credentials": {
74
+ "token": "..."
75
+ }
76
+ }
77
+ )
78
+ ```
79
+
80
+ ### Service Account Credentials
81
+
82
+ To use service account credentials you should specify `service_account_json` as follows:
83
+
84
+ ```python3
85
+ engine = sa.create_engine(
86
+ "yql+ydb://localhost:2136/local",
87
+ connect_args = {
88
+ "credentials": {
89
+ "service_account_json": {
90
+ "id": "...",
91
+ "service_account_id": "...",
92
+ "created_at": "...",
93
+ "key_algorithm": "...",
94
+ "public_key": "...",
95
+ "private_key": "..."
96
+ }
97
+ }
98
+ }
99
+ )
100
+ ```
101
+
102
+ ### Credentials from YDB SDK
103
+
104
+ To use any credentials that comes with `ydb` package, just pass credentials object as follows:
105
+
106
+ ```python3
107
+ import ydb.iam
108
+
109
+ engine = sa.create_engine(
110
+ "yql+ydb://localhost:2136/local",
111
+ connect_args = {
112
+ "credentials": ydb.iam.MetadataUrlCredentials()
113
+ }
114
+ )
115
+
116
+ ```
117
+
118
+
119
+ ## Migrations
120
+
121
+ To setup `alembic` to work with `YDB` please check [this example](https://github.com/ydb-platform/ydb-sqlalchemy/tree/main/examples/alembic).
122
+
123
+ ## Development
124
+
125
+ ### Run Tests:
126
+
127
+ Run the command from the root directory of the repository to start YDB in a local docker container.
128
+ ```bash
129
+ $ docker-compose up
130
+ ```
131
+
132
+ To run all tests execute the command from the root directory of the repository:
133
+ ```bash
134
+ $ tox -e test-all
135
+ ```
136
+
137
+ Run specific test:
138
+ ```bash
139
+ $ tox -e test -- test/test_core.py
140
+ ```
141
+
142
+ Check code style:
143
+ ```bash
144
+ $ tox -e style
145
+ ```
146
+
147
+ Reformat code:
148
+ ```bash
149
+ $ tox -e isort
150
+ $ tox -e black-format
151
+ ```
152
+
153
+ Run example (needs running local YDB):
154
+ ```bash
155
+ $ python -m pip install virtualenv
156
+ $ virtualenv venv
157
+ $ source venv/bin/activate
158
+ $ pip install -r requirements.txt
159
+ $ python examples/example.py
160
+ ```
161
+
162
+ ## Additional Notes
163
+
164
+ ### Pandas
165
+ It is possible to use YDB SA engine with `pandas` fuctions [to_sql()](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_sql.html) and [read_sql](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_sql.html). However, there are some limitations:
166
+
167
+ * `to_sql` method can not be used with column tables, since it is impossible to specify `NOT NULL` columns with current `to_sql` arguments. YDB requires column tables to have `NOT NULL` attribute on `PK` columns.
168
+
169
+ * `to_sql` is not fully optimized to load huge datasets. It is recommended to use `method="multi"` and avoid setting a very large `chunksize`.
170
+
171
+ * `read_sql` is not fully optimized to load huge datasets and could lead to significant memory consumptions.
@@ -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.8", # AUTOVERSION
16
+ version="0.1.10", # AUTOVERSION
17
17
  description="YDB Dialect for SQLAlchemy",
18
18
  author="Yandex LLC",
19
19
  author_email="ydb@yandex-team.ru",
@@ -1,4 +1,5 @@
1
1
  import ctypes
2
+ import decimal
2
3
 
3
4
  import pytest
4
5
  import sqlalchemy as sa
@@ -266,7 +267,7 @@ class IntegerTest(_IntegerTest):
266
267
  pass
267
268
 
268
269
 
269
- @pytest.mark.skip("TODO: fix & skip those tests - add Double/Decimal support. see #12")
270
+ @pytest.mark.skip("Use YdbDecimalTest for Decimal type testing")
270
271
  class NumericTest(_NumericTest):
271
272
  # SqlAlchemy maybe eat Decimal and throw Double
272
273
  pass
@@ -546,6 +547,29 @@ class ContainerTypesTest(fixtures.TablesTest):
546
547
  eq_(connection.execute(sa.select(table)).fetchall(), [(1,), (2,), (3,)])
547
548
 
548
549
 
550
+ class ConcatTest(fixtures.TablesTest):
551
+ @classmethod
552
+ def define_tables(cls, metadata):
553
+ Table(
554
+ "concat_func_test",
555
+ metadata,
556
+ Column("A", String),
557
+ Column("B", String),
558
+ sa.PrimaryKeyConstraint("A"),
559
+ schema=None,
560
+ test_needs_fk=True,
561
+ )
562
+
563
+ def test_concat_func(self, connection):
564
+ table = self.tables.concat_func_test
565
+
566
+ connection.execute(sa.insert(table).values([{"A": "A", "B": "B"}]))
567
+
568
+ stmt = select(func.concat(table.c.A, " ", table.c.B)).limit(1)
569
+
570
+ eq_(connection.scalar(stmt), "A B")
571
+
572
+
549
573
  if not OLD_SA:
550
574
  from sqlalchemy.testing.suite.test_types import NativeUUIDTest as _NativeUUIDTest
551
575
 
@@ -573,3 +597,256 @@ class RowFetchTest(_RowFetchTest):
573
597
  @pytest.mark.skip("scalar subquery unsupported")
574
598
  def test_row_w_scalar_select(self, connection):
575
599
  pass
600
+
601
+
602
+ class DecimalTest(fixtures.TablesTest):
603
+ """Tests for YDB Decimal type using standard sa.DECIMAL"""
604
+
605
+ @classmethod
606
+ def define_tables(cls, metadata):
607
+ Table(
608
+ "decimal_test",
609
+ metadata,
610
+ Column("id", Integer, primary_key=True),
611
+ Column("decimal_default", sa.DECIMAL), # Default: precision=22, scale=9
612
+ Column("decimal_custom", sa.DECIMAL(precision=10, scale=2)),
613
+ Column("decimal_as_float", sa.DECIMAL(asdecimal=False)), # Should behave like Float
614
+ )
615
+
616
+ def test_decimal_basic_operations(self, connection):
617
+ """Test basic insert and select operations with Decimal"""
618
+
619
+ table = self.tables.decimal_test
620
+
621
+ test_values = [
622
+ decimal.Decimal("1"),
623
+ decimal.Decimal("2"),
624
+ decimal.Decimal("3"),
625
+ ]
626
+
627
+ # Insert test values
628
+ for i, val in enumerate(test_values):
629
+ connection.execute(table.insert().values(id=i + 1, decimal_default=val))
630
+
631
+ # Select and verify
632
+ results = connection.execute(select(table.c.decimal_default).order_by(table.c.id)).fetchall()
633
+
634
+ for i, (result,) in enumerate(results):
635
+ expected = test_values[i]
636
+ assert isinstance(result, decimal.Decimal)
637
+ assert result == expected
638
+
639
+ def test_decimal_with_precision_scale(self, connection):
640
+ """Test Decimal with specific precision and scale"""
641
+
642
+ table = self.tables.decimal_test
643
+
644
+ # Test value that fits precision(10, 2)
645
+ test_value = decimal.Decimal("12345678.99")
646
+
647
+ connection.execute(table.insert().values(id=100, decimal_custom=test_value))
648
+
649
+ result = connection.scalar(select(table.c.decimal_custom).where(table.c.id == 100))
650
+
651
+ assert isinstance(result, decimal.Decimal)
652
+ assert result == test_value
653
+
654
+ def test_decimal_literal_rendering(self, connection):
655
+ """Test literal rendering of Decimal values"""
656
+ from sqlalchemy import literal
657
+
658
+ table = self.tables.decimal_test
659
+
660
+ # Test literal in INSERT
661
+ test_value = decimal.Decimal("999.99")
662
+
663
+ connection.execute(table.insert().values(id=300, decimal_default=literal(test_value, sa.DECIMAL())))
664
+
665
+ result = connection.scalar(select(table.c.decimal_default).where(table.c.id == 300))
666
+
667
+ assert isinstance(result, decimal.Decimal)
668
+ assert result == test_value
669
+
670
+ def test_decimal_overflow(self, connection):
671
+ """Test behavior when precision is exceeded"""
672
+
673
+ table = self.tables.decimal_test
674
+
675
+ # Try to insert value that exceeds precision=10, scale=2
676
+ overflow_value = decimal.Decimal("99999.99999")
677
+
678
+ with pytest.raises(Exception): # Should raise some kind of database error
679
+ connection.execute(table.insert().values(id=500, decimal_custom=overflow_value))
680
+ connection.commit()
681
+
682
+ def test_decimal_asdecimal_false(self, connection):
683
+ """Test DECIMAL with asdecimal=False (should return float)"""
684
+
685
+ table = self.tables.decimal_test
686
+
687
+ test_value = decimal.Decimal("123.45")
688
+
689
+ connection.execute(table.insert().values(id=600, decimal_as_float=test_value))
690
+
691
+ result = connection.scalar(select(table.c.decimal_as_float).where(table.c.id == 600))
692
+
693
+ assert isinstance(result, float), f"Expected float, got {type(result)}"
694
+ assert abs(result - 123.45) < 0.01
695
+
696
+ def test_decimal_arithmetic(self, connection):
697
+ """Test arithmetic operations with Decimal columns"""
698
+
699
+ table = self.tables.decimal_test
700
+
701
+ val1 = decimal.Decimal("100.50")
702
+ val2 = decimal.Decimal("25.25")
703
+
704
+ connection.execute(table.insert().values(id=900, decimal_default=val1))
705
+ connection.execute(table.insert().values(id=901, decimal_default=val2))
706
+
707
+ # Test various arithmetic operations
708
+ addition_result = connection.scalar(
709
+ select(table.c.decimal_default + decimal.Decimal("10.00")).where(table.c.id == 900)
710
+ )
711
+
712
+ subtraction_result = connection.scalar(
713
+ select(table.c.decimal_default - decimal.Decimal("5.25")).where(table.c.id == 900)
714
+ )
715
+
716
+ multiplication_result = connection.scalar(
717
+ select(table.c.decimal_default * decimal.Decimal("2.0")).where(table.c.id == 901)
718
+ )
719
+
720
+ division_result = connection.scalar(
721
+ select(table.c.decimal_default / decimal.Decimal("2.0")).where(table.c.id == 901)
722
+ )
723
+
724
+ # Verify results
725
+ assert abs(addition_result - decimal.Decimal("110.50")) < decimal.Decimal("0.01")
726
+ assert abs(subtraction_result - decimal.Decimal("95.25")) < decimal.Decimal("0.01")
727
+ assert abs(multiplication_result - decimal.Decimal("50.50")) < decimal.Decimal("0.01")
728
+ assert abs(division_result - decimal.Decimal("12.625")) < decimal.Decimal("0.01")
729
+
730
+ def test_decimal_comparison_operations(self, connection):
731
+ """Test comparison operations with Decimal columns"""
732
+
733
+ table = self.tables.decimal_test
734
+
735
+ values = [
736
+ decimal.Decimal("10.50"),
737
+ decimal.Decimal("20.75"),
738
+ decimal.Decimal("15.25"),
739
+ ]
740
+
741
+ for i, val in enumerate(values):
742
+ connection.execute(table.insert().values(id=1000 + i, decimal_default=val))
743
+
744
+ # Test various comparisons
745
+ greater_than = connection.execute(
746
+ select(table.c.id).where(table.c.decimal_default > decimal.Decimal("15.00")).order_by(table.c.id)
747
+ ).fetchall()
748
+
749
+ less_than = connection.execute(
750
+ select(table.c.id).where(table.c.decimal_default < decimal.Decimal("15.00")).order_by(table.c.id)
751
+ ).fetchall()
752
+
753
+ equal_to = connection.execute(
754
+ select(table.c.id).where(table.c.decimal_default == decimal.Decimal("15.25"))
755
+ ).fetchall()
756
+
757
+ between_values = connection.execute(
758
+ select(table.c.id)
759
+ .where(table.c.decimal_default.between(decimal.Decimal("15.00"), decimal.Decimal("21.00")))
760
+ .order_by(table.c.id)
761
+ ).fetchall()
762
+
763
+ # Verify results
764
+ assert len(greater_than) == 2 # 20.75 and 15.25
765
+ assert len(less_than) == 1 # 10.50
766
+ assert len(equal_to) == 1 # 15.25
767
+ assert len(between_values) == 2 # 20.75 and 15.25
768
+
769
+ def test_decimal_null_handling(self, connection):
770
+ """Test NULL handling with Decimal columns"""
771
+
772
+ table = self.tables.decimal_test
773
+
774
+ # Insert NULL value
775
+ connection.execute(table.insert().values(id=1100, decimal_default=None))
776
+
777
+ # Insert non-NULL value for comparison
778
+ connection.execute(table.insert().values(id=1101, decimal_default=decimal.Decimal("42.42")))
779
+
780
+ # Test NULL retrieval
781
+ null_result = connection.scalar(select(table.c.decimal_default).where(table.c.id == 1100))
782
+
783
+ non_null_result = connection.scalar(select(table.c.decimal_default).where(table.c.id == 1101))
784
+
785
+ assert null_result is None
786
+ assert non_null_result == decimal.Decimal("42.42")
787
+
788
+ # Test IS NULL / IS NOT NULL
789
+ null_count = connection.scalar(select(func.count()).where(table.c.decimal_default.is_(None)))
790
+
791
+ not_null_count = connection.scalar(select(func.count()).where(table.c.decimal_default.isnot(None)))
792
+
793
+ # Should have at least 1 NULL and several non-NULL values from other tests
794
+ assert null_count >= 1
795
+ assert not_null_count >= 1
796
+
797
+ def test_decimal_input_type_conversion(self, connection):
798
+ """Test that bind_processor handles different input types correctly (float, string, int, Decimal)"""
799
+
800
+ table = self.tables.decimal_test
801
+
802
+ # Test different input types that should all be converted to Decimal
803
+ test_cases = [
804
+ (1400, 123.45, "float input"), # float
805
+ (1401, "456.78", "string input"), # string
806
+ (1402, decimal.Decimal("789.12"), "decimal input"), # already Decimal
807
+ (1403, 100, "int input"), # int
808
+ ]
809
+
810
+ for test_id, input_value, description in test_cases:
811
+ connection.execute(table.insert().values(id=test_id, decimal_default=input_value))
812
+
813
+ result = connection.scalar(select(table.c.decimal_default).where(table.c.id == test_id))
814
+
815
+ # All should be returned as Decimal
816
+ assert isinstance(result, decimal.Decimal), f"Failed for {description}: got {type(result)}"
817
+
818
+ # Verify the value is approximately correct
819
+ expected = decimal.Decimal(str(input_value))
820
+ error_str = f"Failed for {description}: expected {expected}, got {result}"
821
+ assert abs(result - expected) < decimal.Decimal("0.01"), error_str
822
+
823
+ def test_decimal_asdecimal_comparison(self, connection):
824
+ """Test comparison between asdecimal=True and asdecimal=False behavior"""
825
+
826
+ table = self.tables.decimal_test
827
+
828
+ test_value = decimal.Decimal("999.123")
829
+
830
+ # Insert same value into both columns
831
+ connection.execute(
832
+ table.insert().values(
833
+ id=1500,
834
+ decimal_default=test_value, # asdecimal=True (default)
835
+ decimal_as_float=test_value, # asdecimal=False
836
+ )
837
+ )
838
+
839
+ # Get results from both columns
840
+ result_as_decimal = connection.scalar(select(table.c.decimal_default).where(table.c.id == 1500))
841
+ result_as_float = connection.scalar(select(table.c.decimal_as_float).where(table.c.id == 1500))
842
+
843
+ # Check types are different
844
+ assert isinstance(result_as_decimal, decimal.Decimal), f"Expected Decimal, got {type(result_as_decimal)}"
845
+ assert isinstance(result_as_float, float), f"Expected float, got {type(result_as_float)}"
846
+
847
+ # Check values are approximately equal
848
+ assert abs(result_as_decimal - test_value) < decimal.Decimal("0.001")
849
+ assert abs(result_as_float - float(test_value)) < 0.001
850
+
851
+ # Check that converting between them gives same value
852
+ assert abs(float(result_as_decimal) - result_as_float) < 0.001
@@ -0,0 +1 @@
1
+ VERSION = "0.1.10"
@@ -136,9 +136,11 @@ class YqlDialect(StrCompileDialect):
136
136
  colspecs = {
137
137
  sa.types.JSON: types.YqlJSON,
138
138
  sa.types.JSON.JSONPathType: types.YqlJSON.YqlJSONPathType,
139
+ sa.types.Date: types.YqlDate,
139
140
  sa.types.DateTime: types.YqlTimestamp, # Because YDB's DateTime doesn't store microseconds
140
141
  sa.types.DATETIME: types.YqlDateTime,
141
142
  sa.types.TIMESTAMP: types.YqlTimestamp,
143
+ sa.types.DECIMAL: types.Decimal,
142
144
  }
143
145
 
144
146
  connection_characteristics = util.immutabledict(