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.
- ydb_sqlalchemy-0.1.10/PKG-INFO +191 -0
- ydb_sqlalchemy-0.1.10/README.md +171 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/setup.py +1 -1
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/test_suite.py +278 -1
- ydb_sqlalchemy-0.1.10/ydb_sqlalchemy/_version.py +1 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/__init__.py +2 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/compiler/base.py +12 -2
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/datetime_types.py +10 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py +10 -0
- ydb_sqlalchemy-0.1.10/ydb_sqlalchemy/sqlalchemy/types.py +141 -0
- ydb_sqlalchemy-0.1.10/ydb_sqlalchemy.egg-info/PKG-INFO +191 -0
- ydb_sqlalchemy-0.1.8/PKG-INFO +0 -94
- ydb_sqlalchemy-0.1.8/README.md +0 -74
- ydb_sqlalchemy-0.1.8/ydb_sqlalchemy/_version.py +0 -1
- ydb_sqlalchemy-0.1.8/ydb_sqlalchemy/sqlalchemy/types.py +0 -80
- ydb_sqlalchemy-0.1.8/ydb_sqlalchemy.egg-info/PKG-INFO +0 -94
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/LICENSE +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/MANIFEST.in +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/pyproject.toml +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/requirements.txt +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/setup.cfg +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/__init__.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/conftest.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/test_core.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/test_inspect.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/test/test_orm.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/__init__.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/compiler/__init__.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/compiler/sa14.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/compiler/sa20.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/dml.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/json.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy/sqlalchemy/requirements.py +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/SOURCES.txt +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/dependency_links.txt +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/entry_points.txt +0 -0
- {ydb_sqlalchemy-0.1.8 → ydb_sqlalchemy-0.1.10}/ydb_sqlalchemy.egg-info/requires.txt +0 -0
- {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
|
+
[](https://github.com/ydb-platform/ydb-sqlalchemy/blob/main/LICENSE)
|
|
24
|
+
[](https://badge.fury.io/py/ydb-sqlalchemy)
|
|
25
|
+
[](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/tests.yml)
|
|
26
|
+
[](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
|
+
[](https://github.com/ydb-platform/ydb-sqlalchemy/blob/main/LICENSE)
|
|
4
|
+
[](https://badge.fury.io/py/ydb-sqlalchemy)
|
|
5
|
+
[](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/tests.yml)
|
|
6
|
+
[](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.
|
|
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("
|
|
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(
|