diff --git a/pyproject.toml b/pyproject.toml index 8aa6d65..0c69c62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "pyyaml>=6.0", "returns>=0.26.0", "toolz>=1.0.0", - "xorq>=0.3.11", + "xorq>=0.3.14", ] urls = { Homepage = "https://github.com/boringdata/boring-semantic-layer/tree/main" } license = "MIT" diff --git a/src/boring_semantic_layer/serialization/__init__.py b/src/boring_semantic_layer/serialization/__init__.py index 8470ae1..737f8c8 100644 --- a/src/boring_semantic_layer/serialization/__init__.py +++ b/src/boring_semantic_layer/serialization/__init__.py @@ -121,7 +121,7 @@ def extract_path_from_view(table_name): if aggregate_cache_storage is not None and isinstance(op, SemanticAggregateOp): xorq_table = xorq_table.cache(storage=aggregate_cache_storage) - xorq_table = xorq_table.tag(tag="bsl", **tag_data) + xorq_table = xorq_table.hashing_tag(tag="bsl", **tag_data) return xorq_table diff --git a/src/boring_semantic_layer/tests/test_malloy_xorq_roundtrip.py b/src/boring_semantic_layer/tests/test_malloy_xorq_roundtrip.py index 2f9366f..da216b7 100644 --- a/src/boring_semantic_layer/tests/test_malloy_xorq_roundtrip.py +++ b/src/boring_semantic_layer/tests/test_malloy_xorq_roundtrip.py @@ -412,7 +412,7 @@ def test_xorq_caching_with_malloy_model(self): tagged_expr = to_tagged(data_st) # Apply xorq-specific tags (caching hints) - cached_expr = tagged_expr.tag(tag="cache", strategy="memory", ttl="3600") + cached_expr = tagged_expr.hashing_tag(tag="cache", strategy="memory", ttl="3600") assert cached_expr is not None # Should still be convertible back diff --git a/src/boring_semantic_layer/tests/test_xorq_backends.py b/src/boring_semantic_layer/tests/test_xorq_backends.py index b80d902..22cc05c 100644 --- a/src/boring_semantic_layer/tests/test_xorq_backends.py +++ b/src/boring_semantic_layer/tests/test_xorq_backends.py @@ -237,7 +237,7 @@ def test_backend_caching_with_semantic_model(self): tagged_expr = to_tagged(model) # Tag for caching (noop for execution but useful for optimization layers) - cached_expr = tagged_expr.tag(tag="cache", cache_ttl="3600") + cached_expr = tagged_expr.hashing_tag(tag="cache", cache_ttl="3600") df = xo.execute(cached_expr) diff --git a/src/boring_semantic_layer/tests/test_xorq_convert.py b/src/boring_semantic_layer/tests/test_xorq_convert.py index 779dff6..198724b 100644 --- a/src/boring_semantic_layer/tests/test_xorq_convert.py +++ b/src/boring_semantic_layer/tests/test_xorq_convert.py @@ -182,7 +182,7 @@ def test_from_xorq_with_tagged_table(): from xorq.api import memtable # Use nested tuples format (following xorq sklearn pipeline pattern) - xorq_table = memtable({"a": [1, 2, 3]}).tag( + xorq_table = memtable({"a": [1, 2, 3]}).hashing_tag( tag="bsl_test", bsl_op_type="SemanticTableOp", bsl_version="1.0", @@ -195,6 +195,70 @@ def test_from_xorq_with_tagged_table(): assert hasattr(bsl_expr, "dimensions") +@pytest.mark.skipif(not xorq, reason="xorq not available") +def test_different_measures_produce_different_hashes(): + """Two SemanticModels on the same table with different measures should hash differently.""" + import ibis + + from boring_semantic_layer import SemanticModel + from xorq.caching.strategy import SnapshotStrategy + from xorq.common.utils.node_utils import compute_expr_hash + + table = ibis.table({"a": "int64", "b": "float64"}, name="test_table") + + model_sum = SemanticModel( + table=table, + dimensions={"a": lambda t: t.a}, + measures={"agg_b": lambda t: t.b.sum()}, + ) + model_mean = SemanticModel( + table=table, + dimensions={"a": lambda t: t.a}, + measures={"agg_b": lambda t: t.b.mean()}, + ) + + tagged_sum = to_tagged(model_sum) + tagged_mean = to_tagged(model_mean) + + strategy = SnapshotStrategy() + hash_sum = compute_expr_hash(tagged_sum, strategy=strategy) + hash_mean = compute_expr_hash(tagged_mean, strategy=strategy) + + assert hash_sum != hash_mean, "Same table with different measures should produce different hashes" + + +@pytest.mark.skipif(not xorq, reason="xorq not available") +def test_same_model_produces_same_hash(): + """Two identical SemanticModels should produce the same hash.""" + import ibis + + from boring_semantic_layer import SemanticModel + from xorq.caching.strategy import SnapshotStrategy + from xorq.common.utils.node_utils import compute_expr_hash + + table = ibis.table({"a": "int64", "b": "float64"}, name="test_table") + + model1 = SemanticModel( + table=table, + dimensions={"a": lambda t: t.a}, + measures={"sum_b": lambda t: t.b.sum()}, + ) + model2 = SemanticModel( + table=table, + dimensions={"a": lambda t: t.a}, + measures={"sum_b": lambda t: t.b.sum()}, + ) + + tagged1 = to_tagged(model1) + tagged2 = to_tagged(model2) + + strategy = SnapshotStrategy() + hash1 = compute_expr_hash(tagged1, strategy=strategy) + hash2 = compute_expr_hash(tagged2, strategy=strategy) + + assert hash1 == hash2, "Identical models should produce the same hash" + + @pytest.mark.skipif(not xorq, reason="xorq not available") def test_from_xorq_without_tags(): from xorq.api import memtable diff --git a/src/boring_semantic_layer/tests/test_xorq_integration.py b/src/boring_semantic_layer/tests/test_xorq_integration.py index 952fa12..77a3642 100644 --- a/src/boring_semantic_layer/tests/test_xorq_integration.py +++ b/src/boring_semantic_layer/tests/test_xorq_integration.py @@ -173,10 +173,10 @@ def test_xorq_caching_feature(self): # Verify xorq-specific methods are available # (These are xorq features not available in regular ibis) - assert hasattr(tagged_expr, "tag"), "Xorq tables should have tag method" + assert hasattr(tagged_expr, "hashing_tag"), "Xorq tables should have hashing_tag method" # We can add more xorq tags (e.g., for caching hints) - cached_expr = tagged_expr.tag(tag="cache", cache_strategy="aggressive") + cached_expr = tagged_expr.hashing_tag(tag="cache", cache_strategy="aggressive") assert cached_expr is not None def test_filtered_expression_to_xorq(self): @@ -223,8 +223,8 @@ def test_multi_tag_support(self): tagged_expr = to_tagged(model) # Add multiple tags - tagged = tagged_expr.tag(tag="cache", cache_ttl="3600") - tagged = tagged.tag(tag="monitoring", track_queries="true") + tagged = tagged_expr.hashing_tag(tag="cache", cache_ttl="3600") + tagged = tagged.hashing_tag(tag="monitoring", track_queries="true") # Both tags should be preserved # (This tests xorq's ability to nest tags) @@ -238,7 +238,7 @@ def test_xorq_noop_tag_preservation(self): xorq_table = memtable({"a": [1, 2, 3]}) # Tag is a noop - shouldn't affect query results - tagged_table = xorq_table.tag(tag="test", metadata="example") + tagged_table = xorq_table.hashing_tag(tag="test", metadata="example") df_untagged = execute(xorq_table) df_tagged = execute(tagged_table) diff --git a/uv.lock b/uv.lock index c96b8d0..6e5da9f 100644 --- a/uv.lock +++ b/uv.lock @@ -166,7 +166,7 @@ wheels = [ [[package]] name = "boring-semantic-layer" -version = "0.3.8" +version = "0.3.9" source = { editable = "." } dependencies = [ { name = "attrs" }, @@ -266,7 +266,7 @@ requires-dist = [ { name = "toolz", specifier = ">=1.0.0" }, { name = "urllib3", marker = "extra == 'dev'", specifier = ">=2.2.3" }, { name = "vl-convert-python", marker = "extra == 'viz-altair'", specifier = ">=1.0.0" }, - { name = "xorq", specifier = ">=0.3.11" }, + { name = "xorq", specifier = ">=0.3.14" }, { name = "xorq", marker = "extra == 'examples'" }, { name = "xorq", extras = ["duckdb"], marker = "extra == 'examples'", specifier = ">=0.3.4" }, ] @@ -1704,6 +1704,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/79/59ecf7dceafd655ed20270a0f595d9e8e13895231cebcfbff9b6eec51fc4/langsmith-0.4.49-py3-none-any.whl", hash = "sha256:95f84edcd8e74ed658e4a3eb7355b530f35cb08a9a8865dbfde6740e4b18323c", size = 410905, upload-time = "2025-11-26T21:45:14.606Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "locket" version = "1.0.0" @@ -1749,6 +1761,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1869,6 +1886,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/00/76fc92f4892d47fecb37131d0e95ea69259f077d84c68f6793a0d96cfe80/mcp-1.20.0-py3-none-any.whl", hash = "sha256:d0dc06f93653f7432ff89f694721c87f79876b6f93741bf628ad1e48f7ac5e5d", size = 173136, upload-time = "2025-10-30T22:14:51.078Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -3751,6 +3780,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "textual" +version = "8.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/50/d46c43c02e80c67ead6ba72231c1bfa90f3c6105dc26aee92a782c5f10dd/textual-8.1.0.tar.gz", hash = "sha256:4aef2badb7b15db23c9d96002ac0b6307345fac62b6a37935fb9b483414e4071", size = 1842894, upload-time = "2026-03-10T03:01:11.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/e2/5ba08dc536ece4d0bd250bcf4aea9770c5078f559de15af0ad235a1a9eb2/textual-8.1.0-py3-none-any.whl", hash = "sha256:878c05fe1a1332bcbf1000a9d9fbdecccb52a683645107612ba2c838d803b7ad", size = 719618, upload-time = "2026-03-10T03:05:31.149Z" }, +] + [[package]] name = "tiktoken" version = "0.12.0" @@ -3930,6 +3976,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "urllib3" version = "2.5.0" @@ -4077,7 +4132,7 @@ wheels = [ [[package]] name = "xorq" -version = "0.3.11" +version = "0.3.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "atpublic" }, @@ -4088,6 +4143,7 @@ dependencies = [ { name = "cryptography" }, { name = "dask", marker = "python_full_version < '4'" }, { name = "envyaml" }, + { name = "filelock" }, { name = "geoarrow-types", marker = "python_full_version < '4'" }, { name = "gitpython" }, { name = "opentelemetry-exporter-otlp" }, @@ -4108,14 +4164,15 @@ dependencies = [ { name = "sqlglot" }, { name = "strenum", marker = "python_full_version < '3.11'" }, { name = "structlog", marker = "python_full_version < '4'" }, + { name = "textual" }, { name = "toolz" }, { name = "typing-extensions" }, { name = "uv" }, { name = "xorq-datafusion" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/31/84ff558c86200860279a673ff2a3a42df43c73f427e532fa2ce4616b2eb4/xorq-0.3.11.tar.gz", hash = "sha256:7338860b724967d01e78439bc0333c0690efb7d9d83b04e8fa50328515f8dd7c", size = 1522889, upload-time = "2026-02-24T16:24:23.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/e4/871448e8423cd6badeef093e010617c373e473782d770ec0cac085573ee4/xorq-0.3.14.tar.gz", hash = "sha256:4abe4ab6209b125cc5e0db0a2e58aa7bbacf418f67c0a955e0b8478499c79337", size = 1551002, upload-time = "2026-03-09T20:35:13.691Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/aa/d699f5f373f654f32453a3d2886a7e110e89481dd0c78d3d3ade1a119675/xorq-0.3.11-py3-none-any.whl", hash = "sha256:7876413e9924b5dd800d9f51d7c5a10d2c5c9ec892aa15fa4ee27a5c3b6c4ddf", size = 1757077, upload-time = "2026-02-24T16:24:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/e9/0d/ececae397e08da8a0684ae50281afe15214891f59386afed7c16375a448e/xorq-0.3.14-py3-none-any.whl", hash = "sha256:9ae8f217f70facf13ecdac6df008788db11dae24326038952db32c42effe7812", size = 1788923, upload-time = "2026-03-09T20:35:16.953Z" }, ] [package.optional-dependencies]