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 +286 -1
- ydb_sqlalchemy/_version.py +1 -1
- ydb_sqlalchemy/sqlalchemy/__init__.py +5 -0
- ydb_sqlalchemy/sqlalchemy/compiler/base.py +23 -2
- ydb_sqlalchemy/sqlalchemy/datetime_types.py +46 -0
- ydb_sqlalchemy/sqlalchemy/test_sqlalchemy.py +10 -0
- ydb_sqlalchemy/sqlalchemy/types.py +62 -1
- ydb_sqlalchemy-0.1.11.dist-info/METADATA +198 -0
- ydb_sqlalchemy-0.1.11.dist-info/RECORD +26 -0
- ydb_sqlalchemy-0.1.9.dist-info/METADATA +0 -104
- ydb_sqlalchemy-0.1.9.dist-info/RECORD +0 -26
- {ydb_sqlalchemy-0.1.9.dist-info → ydb_sqlalchemy-0.1.11.dist-info}/LICENSE +0 -0
- {ydb_sqlalchemy-0.1.9.dist-info → ydb_sqlalchemy-0.1.11.dist-info}/WHEEL +0 -0
- {ydb_sqlalchemy-0.1.9.dist-info → ydb_sqlalchemy-0.1.11.dist-info}/entry_points.txt +0 -0
- {ydb_sqlalchemy-0.1.9.dist-info → ydb_sqlalchemy-0.1.11.dist-info}/top_level.txt +0 -0
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("
|
|
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
|
ydb_sqlalchemy/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "0.1.
|
|
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
|
-
|
|
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
|
+
[](https://github.com/ydb-platform/ydb-sqlalchemy/blob/main/LICENSE)
|
|
24
|
+
[](https://badge.fury.io/py/ydb-sqlalchemy)
|
|
25
|
+
[](https://ydb-platform.github.io/ydb-sqlalchemy/api/index.html)
|
|
26
|
+
[](https://github.com/ydb-platform/ydb-sqlalchemy/actions/workflows/tests.yml)
|
|
27
|
+
[](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
|
-
[](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
|
-
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|