moose-lib 0.6.90__py3-none-any.whl → 0.6.283__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.
- moose_lib/__init__.py +38 -3
- moose_lib/blocks.py +497 -37
- moose_lib/clients/redis_client.py +26 -14
- moose_lib/commons.py +94 -5
- moose_lib/config/config_file.py +44 -2
- moose_lib/config/runtime.py +137 -5
- moose_lib/data_models.py +451 -46
- moose_lib/dmv2/__init__.py +88 -60
- moose_lib/dmv2/_registry.py +3 -1
- moose_lib/dmv2/_source_capture.py +37 -0
- moose_lib/dmv2/consumption.py +55 -32
- moose_lib/dmv2/ingest_api.py +9 -2
- moose_lib/dmv2/ingest_pipeline.py +56 -13
- moose_lib/dmv2/life_cycle.py +3 -1
- moose_lib/dmv2/materialized_view.py +24 -14
- moose_lib/dmv2/moose_model.py +165 -0
- moose_lib/dmv2/olap_table.py +304 -119
- moose_lib/dmv2/registry.py +28 -3
- moose_lib/dmv2/sql_resource.py +16 -8
- moose_lib/dmv2/stream.py +241 -21
- moose_lib/dmv2/types.py +14 -8
- moose_lib/dmv2/view.py +13 -6
- moose_lib/dmv2/web_app.py +175 -0
- moose_lib/dmv2/web_app_helpers.py +96 -0
- moose_lib/dmv2/workflow.py +37 -9
- moose_lib/internal.py +537 -68
- moose_lib/main.py +87 -56
- moose_lib/query_builder.py +18 -5
- moose_lib/query_param.py +54 -20
- moose_lib/secrets.py +122 -0
- moose_lib/streaming/streaming_function_runner.py +266 -156
- moose_lib/utilities/sql.py +0 -1
- {moose_lib-0.6.90.dist-info → moose_lib-0.6.283.dist-info}/METADATA +19 -1
- moose_lib-0.6.283.dist-info/RECORD +63 -0
- tests/__init__.py +1 -1
- tests/conftest.py +38 -1
- tests/test_backward_compatibility.py +85 -0
- tests/test_cluster_validation.py +85 -0
- tests/test_codec.py +75 -0
- tests/test_column_formatting.py +80 -0
- tests/test_fixedstring.py +43 -0
- tests/test_iceberg_config.py +105 -0
- tests/test_int_types.py +211 -0
- tests/test_kafka_config.py +141 -0
- tests/test_materialized.py +74 -0
- tests/test_metadata.py +37 -0
- tests/test_moose.py +21 -30
- tests/test_moose_model.py +153 -0
- tests/test_olap_table_moosemodel.py +89 -0
- tests/test_olap_table_versioning.py +210 -0
- tests/test_query_builder.py +97 -9
- tests/test_redis_client.py +10 -3
- tests/test_s3queue_config.py +211 -110
- tests/test_secrets.py +239 -0
- tests/test_simple_aggregate.py +114 -0
- tests/test_web_app.py +227 -0
- moose_lib-0.6.90.dist-info/RECORD +0 -42
- {moose_lib-0.6.90.dist-info → moose_lib-0.6.283.dist-info}/WHEEL +0 -0
- {moose_lib-0.6.90.dist-info → moose_lib-0.6.283.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Tests for MooseModel base class with column descriptors"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from moose_lib.dmv2.moose_model import MooseModel
|
|
5
|
+
from moose_lib.data_models import Column
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_moosemodel_inherits_from_basemodel():
|
|
9
|
+
"""MooseModel should be a valid Pydantic BaseModel"""
|
|
10
|
+
|
|
11
|
+
class User(MooseModel):
|
|
12
|
+
user_id: int
|
|
13
|
+
email: str
|
|
14
|
+
|
|
15
|
+
# Should work as normal Pydantic model
|
|
16
|
+
instance = User(user_id=123, email="test@example.com")
|
|
17
|
+
assert instance.user_id == 123
|
|
18
|
+
assert instance.email == "test@example.com"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_moosemodel_adds_column_descriptors():
|
|
22
|
+
"""MooseModel metaclass should add Column descriptors for each field"""
|
|
23
|
+
|
|
24
|
+
class User(MooseModel):
|
|
25
|
+
user_id: int
|
|
26
|
+
email: str
|
|
27
|
+
age: int
|
|
28
|
+
|
|
29
|
+
# Check Column descriptors exist at class level
|
|
30
|
+
assert hasattr(User, "user_id")
|
|
31
|
+
assert isinstance(User.user_id, Column)
|
|
32
|
+
assert User.user_id.name == "user_id"
|
|
33
|
+
|
|
34
|
+
assert hasattr(User, "email")
|
|
35
|
+
assert isinstance(User.email, Column)
|
|
36
|
+
assert User.email.name == "email"
|
|
37
|
+
|
|
38
|
+
assert hasattr(User, "age")
|
|
39
|
+
assert isinstance(User.age, Column)
|
|
40
|
+
assert User.age.name == "age"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_moosemodel_column_format_spec():
|
|
44
|
+
"""Column descriptors should support format specs"""
|
|
45
|
+
|
|
46
|
+
class Product(MooseModel):
|
|
47
|
+
product_id: int
|
|
48
|
+
product_name: str
|
|
49
|
+
|
|
50
|
+
# Test format spec
|
|
51
|
+
result = f"{Product.product_id:col}"
|
|
52
|
+
assert result == "`product_id`"
|
|
53
|
+
|
|
54
|
+
result = f"{Product.product_name:c}"
|
|
55
|
+
assert result == "`product_name`"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_moosemodel_adds_cols_property():
|
|
59
|
+
"""MooseModel should add .cols property for backward compatibility"""
|
|
60
|
+
|
|
61
|
+
class Order(MooseModel):
|
|
62
|
+
order_id: int
|
|
63
|
+
total: float
|
|
64
|
+
|
|
65
|
+
# Check .cols property exists
|
|
66
|
+
assert hasattr(Order, "cols")
|
|
67
|
+
assert hasattr(Order.cols, "order_id")
|
|
68
|
+
assert hasattr(Order.cols, "total")
|
|
69
|
+
|
|
70
|
+
# Verify .cols.field returns Column
|
|
71
|
+
assert isinstance(Order.cols.order_id, Column)
|
|
72
|
+
assert Order.cols.order_id.name == "order_id"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_moosemodel_instance_attributes_separate():
|
|
76
|
+
"""Instance attributes should be separate from class Column descriptors"""
|
|
77
|
+
|
|
78
|
+
class User(MooseModel):
|
|
79
|
+
user_id: int
|
|
80
|
+
email: str
|
|
81
|
+
|
|
82
|
+
# Class level: Column objects
|
|
83
|
+
assert isinstance(User.user_id, Column)
|
|
84
|
+
|
|
85
|
+
# Instance level: actual values
|
|
86
|
+
instance = User(user_id=456, email="user@test.com")
|
|
87
|
+
assert instance.user_id == 456
|
|
88
|
+
assert isinstance(instance.user_id, int)
|
|
89
|
+
assert instance.email == "user@test.com"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_moosemodel_backward_compatible_with_basemodel():
|
|
93
|
+
"""MooseModel should be usable wherever BaseModel is expected"""
|
|
94
|
+
|
|
95
|
+
class User(MooseModel):
|
|
96
|
+
user_id: int
|
|
97
|
+
email: str
|
|
98
|
+
|
|
99
|
+
# Check it's a BaseModel subclass
|
|
100
|
+
assert issubclass(User, BaseModel)
|
|
101
|
+
|
|
102
|
+
# Check Pydantic features work
|
|
103
|
+
assert hasattr(User, "model_fields")
|
|
104
|
+
assert hasattr(User, "model_validate")
|
|
105
|
+
assert hasattr(User, "model_dump")
|
|
106
|
+
|
|
107
|
+
instance = User(user_id=789, email="another@test.com")
|
|
108
|
+
dumped = instance.model_dump()
|
|
109
|
+
assert dumped == {"user_id": 789, "email": "another@test.com"}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_moosemodel_empty_model():
|
|
113
|
+
"""MooseModel should handle models with no fields"""
|
|
114
|
+
|
|
115
|
+
class EmptyModel(MooseModel):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Should not crash
|
|
119
|
+
instance = EmptyModel()
|
|
120
|
+
assert instance is not None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_moosemodel_cols_bracket_access():
|
|
124
|
+
"""MooseModel.cols should support bracket notation"""
|
|
125
|
+
|
|
126
|
+
class User(MooseModel):
|
|
127
|
+
user_id: int
|
|
128
|
+
email: str
|
|
129
|
+
|
|
130
|
+
# Bracket access
|
|
131
|
+
col = User.cols["user_id"]
|
|
132
|
+
assert isinstance(col, Column)
|
|
133
|
+
assert col.name == "user_id"
|
|
134
|
+
|
|
135
|
+
col2 = User.cols["email"]
|
|
136
|
+
assert col2.name == "email"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_moosemodel_in_sql_fstring():
|
|
140
|
+
"""MooseModel columns should work in SQL f-strings"""
|
|
141
|
+
|
|
142
|
+
class Analytics(MooseModel):
|
|
143
|
+
event_id: int
|
|
144
|
+
timestamp: str
|
|
145
|
+
value: float
|
|
146
|
+
|
|
147
|
+
# Test complete SQL construction
|
|
148
|
+
query = f"SELECT {Analytics.event_id:col}, {Analytics.timestamp:col}, {Analytics.value:col} FROM analytics WHERE {Analytics.event_id:col} > 100"
|
|
149
|
+
|
|
150
|
+
expected = (
|
|
151
|
+
"SELECT `event_id`, `timestamp`, `value` FROM analytics WHERE `event_id` > 100"
|
|
152
|
+
)
|
|
153
|
+
assert query == expected
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Tests for OlapTable with MooseModel integration"""
|
|
2
|
+
|
|
3
|
+
from moose_lib.dmv2 import OlapTable, OlapConfig, MooseModel
|
|
4
|
+
from moose_lib.data_models import Column
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_olaptable_works_with_moosemodel():
|
|
8
|
+
"""OlapTable should accept MooseModel types"""
|
|
9
|
+
|
|
10
|
+
class User(MooseModel):
|
|
11
|
+
user_id: int
|
|
12
|
+
email: str
|
|
13
|
+
|
|
14
|
+
table = OlapTable[User]("users", OlapConfig())
|
|
15
|
+
|
|
16
|
+
assert table.name == "users"
|
|
17
|
+
assert table.model_type == User
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_olaptable_moosemodel_direct_column_access():
|
|
21
|
+
"""OlapTable with MooseModel should enable direct column access via model"""
|
|
22
|
+
|
|
23
|
+
class Product(MooseModel):
|
|
24
|
+
product_id: int
|
|
25
|
+
name: str
|
|
26
|
+
price: float
|
|
27
|
+
|
|
28
|
+
table = OlapTable[Product]("products")
|
|
29
|
+
|
|
30
|
+
# Access columns through the model class
|
|
31
|
+
assert isinstance(Product.product_id, Column)
|
|
32
|
+
assert Product.product_id.name == "product_id"
|
|
33
|
+
|
|
34
|
+
# Should work in f-strings
|
|
35
|
+
query = f"SELECT {Product.product_id:col}, {Product.name:col} FROM {table.name}"
|
|
36
|
+
assert query == "SELECT `product_id`, `name` FROM products"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_olaptable_moosemodel_cols_backward_compat():
|
|
40
|
+
"""OlapTable with MooseModel should maintain .cols backward compatibility"""
|
|
41
|
+
|
|
42
|
+
class Order(MooseModel):
|
|
43
|
+
order_id: int
|
|
44
|
+
total: float
|
|
45
|
+
|
|
46
|
+
table = OlapTable[Order]("orders")
|
|
47
|
+
|
|
48
|
+
# OLD pattern still works
|
|
49
|
+
assert hasattr(Order, "cols")
|
|
50
|
+
assert isinstance(Order.cols.order_id, Column)
|
|
51
|
+
|
|
52
|
+
# Can use in queries
|
|
53
|
+
query = f"SELECT {Order.cols.order_id} FROM orders"
|
|
54
|
+
assert "`order_id`" in query
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_olaptable_with_basemodel_still_works():
|
|
58
|
+
"""OlapTable should still work with regular BaseModel (backward compat)"""
|
|
59
|
+
|
|
60
|
+
from pydantic import BaseModel
|
|
61
|
+
|
|
62
|
+
class LegacyModel(BaseModel):
|
|
63
|
+
legacy_id: int
|
|
64
|
+
legacy_name: str
|
|
65
|
+
|
|
66
|
+
# Should not crash
|
|
67
|
+
table = OlapTable[LegacyModel]("legacy")
|
|
68
|
+
|
|
69
|
+
# Old .cols pattern should still work
|
|
70
|
+
assert hasattr(table, "cols")
|
|
71
|
+
|
|
72
|
+
# Note: LegacyModel.legacy_id won't be a Column (no metaclass)
|
|
73
|
+
# This is expected - only MooseModel gets the new feature
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_olaptable_model_property():
|
|
77
|
+
"""OlapTable should provide access to the model class"""
|
|
78
|
+
|
|
79
|
+
class Analytics(MooseModel):
|
|
80
|
+
event_id: int
|
|
81
|
+
timestamp: str
|
|
82
|
+
|
|
83
|
+
table = OlapTable[Analytics]("analytics")
|
|
84
|
+
|
|
85
|
+
# Should be able to access model type
|
|
86
|
+
assert table.model_type == Analytics
|
|
87
|
+
|
|
88
|
+
# Can use for column access
|
|
89
|
+
assert isinstance(table.model_type.event_id, Column)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for OlapTable versioning functionality.
|
|
3
|
+
|
|
4
|
+
This test module verifies that multiple versions of OlapTables with the same name
|
|
5
|
+
can coexist and that the infrastructure map generation handles versioned keys correctly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from moose_lib import (
|
|
10
|
+
OlapTable,
|
|
11
|
+
OlapConfig,
|
|
12
|
+
ClickHouseEngines,
|
|
13
|
+
MergeTreeEngine,
|
|
14
|
+
ReplacingMergeTreeEngine,
|
|
15
|
+
)
|
|
16
|
+
from moose_lib.dmv2.registry import get_tables
|
|
17
|
+
from moose_lib.internal import to_infra_map
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserEvent(BaseModel):
|
|
23
|
+
"""Sample model for testing OlapTable versioning."""
|
|
24
|
+
|
|
25
|
+
user_id: str
|
|
26
|
+
event_type: str
|
|
27
|
+
timestamp: float
|
|
28
|
+
metadata: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UserEventV2(BaseModel):
|
|
32
|
+
"""Updated model with additional fields for version testing."""
|
|
33
|
+
|
|
34
|
+
user_id: str
|
|
35
|
+
event_type: str
|
|
36
|
+
timestamp: float
|
|
37
|
+
metadata: Optional[str] = None
|
|
38
|
+
session_id: str
|
|
39
|
+
user_agent: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_multiple_olap_table_versions_can_coexist():
|
|
43
|
+
"""Test that multiple versions of the same table can be registered simultaneously."""
|
|
44
|
+
# Create version 1.0 of the table
|
|
45
|
+
table_v1 = OlapTable[UserEvent](
|
|
46
|
+
"UserEvents",
|
|
47
|
+
OlapConfig(
|
|
48
|
+
version="1.0",
|
|
49
|
+
engine=MergeTreeEngine(),
|
|
50
|
+
order_by_fields=["user_id", "timestamp"],
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Create version 2.0 of the table with different configuration
|
|
55
|
+
table_v2 = OlapTable[UserEventV2](
|
|
56
|
+
"UserEvents",
|
|
57
|
+
OlapConfig(
|
|
58
|
+
version="2.0",
|
|
59
|
+
engine=ReplacingMergeTreeEngine(),
|
|
60
|
+
order_by_fields=["user_id", "timestamp", "session_id"],
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Both tables should be registered successfully
|
|
65
|
+
tables = get_tables()
|
|
66
|
+
assert "UserEvents_1.0" in tables
|
|
67
|
+
assert "UserEvents_2.0" in tables
|
|
68
|
+
|
|
69
|
+
# Verify they are different instances
|
|
70
|
+
assert tables["UserEvents_1.0"] is table_v1
|
|
71
|
+
assert tables["UserEvents_2.0"] is table_v2
|
|
72
|
+
|
|
73
|
+
# Verify configurations are different
|
|
74
|
+
assert table_v1.config.version == "1.0"
|
|
75
|
+
assert table_v2.config.version == "2.0"
|
|
76
|
+
assert isinstance(table_v1.config.engine, MergeTreeEngine)
|
|
77
|
+
assert isinstance(table_v2.config.engine, ReplacingMergeTreeEngine)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_unversioned_and_versioned_tables_can_coexist():
|
|
81
|
+
"""Test that unversioned and versioned tables with the same name can coexist."""
|
|
82
|
+
# Create unversioned table
|
|
83
|
+
unversioned_table = OlapTable[UserEvent](
|
|
84
|
+
"EventData", OlapConfig(engine=MergeTreeEngine())
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Create versioned table with same name
|
|
88
|
+
versioned_table = OlapTable[UserEvent](
|
|
89
|
+
"EventData", OlapConfig(version="1.5", engine=MergeTreeEngine())
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Both should be registered
|
|
93
|
+
tables = get_tables()
|
|
94
|
+
assert "EventData" in tables # Unversioned
|
|
95
|
+
assert "EventData_1.5" in tables # Versioned
|
|
96
|
+
|
|
97
|
+
assert tables["EventData"] is unversioned_table
|
|
98
|
+
assert tables["EventData_1.5"] is versioned_table
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_duplicate_version_registration_fails():
|
|
102
|
+
"""Test that registering the same table name and version twice fails."""
|
|
103
|
+
# Create first table
|
|
104
|
+
OlapTable[UserEvent](
|
|
105
|
+
"DuplicateTest", OlapConfig(version="1.0", engine=MergeTreeEngine())
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Attempting to create another table with same name and version should fail
|
|
109
|
+
with pytest.raises(
|
|
110
|
+
ValueError,
|
|
111
|
+
match="OlapTable with name DuplicateTest and version 1.0 already exists",
|
|
112
|
+
):
|
|
113
|
+
OlapTable[UserEvent](
|
|
114
|
+
"DuplicateTest", OlapConfig(version="1.0", engine=MergeTreeEngine())
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_infrastructure_map_uses_versioned_keys():
|
|
119
|
+
"""Test that infrastructure map generation uses versioned keys for tables."""
|
|
120
|
+
# Create multiple versions of tables
|
|
121
|
+
table_v1 = OlapTable[UserEvent](
|
|
122
|
+
"InfraMapTest",
|
|
123
|
+
OlapConfig(
|
|
124
|
+
version="1.0", engine=MergeTreeEngine(), order_by_fields=["user_id"]
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
table_v2 = OlapTable[UserEvent](
|
|
129
|
+
"InfraMapTest",
|
|
130
|
+
OlapConfig(
|
|
131
|
+
version="2.0",
|
|
132
|
+
engine=ReplacingMergeTreeEngine(),
|
|
133
|
+
order_by_fields=["user_id", "timestamp"],
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
unversioned_table = OlapTable[UserEvent](
|
|
138
|
+
"UnversionedInfraTest", OlapConfig(engine=MergeTreeEngine())
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Generate infrastructure map
|
|
142
|
+
tables_registry = get_tables()
|
|
143
|
+
infra_map = to_infra_map()
|
|
144
|
+
|
|
145
|
+
# Verify versioned keys are used in infrastructure map
|
|
146
|
+
assert "InfraMapTest_1.0" in infra_map["tables"]
|
|
147
|
+
assert "InfraMapTest_2.0" in infra_map["tables"]
|
|
148
|
+
assert "UnversionedInfraTest" in infra_map["tables"]
|
|
149
|
+
|
|
150
|
+
# Verify table configurations in infra map
|
|
151
|
+
v1_config = infra_map["tables"]["InfraMapTest_1.0"]
|
|
152
|
+
v2_config = infra_map["tables"]["InfraMapTest_2.0"]
|
|
153
|
+
unversioned_config = infra_map["tables"]["UnversionedInfraTest"]
|
|
154
|
+
|
|
155
|
+
assert v1_config["name"] == "InfraMapTest"
|
|
156
|
+
assert v1_config["version"] == "1.0"
|
|
157
|
+
assert v1_config["engineConfig"]["engine"] == "MergeTree"
|
|
158
|
+
|
|
159
|
+
assert v2_config["name"] == "InfraMapTest"
|
|
160
|
+
assert v2_config["version"] == "2.0"
|
|
161
|
+
assert v2_config["engineConfig"]["engine"] == "ReplacingMergeTree"
|
|
162
|
+
|
|
163
|
+
assert unversioned_config["name"] == "UnversionedInfraTest"
|
|
164
|
+
assert unversioned_config.get("version") is None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_version_with_dots_handled_correctly():
|
|
168
|
+
"""Test that versions with dots are handled correctly in keys."""
|
|
169
|
+
# Create table with semantic version
|
|
170
|
+
table = OlapTable[UserEvent](
|
|
171
|
+
"SemanticVersionTest", OlapConfig(version="1.2.3", engine=MergeTreeEngine())
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Should be registered with version in key
|
|
175
|
+
tables = get_tables()
|
|
176
|
+
assert "SemanticVersionTest_1.2.3" in tables
|
|
177
|
+
assert tables["SemanticVersionTest_1.2.3"] is table
|
|
178
|
+
|
|
179
|
+
# Verify in infrastructure map
|
|
180
|
+
infra_map = to_infra_map()
|
|
181
|
+
assert "SemanticVersionTest_1.2.3" in infra_map["tables"]
|
|
182
|
+
|
|
183
|
+
table_config = infra_map["tables"]["SemanticVersionTest_1.2.3"]
|
|
184
|
+
assert table_config["version"] == "1.2.3"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_backward_compatibility_with_legacy_engines():
|
|
188
|
+
"""Test that versioning works with legacy enum-based engine configuration."""
|
|
189
|
+
# Create table with legacy enum engine (should show deprecation warning)
|
|
190
|
+
table = OlapTable[UserEvent](
|
|
191
|
+
"LegacyEngineTest",
|
|
192
|
+
OlapConfig(version="1.0", engine=ClickHouseEngines.ReplacingMergeTree),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Should still be registered correctly
|
|
196
|
+
tables = get_tables()
|
|
197
|
+
assert "LegacyEngineTest_1.0" in tables
|
|
198
|
+
assert tables["LegacyEngineTest_1.0"] is table
|
|
199
|
+
|
|
200
|
+
# Should work in infrastructure map
|
|
201
|
+
infra_map = to_infra_map()
|
|
202
|
+
assert "LegacyEngineTest_1.0" in infra_map["tables"]
|
|
203
|
+
|
|
204
|
+
table_config = infra_map["tables"]["LegacyEngineTest_1.0"]
|
|
205
|
+
assert table_config["version"] == "1.0"
|
|
206
|
+
assert table_config["engineConfig"]["engine"] == "ReplacingMergeTree"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
pytest.main([__file__, "-v"])
|
tests/test_query_builder.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
|
|
3
3
|
from moose_lib.query_builder import Query, col
|
|
4
|
-
from moose_lib.dmv2 import IngestPipeline, IngestPipelineConfig
|
|
4
|
+
from moose_lib.dmv2 import IngestPipeline, IngestPipelineConfig, OlapTable, OlapConfig
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
from moose_lib.data_models import Key
|
|
7
7
|
|
|
@@ -14,15 +14,19 @@ class Bar(BaseModel):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def test_simple_select_and_where():
|
|
17
|
-
bar_model = IngestPipeline[Bar](
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
17
|
+
bar_model = IngestPipeline[Bar](
|
|
18
|
+
"Bar",
|
|
19
|
+
IngestPipelineConfig(
|
|
20
|
+
ingest=False, stream=True, table=True, dead_letter_queue=True
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
23
|
bar_cols = bar_model.get_table().cols
|
|
24
24
|
|
|
25
|
-
q1 =
|
|
25
|
+
q1 = (
|
|
26
|
+
Query()
|
|
27
|
+
.from_(bar_model.get_table())
|
|
28
|
+
.select(bar_cols.has_text, bar_cols.text_length)
|
|
29
|
+
)
|
|
26
30
|
assert q1.to_sql() == 'SELECT "Bar"."has_text", "Bar"."text_length" FROM Bar'
|
|
27
31
|
|
|
28
32
|
q2 = (
|
|
@@ -32,7 +36,91 @@ def test_simple_select_and_where():
|
|
|
32
36
|
.where(col(bar_cols.has_text).eq(True))
|
|
33
37
|
)
|
|
34
38
|
sql, params = q2.to_sql_and_params()
|
|
35
|
-
assert
|
|
39
|
+
assert (
|
|
40
|
+
sql
|
|
41
|
+
== 'SELECT "Bar"."has_text", "Bar"."text_length" FROM Bar WHERE "Bar"."has_text" = {p0: Bool}'
|
|
42
|
+
)
|
|
36
43
|
assert params == {"p0": True}
|
|
37
44
|
|
|
38
45
|
|
|
46
|
+
def test_table_with_database_config():
|
|
47
|
+
"""Test that tables with database config generate correct SQL with two identifiers"""
|
|
48
|
+
|
|
49
|
+
class TestModel(BaseModel):
|
|
50
|
+
id: int
|
|
51
|
+
name: str
|
|
52
|
+
|
|
53
|
+
# Table without database
|
|
54
|
+
table_without_db = OlapTable[TestModel]("my_table_no_db", OlapConfig())
|
|
55
|
+
|
|
56
|
+
# Table with database
|
|
57
|
+
table_with_db = OlapTable[TestModel](
|
|
58
|
+
"my_table_with_db", OlapConfig(database="my_database")
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Test Query builder with table that has database
|
|
62
|
+
q1 = (
|
|
63
|
+
Query()
|
|
64
|
+
.from_(table_with_db)
|
|
65
|
+
.select(table_with_db.cols.id, table_with_db.cols.name)
|
|
66
|
+
)
|
|
67
|
+
sql1 = q1.to_sql()
|
|
68
|
+
# The Query builder should handle the database-qualified table reference
|
|
69
|
+
assert "my_database" in sql1 or "my_table" in sql1
|
|
70
|
+
|
|
71
|
+
# Test string interpolation format for QueryClient.execute()
|
|
72
|
+
# When a table with database is used, it should generate two separate Identifier parameters
|
|
73
|
+
from string import Formatter
|
|
74
|
+
|
|
75
|
+
# Simulate what happens in QueryClient.execute() with a table that has database
|
|
76
|
+
template = "SELECT * FROM {table}"
|
|
77
|
+
variables = {"table": table_with_db}
|
|
78
|
+
|
|
79
|
+
params = {}
|
|
80
|
+
values = {}
|
|
81
|
+
i = 0
|
|
82
|
+
|
|
83
|
+
for _, variable_name, _, _ in Formatter().parse(template):
|
|
84
|
+
if variable_name:
|
|
85
|
+
value = variables[variable_name]
|
|
86
|
+
if isinstance(value, OlapTable) and value.config.database:
|
|
87
|
+
# Should use two separate Identifier parameters
|
|
88
|
+
params[variable_name] = f"{{p{i}: Identifier}}.{{p{i + 1}: Identifier}}"
|
|
89
|
+
values[f"p{i}"] = value.config.database
|
|
90
|
+
values[f"p{i + 1}"] = value.name
|
|
91
|
+
i += 2
|
|
92
|
+
else:
|
|
93
|
+
params[variable_name] = f"{{p{i}: Identifier}}"
|
|
94
|
+
values[f"p{i}"] = value.name
|
|
95
|
+
i += 1
|
|
96
|
+
|
|
97
|
+
clickhouse_query = template.format_map(params)
|
|
98
|
+
|
|
99
|
+
assert clickhouse_query == "SELECT * FROM {p0: Identifier}.{p1: Identifier}"
|
|
100
|
+
assert values == {"p0": "my_database", "p1": "my_table_with_db"}
|
|
101
|
+
|
|
102
|
+
# Test with table without database
|
|
103
|
+
variables_no_db = {"table": table_without_db}
|
|
104
|
+
params_no_db = {}
|
|
105
|
+
values_no_db = {}
|
|
106
|
+
i = 0
|
|
107
|
+
|
|
108
|
+
for _, variable_name, _, _ in Formatter().parse(template):
|
|
109
|
+
if variable_name:
|
|
110
|
+
value = variables_no_db[variable_name]
|
|
111
|
+
if isinstance(value, OlapTable) and value.config.database:
|
|
112
|
+
params_no_db[variable_name] = (
|
|
113
|
+
f"{{p{i}: Identifier}}.{{p{i + 1}: Identifier}}"
|
|
114
|
+
)
|
|
115
|
+
values_no_db[f"p{i}"] = value.config.database
|
|
116
|
+
values_no_db[f"p{i + 1}"] = value.name
|
|
117
|
+
i += 2
|
|
118
|
+
else:
|
|
119
|
+
params_no_db[variable_name] = f"{{p{i}: Identifier}}"
|
|
120
|
+
values_no_db[f"p{i}"] = value.name
|
|
121
|
+
i += 1
|
|
122
|
+
|
|
123
|
+
clickhouse_query_no_db = template.format_map(params_no_db)
|
|
124
|
+
|
|
125
|
+
assert clickhouse_query_no_db == "SELECT * FROM {p0: Identifier}"
|
|
126
|
+
assert values_no_db == {"p0": "my_table_no_db"}
|
tests/test_redis_client.py
CHANGED
|
@@ -6,10 +6,12 @@ import pytest
|
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
from moose_lib import MooseCache
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
class Config(BaseModel):
|
|
10
11
|
baz: int
|
|
11
12
|
qux: bool
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
@pytest.mark.integration
|
|
14
16
|
def test_cache_strings():
|
|
15
17
|
cache = MooseCache()
|
|
@@ -26,6 +28,7 @@ def test_cache_strings():
|
|
|
26
28
|
# Clean up
|
|
27
29
|
cache.clear_keys("test")
|
|
28
30
|
|
|
31
|
+
|
|
29
32
|
@pytest.mark.integration
|
|
30
33
|
def test_cache_pydantic():
|
|
31
34
|
cache = MooseCache()
|
|
@@ -47,6 +50,7 @@ def test_cache_pydantic():
|
|
|
47
50
|
# Clean up
|
|
48
51
|
cache.clear_keys("test")
|
|
49
52
|
|
|
53
|
+
|
|
50
54
|
@pytest.mark.integration
|
|
51
55
|
def test_cache_ttl():
|
|
52
56
|
cache = MooseCache()
|
|
@@ -66,6 +70,7 @@ def test_cache_ttl():
|
|
|
66
70
|
# Clean up
|
|
67
71
|
cache.clear_keys("test")
|
|
68
72
|
|
|
73
|
+
|
|
69
74
|
@pytest.mark.integration
|
|
70
75
|
def test_cache_nonexistent():
|
|
71
76
|
cache = MooseCache()
|
|
@@ -75,6 +80,7 @@ def test_cache_nonexistent():
|
|
|
75
80
|
assert cache.get("nonexistent", str) is None
|
|
76
81
|
assert cache.get("nonexistent", Config) is None
|
|
77
82
|
|
|
83
|
+
|
|
78
84
|
@pytest.mark.integration
|
|
79
85
|
def test_cache_invalid_type():
|
|
80
86
|
cache = MooseCache()
|
|
@@ -86,6 +92,7 @@ def test_cache_invalid_type():
|
|
|
86
92
|
with pytest.raises(TypeError):
|
|
87
93
|
cache.get("test", dict)
|
|
88
94
|
|
|
95
|
+
|
|
89
96
|
@pytest.mark.integration
|
|
90
97
|
def test_atexit_cleanup():
|
|
91
98
|
# Create a test script that will be run in a separate process
|
|
@@ -107,9 +114,9 @@ sys.exit(0)
|
|
|
107
114
|
|
|
108
115
|
try:
|
|
109
116
|
# Run the script and capture output
|
|
110
|
-
result = subprocess.run(
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
result = subprocess.run(
|
|
118
|
+
[sys.executable, "test_atexit.py"], capture_output=True, text=True
|
|
119
|
+
)
|
|
113
120
|
|
|
114
121
|
# Check if we see both the connection and disconnection messages
|
|
115
122
|
output = result.stdout + result.stderr
|