ydb-sqlalchemy 0.1.9__py2.py3-none-any.whl → 0.1.11__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_suite.py CHANGED
@@ -1,4 +1,6 @@
1
1
  import ctypes
2
+ import datetime
3
+ import decimal
2
4
 
3
5
  import pytest
4
6
  import sqlalchemy as sa
@@ -266,7 +268,7 @@ class IntegerTest(_IntegerTest):
266
268
  pass
267
269
 
268
270
 
269
- @pytest.mark.skip("TODO: fix & skip those tests - add Double/Decimal support. see #12")
271
+ @pytest.mark.skip("Use YdbDecimalTest for Decimal type testing")
270
272
  class NumericTest(_NumericTest):
271
273
  # SqlAlchemy maybe eat Decimal and throw Double
272
274
  pass
@@ -423,6 +425,16 @@ class DateTest(_DateTest):
423
425
  run_dispose_bind = "once"
424
426
 
425
427
 
428
+ class Date32Test(_DateTest):
429
+ run_dispose_bind = "once"
430
+ datatype = ydb_sa_types.YqlDate32
431
+ data = datetime.date(1969, 1, 1)
432
+
433
+ @pytest.mark.skip("Default binding for DATE is not compatible with Date32")
434
+ def test_select_direct(self, connection):
435
+ pass
436
+
437
+
426
438
  class DateTimeMicrosecondsTest(_DateTimeMicrosecondsTest):
427
439
  run_dispose_bind = "once"
428
440
 
@@ -431,10 +443,30 @@ class DateTimeTest(_DateTimeTest):
431
443
  run_dispose_bind = "once"
432
444
 
433
445
 
446
+ class DateTime64Test(_DateTimeTest):
447
+ datatype = ydb_sa_types.YqlDateTime64
448
+ data = datetime.datetime(1969, 10, 15, 12, 57, 18)
449
+ run_dispose_bind = "once"
450
+
451
+ @pytest.mark.skip("Default binding for DATETIME is not compatible with DateTime64")
452
+ def test_select_direct(self, connection):
453
+ pass
454
+
455
+
434
456
  class TimestampMicrosecondsTest(_TimestampMicrosecondsTest):
435
457
  run_dispose_bind = "once"
436
458
 
437
459
 
460
+ class Timestamp64MicrosecondsTest(_TimestampMicrosecondsTest):
461
+ run_dispose_bind = "once"
462
+ datatype = ydb_sa_types.YqlTimestamp64
463
+ data = datetime.datetime(1969, 10, 15, 12, 57, 18, 396)
464
+
465
+ @pytest.mark.skip("Default binding for TIMESTAMP is not compatible with Timestamp64")
466
+ def test_select_direct(self, connection):
467
+ pass
468
+
469
+
438
470
  @pytest.mark.skip("unsupported Time data type")
439
471
  class TimeTest(_TimeTest):
440
472
  pass
@@ -596,3 +628,256 @@ class RowFetchTest(_RowFetchTest):
596
628
  @pytest.mark.skip("scalar subquery unsupported")
597
629
  def test_row_w_scalar_select(self, connection):
598
630
  pass
631
+
632
+
633
+ class DecimalTest(fixtures.TablesTest):
634
+ """Tests for YDB Decimal type using standard sa.DECIMAL"""
635
+
636
+ @classmethod
637
+ def define_tables(cls, metadata):
638
+ Table(
639
+ "decimal_test",
640
+ metadata,
641
+ Column("id", Integer, primary_key=True),
642
+ Column("decimal_default", sa.DECIMAL), # Default: precision=22, scale=9
643
+ Column("decimal_custom", sa.DECIMAL(precision=10, scale=2)),
644
+ Column("decimal_as_float", sa.DECIMAL(asdecimal=False)), # Should behave like Float
645
+ )
646
+
647
+ def test_decimal_basic_operations(self, connection):
648
+ """Test basic insert and select operations with Decimal"""
649
+
650
+ table = self.tables.decimal_test
651
+
652
+ test_values = [
653
+ decimal.Decimal("1"),
654
+ decimal.Decimal("2"),
655
+ decimal.Decimal("3"),
656
+ ]
657
+
658
+ # Insert test values
659
+ for i, val in enumerate(test_values):
660
+ connection.execute(table.insert().values(id=i + 1, decimal_default=val))
661
+
662
+ # Select and verify
663
+ results = connection.execute(select(table.c.decimal_default).order_by(table.c.id)).fetchall()
664
+
665
+ for i, (result,) in enumerate(results):
666
+ expected = test_values[i]
667
+ assert isinstance(result, decimal.Decimal)
668
+ assert result == expected
669
+
670
+ def test_decimal_with_precision_scale(self, connection):
671
+ """Test Decimal with specific precision and scale"""
672
+
673
+ table = self.tables.decimal_test
674
+
675
+ # Test value that fits precision(10, 2)
676
+ test_value = decimal.Decimal("12345678.99")
677
+
678
+ connection.execute(table.insert().values(id=100, decimal_custom=test_value))
679
+
680
+ result = connection.scalar(select(table.c.decimal_custom).where(table.c.id == 100))
681
+
682
+ assert isinstance(result, decimal.Decimal)
683
+ assert result == test_value
684
+
685
+ def test_decimal_literal_rendering(self, connection):
686
+ """Test literal rendering of Decimal values"""
687
+ from sqlalchemy import literal
688
+
689
+ table = self.tables.decimal_test
690
+
691
+ # Test literal in INSERT
692
+ test_value = decimal.Decimal("999.99")
693
+
694
+ connection.execute(table.insert().values(id=300, decimal_default=literal(test_value, sa.DECIMAL())))
695
+
696
+ result = connection.scalar(select(table.c.decimal_default).where(table.c.id == 300))
697
+
698
+ assert isinstance(result, decimal.Decimal)
699
+ assert result == test_value
700
+
701
+ def test_decimal_overflow(self, connection):
702
+ """Test behavior when precision is exceeded"""
703
+
704
+ table = self.tables.decimal_test
705
+
706
+ # Try to insert value that exceeds precision=10, scale=2
707
+ overflow_value = decimal.Decimal("99999.99999")
708
+
709
+ with pytest.raises(Exception): # Should raise some kind of database error
710
+ connection.execute(table.insert().values(id=500, decimal_custom=overflow_value))
711
+ connection.commit()
712
+
713
+ def test_decimal_asdecimal_false(self, connection):
714
+ """Test DECIMAL with asdecimal=False (should return float)"""
715
+
716
+ table = self.tables.decimal_test
717
+
718
+ test_value = decimal.Decimal("123.45")
719
+
720
+ connection.execute(table.insert().values(id=600, decimal_as_float=test_value))
721
+
722
+ result = connection.scalar(select(table.c.decimal_as_float).where(table.c.id == 600))
723
+
724
+ assert isinstance(result, float), f"Expected float, got {type(result)}"
725
+ assert abs(result - 123.45) < 0.01
726
+
727
+ def test_decimal_arithmetic(self, connection):
728
+ """Test arithmetic operations with Decimal columns"""
729
+
730
+ table = self.tables.decimal_test
731
+
732
+ val1 = decimal.Decimal("100.50")
733
+ val2 = decimal.Decimal("25.25")
734
+
735
+ connection.execute(table.insert().values(id=900, decimal_default=val1))
736
+ connection.execute(table.insert().values(id=901, decimal_default=val2))
737
+
738
+ # Test various arithmetic operations
739
+ addition_result = connection.scalar(
740
+ select(table.c.decimal_default + decimal.Decimal("10.00")).where(table.c.id == 900)
741
+ )
742
+
743
+ subtraction_result = connection.scalar(
744
+ select(table.c.decimal_default - decimal.Decimal("5.25")).where(table.c.id == 900)
745
+ )
746
+
747
+ multiplication_result = connection.scalar(
748
+ select(table.c.decimal_default * decimal.Decimal("2.0")).where(table.c.id == 901)
749
+ )
750
+
751
+ division_result = connection.scalar(
752
+ select(table.c.decimal_default / decimal.Decimal("2.0")).where(table.c.id == 901)
753
+ )
754
+
755
+ # Verify results
756
+ assert abs(addition_result - decimal.Decimal("110.50")) < decimal.Decimal("0.01")
757
+ assert abs(subtraction_result - decimal.Decimal("95.25")) < decimal.Decimal("0.01")
758
+ assert abs(multiplication_result - decimal.Decimal("50.50")) < decimal.Decimal("0.01")
759
+ assert abs(division_result - decimal.Decimal("12.625")) < decimal.Decimal("0.01")
760
+
761
+ def test_decimal_comparison_operations(self, connection):
762
+ """Test comparison operations with Decimal columns"""
763
+
764
+ table = self.tables.decimal_test
765
+
766
+ values = [
767
+ decimal.Decimal("10.50"),
768
+ decimal.Decimal("20.75"),
769
+ decimal.Decimal("15.25"),
770
+ ]
771
+
772
+ for i, val in enumerate(values):
773
+ connection.execute(table.insert().values(id=1000 + i, decimal_default=val))
774
+
775
+ # Test various comparisons
776
+ greater_than = connection.execute(
777
+ select(table.c.id).where(table.c.decimal_default > decimal.Decimal("15.00")).order_by(table.c.id)
778
+ ).fetchall()
779
+
780
+ less_than = connection.execute(
781
+ select(table.c.id).where(table.c.decimal_default < decimal.Decimal("15.00")).order_by(table.c.id)
782
+ ).fetchall()
783
+
784
+ equal_to = connection.execute(
785
+ select(table.c.id).where(table.c.decimal_default == decimal.Decimal("15.25"))
786
+ ).fetchall()
787
+
788
+ between_values = connection.execute(
789
+ select(table.c.id)
790
+ .where(table.c.decimal_default.between(decimal.Decimal("15.00"), decimal.Decimal("21.00")))
791
+ .order_by(table.c.id)
792
+ ).fetchall()
793
+
794
+ # Verify results
795
+ assert len(greater_than) == 2 # 20.75 and 15.25
796
+ assert len(less_than) == 1 # 10.50
797
+ assert len(equal_to) == 1 # 15.25
798
+ assert len(between_values) == 2 # 20.75 and 15.25
799
+
800
+ def test_decimal_null_handling(self, connection):
801
+ """Test NULL handling with Decimal columns"""
802
+
803
+ table = self.tables.decimal_test
804
+
805
+ # Insert NULL value
806
+ connection.execute(table.insert().values(id=1100, decimal_default=None))
807
+
808
+ # Insert non-NULL value for comparison
809
+ connection.execute(table.insert().values(id=1101, decimal_default=decimal.Decimal("42.42")))
810
+
811
+ # Test NULL retrieval
812
+ null_result = connection.scalar(select(table.c.decimal_default).where(table.c.id == 1100))
813
+
814
+ non_null_result = connection.scalar(select(table.c.decimal_default).where(table.c.id == 1101))
815
+
816
+ assert null_result is None
817
+ assert non_null_result == decimal.Decimal("42.42")
818
+
819
+ # Test IS NULL / IS NOT NULL
820
+ null_count = connection.scalar(select(func.count()).where(table.c.decimal_default.is_(None)))
821
+
822
+ not_null_count = connection.scalar(select(func.count()).where(table.c.decimal_default.isnot(None)))
823
+
824
+ # Should have at least 1 NULL and several non-NULL values from other tests
825
+ assert null_count >= 1
826
+ assert not_null_count >= 1
827
+
828
+ def test_decimal_input_type_conversion(self, connection):
829
+ """Test that bind_processor handles different input types correctly (float, string, int, Decimal)"""
830
+
831
+ table = self.tables.decimal_test
832
+
833
+ # Test different input types that should all be converted to Decimal
834
+ test_cases = [
835
+ (1400, 123.45, "float input"), # float
836
+ (1401, "456.78", "string input"), # string
837
+ (1402, decimal.Decimal("789.12"), "decimal input"), # already Decimal
838
+ (1403, 100, "int input"), # int
839
+ ]
840
+
841
+ for test_id, input_value, description in test_cases:
842
+ connection.execute(table.insert().values(id=test_id, decimal_default=input_value))
843
+
844
+ result = connection.scalar(select(table.c.decimal_default).where(table.c.id == test_id))
845
+
846
+ # All should be returned as Decimal
847
+ assert isinstance(result, decimal.Decimal), f"Failed for {description}: got {type(result)}"
848
+
849
+ # Verify the value is approximately correct
850
+ expected = decimal.Decimal(str(input_value))
851
+ error_str = f"Failed for {description}: expected {expected}, got {result}"
852
+ assert abs(result - expected) < decimal.Decimal("0.01"), error_str
853
+
854
+ def test_decimal_asdecimal_comparison(self, connection):
855
+ """Test comparison between asdecimal=True and asdecimal=False behavior"""
856
+
857
+ table = self.tables.decimal_test
858
+
859
+ test_value = decimal.Decimal("999.123")
860
+
861
+ # Insert same value into both columns
862
+ connection.execute(
863
+ table.insert().values(
864
+ id=1500,
865
+ decimal_default=test_value, # asdecimal=True (default)
866
+ decimal_as_float=test_value, # asdecimal=False
867
+ )
868
+ )
869
+
870
+ # Get results from both columns
871
+ result_as_decimal = connection.scalar(select(table.c.decimal_default).where(table.c.id == 1500))
872
+ result_as_float = connection.scalar(select(table.c.decimal_as_float).where(table.c.id == 1500))
873
+
874
+ # Check types are different
875
+ assert isinstance(result_as_decimal, decimal.Decimal), f"Expected Decimal, got {type(result_as_decimal)}"
876
+ assert isinstance(result_as_float, float), f"Expected float, got {type(result_as_float)}"
877
+
878
+ # Check values are approximately equal
879
+ assert abs(result_as_decimal - test_value) < decimal.Decimal("0.001")
880
+ assert abs(result_as_float - float(test_value)) < 0.001
881
+
882
+ # Check that converting between them gives same value
883
+ assert abs(float(result_as_decimal) - result_as_float) < 0.001
@@ -1 +1 @@
1
- VERSION = "0.1.9"
1
+ VERSION = "0.1.11"
@@ -61,6 +61,9 @@ COLUMN_TYPES = {
61
61
  ydb.DecimalType: sa.DECIMAL,
62
62
  ydb.PrimitiveType.Yson: sa.TEXT,
63
63
  ydb.PrimitiveType.Date: sa.DATE,
64
+ ydb.PrimitiveType.Date32: sa.DATE,
65
+ ydb.PrimitiveType.Timestamp64: sa.TIMESTAMP,
66
+ ydb.PrimitiveType.Datetime64: sa.DATETIME,
64
67
  ydb.PrimitiveType.Datetime: sa.DATETIME,
65
68
  ydb.PrimitiveType.Timestamp: sa.TIMESTAMP,
66
69
  ydb.PrimitiveType.Interval: sa.INTEGER,
@@ -136,9 +139,11 @@ class YqlDialect(StrCompileDialect):
136
139
  colspecs = {
137
140
  sa.types.JSON: types.YqlJSON,
138
141
  sa.types.JSON.JSONPathType: types.YqlJSON.YqlJSONPathType,
142
+ sa.types.Date: types.YqlDate,
139
143
  sa.types.DateTime: types.YqlTimestamp, # Because YDB's DateTime doesn't store microseconds
140
144
  sa.types.DATETIME: types.YqlDateTime,
141
145
  sa.types.TIMESTAMP: types.YqlTimestamp,
146
+ sa.types.DECIMAL: types.Decimal,
142
147
  }
143
148
 
144
149
  connection_characteristics = util.immutabledict(
@@ -113,9 +113,13 @@ class BaseYqlTypeCompiler(StrSQLTypeCompiler):
113
113
  return "Int64"
114
114
 
115
115
  def visit_NUMERIC(self, type_: sa.Numeric, **kw):
116
- """Only Decimal(22,9) is supported for table columns"""
117
116
  return f"Decimal({type_.precision}, {type_.scale})"
118
117
 
118
+ def visit_DECIMAL(self, type_: sa.DECIMAL, **kw):
119
+ precision = getattr(type_, "precision", None) or 22
120
+ scale = getattr(type_, "scale", None) or 9
121
+ return f"Decimal({precision}, {scale})"
122
+
119
123
  def visit_BINARY(self, type_: sa.BINARY, **kw):
120
124
  return "String"
121
125
 
@@ -131,6 +135,15 @@ class BaseYqlTypeCompiler(StrSQLTypeCompiler):
131
135
  def visit_TIMESTAMP(self, type_: sa.TIMESTAMP, **kw):
132
136
  return "Timestamp"
133
137
 
138
+ def visit_date32(self, type_: types.YqlDate32, **kw):
139
+ return "Date32"
140
+
141
+ def visit_timestamp64(self, type_: types.YqlTimestamp64, **kw):
142
+ return "Timestamp64"
143
+
144
+ def visit_datetime64(self, type_: types.YqlDateTime64, **kw):
145
+ return "DateTime64"
146
+
134
147
  def visit_list_type(self, type_: types.ListType, **kw):
135
148
  inner = self.process(type_.item_type, **kw)
136
149
  return f"List<{inner}>"
@@ -189,6 +202,12 @@ class BaseYqlTypeCompiler(StrSQLTypeCompiler):
189
202
  elif isinstance(type_, types.YqlJSON.YqlJSONPathType):
190
203
  ydb_type = ydb.PrimitiveType.Utf8
191
204
  # Json
205
+ elif isinstance(type_, types.YqlDate32):
206
+ ydb_type = ydb.PrimitiveType.Date32
207
+ elif isinstance(type_, types.YqlTimestamp64):
208
+ ydb_type = ydb.PrimitiveType.Timestamp64
209
+ elif isinstance(type_, types.YqlDateTime64):
210
+ ydb_type = ydb.PrimitiveType.Datetime64
192
211
  elif isinstance(type_, sa.DATETIME):
193
212
  ydb_type = ydb.PrimitiveType.Datetime
194
213
  elif isinstance(type_, sa.TIMESTAMP):
@@ -204,7 +223,9 @@ class BaseYqlTypeCompiler(StrSQLTypeCompiler):
204
223
  elif isinstance(type_, sa.Boolean):
205
224
  ydb_type = ydb.PrimitiveType.Bool
206
225
  elif isinstance(type_, sa.Numeric):
207
- ydb_type = ydb.DecimalType(type_.precision, type_.scale)
226
+ precision = getattr(type_, "precision", None) or 22
227
+ scale = getattr(type_, "scale", None) or 9
228
+ ydb_type = ydb.DecimalType(precision, scale)
208
229
  elif isinstance(type_, (types.ListType, sa.ARRAY)):
209
230
  ydb_type = ydb.ListType(self.get_ydb_type(type_.item_type, is_optional=False))
210
231
  elif isinstance(type_, types.StructType):
@@ -4,6 +4,16 @@ from typing import Optional
4
4
  from sqlalchemy import types as sqltypes
5
5
 
6
6
 
7
+ class YqlDate(sqltypes.Date):
8
+ def literal_processor(self, dialect):
9
+ parent = super().literal_processor(dialect)
10
+
11
+ def process(value):
12
+ return f"Date({parent(value)})"
13
+
14
+ return process
15
+
16
+
7
17
  class YqlTimestamp(sqltypes.TIMESTAMP):
8
18
  def result_processor(self, dialect, coltype):
9
19
  def process(value: Optional[datetime.datetime]) -> Optional[datetime.datetime]:
@@ -26,3 +36,39 @@ class YqlDateTime(YqlTimestamp, sqltypes.DATETIME):
26
36
  return int(value.timestamp())
27
37
 
28
38
  return process
39
+
40
+
41
+ class YqlDate32(YqlDate):
42
+ __visit_name__ = "date32"
43
+
44
+ def literal_processor(self, dialect):
45
+ parent = super().literal_processor(dialect)
46
+
47
+ def process(value):
48
+ return f"Date32({parent(value)})"
49
+
50
+ return process
51
+
52
+
53
+ class YqlTimestamp64(YqlTimestamp):
54
+ __visit_name__ = "timestamp64"
55
+
56
+ def literal_processor(self, dialect):
57
+ parent = super().literal_processor(dialect)
58
+
59
+ def process(value):
60
+ return f"Timestamp64({parent(value)})"
61
+
62
+ return process
63
+
64
+
65
+ class YqlDateTime64(YqlDateTime):
66
+ __visit_name__ = "datetime64"
67
+
68
+ def literal_processor(self, dialect):
69
+ parent = super().literal_processor(dialect)
70
+
71
+ def process(value):
72
+ return f"DateTime64({parent(value)})"
73
+
74
+ return process
@@ -1,3 +1,4 @@
1
+ from datetime import date
1
2
  import sqlalchemy as sa
2
3
 
3
4
  from . import YqlDialect, types
@@ -25,3 +26,12 @@ def test_casts():
25
26
  "CAST(1/2 AS UInt8)",
26
27
  "String::JoinFromList(ListMap(TOPFREQ(1/2, 5), ($x) -> { RETURN CAST($x AS UTF8) ;}), ', ')",
27
28
  ]
29
+
30
+
31
+ def test_ydb_types():
32
+ dialect = YqlDialect()
33
+
34
+ query = sa.literal(date(1996, 11, 19))
35
+ compiled = query.compile(dialect=dialect, compile_kwargs={"literal_binds": True})
36
+
37
+ assert str(compiled) == "Date('1996-11-19')"
@@ -1,3 +1,4 @@
1
+ import decimal
1
2
  from typing import Any, Mapping, Type, Union
2
3
 
3
4
  from sqlalchemy import __version__ as sa_version
@@ -10,7 +11,7 @@ else:
10
11
  from sqlalchemy import ARRAY, exc, types
11
12
  from sqlalchemy.sql import type_api
12
13
 
13
- from .datetime_types import YqlDateTime, YqlTimestamp # noqa: F401
14
+ from .datetime_types import YqlDate, YqlDateTime, YqlTimestamp, YqlDate32, YqlTimestamp64, YqlDateTime64 # noqa: F401
14
15
  from .json import YqlJSON # noqa: F401
15
16
 
16
17
 
@@ -46,6 +47,66 @@ class Int8(types.Integer):
46
47
  __visit_name__ = "int8"
47
48
 
48
49
 
50
+ class Decimal(types.DECIMAL):
51
+ __visit_name__ = "DECIMAL"
52
+
53
+ def __init__(self, precision=None, scale=None, asdecimal=True):
54
+ # YDB supports Decimal(22,9) by default
55
+ if precision is None:
56
+ precision = 22
57
+ if scale is None:
58
+ scale = 9
59
+ super().__init__(precision=precision, scale=scale, asdecimal=asdecimal)
60
+
61
+ def bind_processor(self, dialect):
62
+ def process(value):
63
+ if value is None:
64
+ return None
65
+ # Convert float to Decimal if needed
66
+ if isinstance(value, float):
67
+ return decimal.Decimal(str(value))
68
+ elif isinstance(value, str):
69
+ return decimal.Decimal(value)
70
+ elif not isinstance(value, decimal.Decimal):
71
+ return decimal.Decimal(str(value))
72
+ return value
73
+
74
+ return process
75
+
76
+ def result_processor(self, dialect, coltype):
77
+ def process(value):
78
+ if value is None:
79
+ return None
80
+
81
+ # YDB always returns Decimal values as decimal.Decimal objects
82
+ # But if asdecimal=False, we should convert to float
83
+ if not self.asdecimal:
84
+ return float(value)
85
+
86
+ # For asdecimal=True (default), return as Decimal
87
+ if not isinstance(value, decimal.Decimal):
88
+ return decimal.Decimal(str(value))
89
+ return value
90
+
91
+ return process
92
+
93
+ def literal_processor(self, dialect):
94
+ def process(value):
95
+ # Convert float to Decimal if needed
96
+ if isinstance(value, float):
97
+ value = decimal.Decimal(str(value))
98
+ elif not isinstance(value, decimal.Decimal):
99
+ value = decimal.Decimal(str(value))
100
+
101
+ # Use default precision and scale if not specified
102
+ precision = self.precision if self.precision is not None else 22
103
+ scale = self.scale if self.scale is not None else 9
104
+
105
+ return f'Decimal("{str(value)}", {precision}, {scale})'
106
+
107
+ return process
108
+
109
+
49
110
  class ListType(ARRAY):
50
111
  __visit_name__ = "list_type"
51
112
 
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.1
2
+ Name: ydb-sqlalchemy
3
+ Version: 0.1.11
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.21.6
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
+ [![API Reference](https://img.shields.io/badge/API-Reference-lightgreen.svg)](https://ydb-platform.github.io/ydb-sqlalchemy/api/index.html)
26
+ [![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)
27
+ [![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)
28
+
29
+ This repository contains YQL dialect for SqlAlchemy 2.0.
30
+
31
+ ---
32
+
33
+ **Documentation**: <a href="https://ydb-platform.github.io/ydb-sqlalchemy" target="_blank">https://ydb-platform.github.io/ydb-sqlalchemy</a>
34
+
35
+ ---
36
+
37
+ **Note**: Dialect also works with SqlAlchemy 1.4, but it is not fully tested.
38
+
39
+
40
+ ## Installation
41
+
42
+ ### Via PyPI
43
+ To install ydb-sqlalchemy from PyPI:
44
+
45
+ ```bash
46
+ $ pip install ydb-sqlalchemy
47
+ ```
48
+
49
+ ### Installation from source code
50
+ To work with current ydb-sqlalchemy version clone this repo and run from source root:
51
+
52
+ ```bash
53
+ $ pip install -U .
54
+ ```
55
+
56
+ ## Getting started
57
+
58
+ Connect to local YDB using SqlAlchemy:
59
+
60
+ ```python3
61
+ import sqlalchemy as sa
62
+
63
+
64
+ engine = sa.create_engine("yql+ydb://localhost:2136/local")
65
+
66
+ with engine.connect() as conn:
67
+ rs = conn.execute(sa.text("SELECT 1 AS value"))
68
+ print(rs.fetchone())
69
+
70
+ ```
71
+
72
+ ## Authentication
73
+
74
+ To specify credentials, you should pass `credentials` object to `connect_args` argument of `create_engine` method.
75
+
76
+ ### Static Credentials
77
+
78
+ To use static credentials you should specify `username` and `password` as follows:
79
+
80
+ ```python3
81
+ engine = sa.create_engine(
82
+ "yql+ydb://localhost:2136/local",
83
+ connect_args = {
84
+ "credentials": {
85
+ "username": "...",
86
+ "password": "..."
87
+ }
88
+ }
89
+ )
90
+ ```
91
+
92
+ ### Token Credentials
93
+
94
+ To use access token credentials you should specify `token` as follows:
95
+
96
+ ```python3
97
+ engine = sa.create_engine(
98
+ "yql+ydb://localhost:2136/local",
99
+ connect_args = {
100
+ "credentials": {
101
+ "token": "..."
102
+ }
103
+ }
104
+ )
105
+ ```
106
+
107
+ ### Service Account Credentials
108
+
109
+ To use service account credentials you should specify `service_account_json` as follows:
110
+
111
+ ```python3
112
+ engine = sa.create_engine(
113
+ "yql+ydb://localhost:2136/local",
114
+ connect_args = {
115
+ "credentials": {
116
+ "service_account_json": {
117
+ "id": "...",
118
+ "service_account_id": "...",
119
+ "created_at": "...",
120
+ "key_algorithm": "...",
121
+ "public_key": "...",
122
+ "private_key": "..."
123
+ }
124
+ }
125
+ }
126
+ )
127
+ ```
128
+
129
+ ### Credentials from YDB SDK
130
+
131
+ To use any credentials that comes with `ydb` package, just pass credentials object as follows:
132
+
133
+ ```python3
134
+ import ydb.iam
135
+
136
+ engine = sa.create_engine(
137
+ "yql+ydb://localhost:2136/local",
138
+ connect_args = {
139
+ "credentials": ydb.iam.MetadataUrlCredentials()
140
+ }
141
+ )
142
+
143
+ ```
144
+
145
+
146
+ ## Migrations
147
+
148
+ To setup `alembic` to work with `YDB` please check [this example](https://github.com/ydb-platform/ydb-sqlalchemy/tree/main/examples/alembic).
149
+
150
+ ## Development
151
+
152
+ ### Run Tests:
153
+
154
+ Run the command from the root directory of the repository to start YDB in a local docker container.
155
+ ```bash
156
+ $ docker-compose up
157
+ ```
158
+
159
+ To run all tests execute the command from the root directory of the repository:
160
+ ```bash
161
+ $ tox -e test-all
162
+ ```
163
+
164
+ Run specific test:
165
+ ```bash
166
+ $ tox -e test -- test/test_core.py
167
+ ```
168
+
169
+ Check code style:
170
+ ```bash
171
+ $ tox -e style
172
+ ```
173
+
174
+ Reformat code:
175
+ ```bash
176
+ $ tox -e isort
177
+ $ tox -e black-format
178
+ ```
179
+
180
+ Run example (needs running local YDB):
181
+ ```bash
182
+ $ python -m pip install virtualenv
183
+ $ virtualenv venv
184
+ $ source venv/bin/activate
185
+ $ pip install -r requirements.txt
186
+ $ python examples/example.py
187
+ ```
188
+
189
+ ## Additional Notes
190
+
191
+ ### Pandas
192
+ 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:
193
+
194
+ * `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.
195
+
196
+ * `to_sql` is not fully optimized to load huge datasets. It is recommended to use `method="multi"` and avoid setting a very large `chunksize`.
197
+
198
+ * `read_sql` is not fully optimized to load huge datasets and could lead to significant memory consumptions.
@@ -0,0 +1,26 @@
1
+ test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ test/conftest.py,sha256=rhWa0EQB9EwO_wAwxPdK17Qi582DdbBE8p5Gv4180Ds,570
3
+ test/test_core.py,sha256=XvPJ0MtWK2gqGytps4YMUpHtJWKlEqN1rQBUpeeelAg,42859
4
+ test/test_inspect.py,sha256=c4kc3jc48MCOfllO-ciiYf1vO-HOfuv0xVoXYT1Jxro,1106
5
+ test/test_orm.py,sha256=jQVVld50zbUwxwgW9ySIWGaNDEOLzHKXjTkdpsG9TpA,1825
6
+ test/test_suite.py,sha256=XIW5kxJNKGK4FKE_nza-379_OFOwhP8zcsXEC0lPqWY,30569
7
+ ydb_sqlalchemy/__init__.py,sha256=hX7Gy-KOiHk7B5-0wj3ZmLjk4YDJnSMHIAqxVGn_PJY,181
8
+ ydb_sqlalchemy/_version.py,sha256=ZYmaLdPDGWutBbfaGi2OftPL61_FbKbiiNjA7KrAwGs,19
9
+ ydb_sqlalchemy/sqlalchemy/__init__.py,sha256=YAj29mdu0GEG0Udoo0p5ANmEyH6OsAKz5FZc0qn6FDY,16570
10
+ ydb_sqlalchemy/sqlalchemy/datetime_types.py,sha256=wrI9kpsI_f7Jhbm7Fu0o_S1QoGCLIe6A9jfUwb41aMM,1929
11
+ ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py,sha256=HsO4Vhv2nj_Qowt23mUH-jRuNdWKV3ryKnZG5OFp5m0,3109
12
+ ydb_sqlalchemy/sqlalchemy/dml.py,sha256=k_m6PLOAY7dVzG1gsyo2bB3Lp-o3rhzN0oSX_nfkbFU,310
13
+ ydb_sqlalchemy/sqlalchemy/json.py,sha256=b4ydjlQjBhlhqGP_Sy2uZVKmt__D-9M7-YLGQMdYGME,1043
14
+ ydb_sqlalchemy/sqlalchemy/requirements.py,sha256=zm6fcLormtk3KHnbtrBvxfkbG9ZyzNan38HrRB6vC3c,2505
15
+ ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py,sha256=QPRgUPsTOqAf0gzWjidsIhTPVkfOILy4SCSgNb1GE7o,1039
16
+ ydb_sqlalchemy/sqlalchemy/types.py,sha256=BVeC8RSa2nFnI3-Q7XZxCnAolHc_k_kMCfWhUmbi5wo,3823
17
+ ydb_sqlalchemy/sqlalchemy/compiler/__init__.py,sha256=QqA6r-_bw1R97nQZy5ZSJN724znXg88l4mi5PpqAOxI,492
18
+ ydb_sqlalchemy/sqlalchemy/compiler/base.py,sha256=FKtVDKl33hGLcIKpV0UR2ppQ-X2XJ5oyh10FXw4I2uY,19220
19
+ ydb_sqlalchemy/sqlalchemy/compiler/sa14.py,sha256=LanxAnwOiMnsnrY05B0jpmvGn5NXuOKMcxi_6N3obVM,1186
20
+ ydb_sqlalchemy/sqlalchemy/compiler/sa20.py,sha256=rvVhe-pq5bOyuW4KMMMAD7JIWMzy355eijymBvuPwKw,3421
21
+ ydb_sqlalchemy-0.1.11.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
22
+ ydb_sqlalchemy-0.1.11.dist-info/METADATA,sha256=wnHV82yNZg1Q5ZRAzHdO6UnOpGRLqOlF-ob2YJPObKM,5395
23
+ ydb_sqlalchemy-0.1.11.dist-info/WHEEL,sha256=Ll72iyqtt6Rbxp-Q7FSafYA1LeRv98X15xcZWRsFEmY,109
24
+ ydb_sqlalchemy-0.1.11.dist-info/entry_points.txt,sha256=iJxbKYuliWNBmL0iIiw8MxvOXrSEz5xe5fuEBqMRwCE,267
25
+ ydb_sqlalchemy-0.1.11.dist-info/top_level.txt,sha256=iS69Y1GTAcTok0u0oQdxP-Q5iVgUGI71XBsaEUrWhMg,20
26
+ ydb_sqlalchemy-0.1.11.dist-info/RECORD,,
@@ -1,104 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: ydb-sqlalchemy
3
- Version: 0.1.9
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
- To setup `alembic` to work with `YDB` please check [this example](https://github.com/ydb-platform/ydb-sqlalchemy/tree/main/examples/alembic).
66
-
67
- ## Development
68
-
69
- ### Run Tests:
70
-
71
- Run the command from the root directory of the repository to start YDB in a local docker container.
72
- ```bash
73
- $ docker-compose up
74
- ```
75
-
76
- To run all tests execute the command from the root directory of the repository:
77
- ```bash
78
- $ tox -e test-all
79
- ```
80
-
81
- Run specific test:
82
- ```bash
83
- $ tox -e test -- test/test_core.py
84
- ```
85
-
86
- Check code style:
87
- ```bash
88
- $ tox -e style
89
- ```
90
-
91
- Reformat code:
92
- ```bash
93
- $ tox -e isort
94
- $ tox -e black-format
95
- ```
96
-
97
- Run example (needs running local YDB):
98
- ```bash
99
- $ python -m pip install virtualenv
100
- $ virtualenv venv
101
- $ source venv/bin/activate
102
- $ pip install -r requirements.txt
103
- $ python examples/example.py
104
- ```
@@ -1,26 +0,0 @@
1
- test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- test/conftest.py,sha256=rhWa0EQB9EwO_wAwxPdK17Qi582DdbBE8p5Gv4180Ds,570
3
- test/test_core.py,sha256=XvPJ0MtWK2gqGytps4YMUpHtJWKlEqN1rQBUpeeelAg,42859
4
- test/test_inspect.py,sha256=c4kc3jc48MCOfllO-ciiYf1vO-HOfuv0xVoXYT1Jxro,1106
5
- test/test_orm.py,sha256=jQVVld50zbUwxwgW9ySIWGaNDEOLzHKXjTkdpsG9TpA,1825
6
- test/test_suite.py,sha256=peBlmjGpdvhkGQM0w0ulBtX2U-7V-nqqeQmm2RgAH7w,19568
7
- ydb_sqlalchemy/__init__.py,sha256=hX7Gy-KOiHk7B5-0wj3ZmLjk4YDJnSMHIAqxVGn_PJY,181
8
- ydb_sqlalchemy/_version.py,sha256=V3G9KvW0YJgXF7ujb-Y3MU9L6nKQa_pL4l-az9Fly0w,18
9
- ydb_sqlalchemy/sqlalchemy/__init__.py,sha256=GXkc4N-margcHMdZc1_xeB748y0SX2gDzgbGQONu1H4,16356
10
- ydb_sqlalchemy/sqlalchemy/datetime_types.py,sha256=MlH4YGlNeo0YihHX8ZiSIEkPdyRVL2QWP0wj-uxqzTI,914
11
- ydb_sqlalchemy/sqlalchemy/dbapi_adapter.py,sha256=HsO4Vhv2nj_Qowt23mUH-jRuNdWKV3ryKnZG5OFp5m0,3109
12
- ydb_sqlalchemy/sqlalchemy/dml.py,sha256=k_m6PLOAY7dVzG1gsyo2bB3Lp-o3rhzN0oSX_nfkbFU,310
13
- ydb_sqlalchemy/sqlalchemy/json.py,sha256=b4ydjlQjBhlhqGP_Sy2uZVKmt__D-9M7-YLGQMdYGME,1043
14
- ydb_sqlalchemy/sqlalchemy/requirements.py,sha256=zm6fcLormtk3KHnbtrBvxfkbG9ZyzNan38HrRB6vC3c,2505
15
- ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py,sha256=HSNDtmqYXf5aR1hDtzioQV9YzHALK7NPerFxZRWn7hk,782
16
- ydb_sqlalchemy/sqlalchemy/types.py,sha256=sTL3VCMs-UvYONZfheYgSVOyPd4udVYOMz4A6BtauUw,1694
17
- ydb_sqlalchemy/sqlalchemy/compiler/__init__.py,sha256=QqA6r-_bw1R97nQZy5ZSJN724znXg88l4mi5PpqAOxI,492
18
- ydb_sqlalchemy/sqlalchemy/compiler/base.py,sha256=BfaK1J-wBkixKuH1944b0dfDrZ1D4xWU9WmHDy3KM94,18378
19
- ydb_sqlalchemy/sqlalchemy/compiler/sa14.py,sha256=LanxAnwOiMnsnrY05B0jpmvGn5NXuOKMcxi_6N3obVM,1186
20
- ydb_sqlalchemy/sqlalchemy/compiler/sa20.py,sha256=rvVhe-pq5bOyuW4KMMMAD7JIWMzy355eijymBvuPwKw,3421
21
- ydb_sqlalchemy-0.1.9.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
22
- ydb_sqlalchemy-0.1.9.dist-info/METADATA,sha256=PFa6IKaOU1Q1rCSsiDk7joOJxPf5Bp4R8yIZJukBh_k,2722
23
- ydb_sqlalchemy-0.1.9.dist-info/WHEEL,sha256=Ll72iyqtt6Rbxp-Q7FSafYA1LeRv98X15xcZWRsFEmY,109
24
- ydb_sqlalchemy-0.1.9.dist-info/entry_points.txt,sha256=iJxbKYuliWNBmL0iIiw8MxvOXrSEz5xe5fuEBqMRwCE,267
25
- ydb_sqlalchemy-0.1.9.dist-info/top_level.txt,sha256=iS69Y1GTAcTok0u0oQdxP-Q5iVgUGI71XBsaEUrWhMg,20
26
- ydb_sqlalchemy-0.1.9.dist-info/RECORD,,