From c5bd990938d174e3edf6444ddeda10ec42ed4f25 Mon Sep 17 00:00:00 2001 From: ghoersti Date: Tue, 19 May 2026 15:48:01 -0400 Subject: [PATCH 1/3] chore(deps): bump xorq>=0.3.25 and adapt tests to API changes Between the previously-pinned 0.3.19 and the new 0.3.25, xorq: - changed the dispatch returned by ``get_rebuild_dispatch`` to take ``(rebuild_subexpr, remap, to_catalog)`` instead of just ``(rebuild_subexpr)``. - made tag-op classes frozen with ``parent: Relation``, so ``parent=None`` is no longer constructible (and ``.parent`` cannot be mutated after construction). - removed ``xorq.api.read_parquet``; callers use ``deferred_read_parquet``. - tightened ``xorq.vendor.ibis`` literal inference: scalars constructed via the foreign top-level ``ibis`` package are rejected inside a vendored expression. - added a ``Replayer._rewrite_noop_commits`` step that invokes ``git rebase --onto``, which needs a configured git identity. Adaptations: - test_xorq_backends: rename xo.read_parquet -> xo.deferred_read_parquet. - test_xorq_rebuild: pass ``(rebuild_subexpr, None, None)`` to the dispatch callable, and replace ``ibis.literal(1)`` inside a reemit callback with a raw Python scalar so ``mutate`` infers in the parent's flavor. - test_xorq_rebuild: add an autouse fixture that sets ``GIT_*_NAME/EMAIL`` env vars so the catalog-replay rebase works on CI runners with no global git identity. - serialization/tag_handler.reemit: drop the ``if tag_node.parent is None`` guard. xorq now declares ``parent`` as a non-null ``Relation``, so the precondition is structurally impossible. Drop the matching ``test_reemit_raises_on_missing_parent`` test (it could only fire by mutating a real tag node into an invalid shape, which xorq's immutability now rejects). Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 2 +- .../serialization/tag_handler.py | 8 +++- .../tests/test_xorq_backends.py | 2 +- .../tests/test_xorq_rebuild.py | 38 +++++++++---------- uv.lock | 33 ++++++++-------- 5 files changed, 41 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 84507a75..7f1acef0 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.19", + "xorq>=0.3.25", ] urls = { Homepage = "https://github.com/boringdata/boring-semantic-layer/tree/main" } license = "MIT" diff --git a/src/boring_semantic_layer/serialization/tag_handler.py b/src/boring_semantic_layer/serialization/tag_handler.py index f0cb7696..d964ee18 100644 --- a/src/boring_semantic_layer/serialization/tag_handler.py +++ b/src/boring_semantic_layer/serialization/tag_handler.py @@ -103,9 +103,13 @@ def reemit(tag_node, rebuild_subexpr): Re-stamping uses ``hashing_tag`` (not ``tag``) so the rebuilt expression keeps the same hash-contribution guarantee as ``to_tagged`` — see #263. + + Precondition: ``tag_node`` is a BSL-tagged xorq tag op (HashingTag/Tag). + xorq's dispatch only routes here when ``tag_node.metadata["tag"]`` + resolves to this handler, and xorq's op definition declares + ``parent: Relation`` (non-null) — so by construction + ``tag_node.parent`` is always a valid relation. """ - if tag_node.parent is None: - raise ValueError("tag_node has no parent; cannot rebuild a root tag node") new_source = rebuild_subexpr(tag_node.parent.to_expr()) meta = dict(tag_node.metadata) tag_name = meta.pop("tag") diff --git a/src/boring_semantic_layer/tests/test_xorq_backends.py b/src/boring_semantic_layer/tests/test_xorq_backends.py index b80d9022..642b7788 100644 --- a/src/boring_semantic_layer/tests/test_xorq_backends.py +++ b/src/boring_semantic_layer/tests/test_xorq_backends.py @@ -269,7 +269,7 @@ def test_read_write_operations(self): df.to_parquet(temp_path) # Read back with xorq - read_back = xo.read_parquet(temp_path) + read_back = xo.deferred_read_parquet(temp_path) df_back = xo.execute(read_back) assert len(df_back) == 3 diff --git a/src/boring_semantic_layer/tests/test_xorq_rebuild.py b/src/boring_semantic_layer/tests/test_xorq_rebuild.py index 8128c480..a32431d7 100644 --- a/src/boring_semantic_layer/tests/test_xorq_rebuild.py +++ b/src/boring_semantic_layer/tests/test_xorq_rebuild.py @@ -37,6 +37,17 @@ def _tag_node(tagged_expr): return tagged_expr.op() +@pytest.fixture(autouse=True) +def _git_identity(monkeypatch): + # xorq>=0.3.24's Replayer rewrites no-op commits via ``git rebase --onto``, + # which fails on CI runners that have no global git user.email/user.name. + # Set GIT_*_NAME/EMAIL env vars (they take precedence over git config). + for var in ("GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME"): + monkeypatch.setenv(var, "bsl-test") + for var in ("GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL"): + monkeypatch.setenv(var, "bsl-test@example.invalid") + + # --------------------------------------------------------------------------- # Phase 2: reemit registration # --------------------------------------------------------------------------- @@ -112,7 +123,10 @@ def test_reemit_query_chain_with_source_transform(simple_model): original_meta = dict(_tag_node(tagged).metadata) def add_column(expr): - return expr.mutate(extra=ibis.literal(1)) + # ``expr`` is in xorq.vendor.ibis space; pass a raw scalar so mutate + # infers the literal in the same flavor (xorq>=0.3.24 rejects + # cross-package ``ibis.literal`` here). + return expr.mutate(extra=1) rebuilt = reemit(_tag_node(tagged), rebuild_subexpr=add_column) rebuilt_meta = dict(_tag_node(rebuilt).metadata) @@ -140,7 +154,9 @@ def test_get_rebuild_dispatch_invokes_handler_reemit(simple_model): tagged = to_tagged(simple_model) dispatch = get_rebuild_dispatch(_tag_node(tagged)) - result = dispatch(lambda e: e) + # xorq>=0.3.20 widened the dispatch callable signature to + # (rebuild_subexpr, remap, to_catalog). + result = dispatch(lambda e: e, None, None) assert result is not None rebuilt_meta = dict(_tag_node(result).metadata) original_meta = dict(_tag_node(tagged).metadata) @@ -293,21 +309,3 @@ def test_catalog_rebuild_base_model_executes(catalog_with_base_model, tmpdir): entry = target.get_catalog_entry("city-stats", maybe_alias=True) result = entry.lazy_expr.execute() assert len(result) == 2 - - -# --------------------------------------------------------------------------- -# Edge cases -# --------------------------------------------------------------------------- - - -@requires_reemit -def test_reemit_raises_on_missing_parent(simple_model): - tagged = to_tagged(simple_model) - node = _tag_node(tagged) - original_parent = node.parent - try: - node.parent = None - with pytest.raises(ValueError, match="no parent"): - reemit(node, rebuild_subexpr=lambda e: e) - finally: - node.parent = original_parent diff --git a/uv.lock b/uv.lock index 131ed00e..7799a54d 100644 --- a/uv.lock +++ b/uv.lock @@ -175,7 +175,7 @@ wheels = [ [[package]] name = "boring-semantic-layer" -version = "0.3.12" +version = "0.3.13" source = { editable = "." } dependencies = [ { name = "attrs" }, @@ -283,7 +283,7 @@ requires-dist = [ { name = "urllib3", marker = "extra == 'dev'", specifier = ">=2.2.3" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.30.0" }, { name = "vl-convert-python", marker = "extra == 'viz-altair'", specifier = ">=1.0.0" }, - { name = "xorq", specifier = ">=0.3.19" }, + { name = "xorq", specifier = ">=0.3.25" }, { name = "xorq", marker = "extra == 'examples'" }, { name = "xorq", extras = ["duckdb"], marker = "extra == 'examples'", specifier = ">=0.3.4" }, ] @@ -4391,7 +4391,7 @@ wheels = [ [[package]] name = "xorq" -version = "0.3.19" +version = "0.3.25" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "atpublic" }, @@ -4430,9 +4430,9 @@ dependencies = [ { name = "uv" }, { name = "xorq-datafusion" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/5f/17f6fe75773c313c688728a5273e818890a3307a009e54a69f0565a76079/xorq-0.3.19.tar.gz", hash = "sha256:3e0c46246db2bcd7653c0f581b7264fe49d48bfcda1ae40dcbea050581145726", size = 1964477, upload-time = "2026-04-14T13:14:23.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/49/260378a74cfc43858ded0af74887abe90ea5a38b1c27487febc4d82f402a/xorq-0.3.25.tar.gz", hash = "sha256:ad12f17ea6958cfee786cc18e6cdd5b9602c7d2cfcb763133302817f8adb5e18", size = 1779036, upload-time = "2026-05-19T12:00:38.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/04/aa60dc5f4fb30e12debbac2354b534c3f336467cc93db6769902f68ab312/xorq-0.3.19-py3-none-any.whl", hash = "sha256:eb55e59202c70f471be295882828f09e6728f43c7b04291c8013ca89b2ca5e69", size = 1898204, upload-time = "2026-04-14T13:14:21.966Z" }, + { url = "https://files.pythonhosted.org/packages/6e/20/378d9c10d5660718d36af863ec77f1f5ffb912e9565bb4adb89d27bdd039/xorq-0.3.25-py3-none-any.whl", hash = "sha256:8502c64f10559f8496af76f397a860e5c41bf0b5372de54ef7c241ccca696e06", size = 1705626, upload-time = "2026-05-19T12:00:40.25Z" }, ] [package.optional-dependencies] @@ -4442,23 +4442,20 @@ duckdb = [ [[package]] name = "xorq-datafusion" -version = "0.2.5" +version = "0.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyarrow", marker = "python_full_version < '4'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/f0/cdf7aea073b2bc1f5864e3792d9e8b3003b3147a6164b33bb2dd56bc2de6/xorq_datafusion-0.2.5.tar.gz", hash = "sha256:3e81e69c58556494ad3728f8e27fbdbf779ca67fce33980c84e97dbb66883e70", size = 20626755, upload-time = "2025-12-17T14:41:52.12Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/cc/3dd6148e7f8c3b59cad2f1efd09922dd7089a379a06821540529d9a9d17c/xorq_datafusion-0.2.5-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a6c295b438966fa4257443d0635d8c346d462afaf319831966a0908b064b14ec", size = 42405161, upload-time = "2025-12-17T14:41:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/7ecd70e09f078298e30ec94fa605ce8b678d2e83453c9c7aa917ca812ccf/xorq_datafusion-0.2.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:e4717d8fdfce89ee500750bf189bf1719ba324c277381eeb1e5b2feb21ec53e6", size = 40068779, upload-time = "2025-12-17T14:41:27.558Z" }, - { url = "https://files.pythonhosted.org/packages/01/03/7b23347f54825b30960a7691e2483c88294201e44471d8153d600906fe84/xorq_datafusion-0.2.5-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c32d30baaeec30f761771934d8e9b62b3d3436b474a413542927f9cc051fec", size = 50202159, upload-time = "2025-12-17T14:41:30.467Z" }, - { url = "https://files.pythonhosted.org/packages/f1/93/8083bba0fd205ca811984fc71f18a06ecbd4a857351c3de0948dca026a3b/xorq_datafusion-0.2.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948a783380bb51dd0de827433414c01c28ed6660eaaa246faa0040e6daddb246", size = 45667583, upload-time = "2025-12-17T14:41:33.11Z" }, - { url = "https://files.pythonhosted.org/packages/38/b7/0550a8c694b419eda36c6e6c7f60fd2a9e032189586fac0bb073182c4d99/xorq_datafusion-0.2.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bc9b92ed0e09f53feb8049eab0d5c02bb75204fbaf8f80a96ee6ae33f239683d", size = 44002341, upload-time = "2025-12-17T14:41:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/04/0d/2d3a2c5f929528bdfe3afbd15ad28813d5d01820dc88bf9fd1079f601b8d/xorq_datafusion-0.2.5-cp38-abi3-win32.whl", hash = "sha256:e0d6b63766c43e3c36c66d38ba71b40699065bd1ce795efc22e1f9e91cb2479a", size = 34385556, upload-time = "2025-12-17T14:41:38.566Z" }, - { url = "https://files.pythonhosted.org/packages/64/89/d6811880f3a9b381fa4e50df7f27bbeccccd6985ff05f85ee3added3225b/xorq_datafusion-0.2.5-cp38-abi3-win_amd64.whl", hash = "sha256:e7206853ef20c48a38ed7b24fce0a743fdcd12de40c2015a034ae37a76e72573", size = 39385564, upload-time = "2025-12-17T14:41:41.029Z" }, - { url = "https://files.pythonhosted.org/packages/72/6c/a1b83f292cef9ea95646a69a8b3a58a856892cb8a8e8d36671f02108b849/xorq_datafusion-0.2.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c456e4cff93e76abcc3b30c888d9c0a5fbdb77553bc8f22ff36dbbb1b5b4d7df", size = 43994555, upload-time = "2025-12-17T14:41:43.805Z" }, - { url = "https://files.pythonhosted.org/packages/e6/bb/eef7a76c612f95a47da5573f593879513f13ee84cc770e09aa47b80e767f/xorq_datafusion-0.2.5-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e4354979c60164fb6f6aff7b116fc938fd5b204629033f7275f29370d18f860", size = 50196801, upload-time = "2025-12-17T14:41:46.534Z" }, - { url = "https://files.pythonhosted.org/packages/18/a2/74faac562c4ec9cb930f9a73a1a89ee6fb5a53fdf57e2da597a54eb6f450/xorq_datafusion-0.2.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e88801cd628f39be1db2bdbc7443c80157c7f2441b41bacbd1576eb4e723b83", size = 45655221, upload-time = "2025-12-17T14:41:49.368Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/d01aaf4b6f6940f8709321d356ca29490280a0bce85b90f4247b13256725/xorq_datafusion-0.2.7.tar.gz", hash = "sha256:be1dbdbef6ea513df1aae189fe5fa6e54a8e8556dc824a74a888473b4c1929cb", size = 20639907, upload-time = "2026-05-14T11:00:22.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/84/e514a0e1d63197817de49da5d940136060e57447396c69748ebae210c291/xorq_datafusion-0.2.7-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5152aabfc7452ef5e7f7469d7ed835cb6d478696745ce98eccac1abf9cb04112", size = 48099334, upload-time = "2026-05-14T10:59:48.596Z" }, + { url = "https://files.pythonhosted.org/packages/51/98/5f8a1555af37c4a814c69231015ac08dea4ade4e8b250509bb65cdc56ae3/xorq_datafusion-0.2.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:3874fc7e0186e53dcce7fe5390a71be97c8c386f5044063cb96327a64d6a9112", size = 45730841, upload-time = "2026-05-14T10:59:54.741Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/8137598bf67c40ea0b75d39adfe8a3b6824440a56e9bc201f3d26992c355/xorq_datafusion-0.2.7-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:225b612874ee2da79c2a7d80ec4e7040baaf6db0f1fa364dc79b16b320fb17c0", size = 58486501, upload-time = "2026-05-14T10:59:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/ba/23/2fa5c4eb26d3755cf34df95aeb79af76647e04159c88897232a561b46fdd/xorq_datafusion-0.2.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fec22bd24018410152fc5f8582033e86046f0f31e581aaa2f29103d3dacb35ef", size = 51751418, upload-time = "2026-05-14T11:00:04.567Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4c/1309100ba3e21be388d8dec28c4560481447cada4f409da1ea0863b7bd70/xorq_datafusion-0.2.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b1177c5dcd9ea3b519d2c9d2db16296740463f2302319dda88983a9204463a32", size = 49958539, upload-time = "2026-05-14T11:00:09.619Z" }, + { url = "https://files.pythonhosted.org/packages/08/6a/5dcf6d9db0d7b94afc9851f309c505b9edd4c4c571c11efd301a4dad12cc/xorq_datafusion-0.2.7-cp38-abi3-win32.whl", hash = "sha256:36193827af0c134b14dcf6229ffe9d064e8e8a94888373e6ecaf4c47a5ef6587", size = 40319107, upload-time = "2026-05-14T11:00:14.359Z" }, + { url = "https://files.pythonhosted.org/packages/5b/35/e820dba4c884f3eb6aaec6a8a53b94db0a4a7d9ec1b2cc4bbfde45df26fd/xorq_datafusion-0.2.7-cp38-abi3-win_amd64.whl", hash = "sha256:b018b51dc8d1a4c9336cb3a477885fe5224e376b8ba83a2fa191c43dde90e6fe", size = 44687714, upload-time = "2026-05-14T11:00:18.889Z" }, ] [[package]] From 11052103b8dc23484b3300350580dc85bf131cb3 Mon Sep 17 00:00:00 2001 From: ghoersti Date: Tue, 19 May 2026 16:05:39 -0400 Subject: [PATCH 2/3] docs(tag_handler): narrow reemit precondition to HashingTag only BSL only ever emits HashingTag (both to_tagged and reemit itself use ``hashing_tag``), so the previous "(HashingTag/Tag)" was misleading. Co-Authored-By: Claude Opus 4.6 --- .../serialization/tag_handler.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/boring_semantic_layer/serialization/tag_handler.py b/src/boring_semantic_layer/serialization/tag_handler.py index d964ee18..e141b249 100644 --- a/src/boring_semantic_layer/serialization/tag_handler.py +++ b/src/boring_semantic_layer/serialization/tag_handler.py @@ -104,11 +104,12 @@ def reemit(tag_node, rebuild_subexpr): Re-stamping uses ``hashing_tag`` (not ``tag``) so the rebuilt expression keeps the same hash-contribution guarantee as ``to_tagged`` — see #263. - Precondition: ``tag_node`` is a BSL-tagged xorq tag op (HashingTag/Tag). - xorq's dispatch only routes here when ``tag_node.metadata["tag"]`` - resolves to this handler, and xorq's op definition declares - ``parent: Relation`` (non-null) — so by construction - ``tag_node.parent`` is always a valid relation. + Precondition: ``tag_node`` is a BSL-tagged xorq ``HashingTag`` op (BSL + only ever emits ``HashingTag`` — see ``to_tagged`` and the re-stamp + below). xorq's dispatch only routes here when + ``tag_node.metadata["tag"]`` resolves to this handler, and xorq's op + definition declares ``parent: Relation`` (non-null) — so by + construction ``tag_node.parent`` is always a valid relation. """ new_source = rebuild_subexpr(tag_node.parent.to_expr()) meta = dict(tag_node.metadata) From 9c3aa56195cdfd28231a9d8f38457e813a033a25 Mon Sep 17 00:00:00 2001 From: ghoersti Date: Wed, 20 May 2026 09:30:56 -0400 Subject: [PATCH 3/3] fix(examples): rename xo.read_parquet to deferred_read_parquet Same xorq 0.3.20+ API rename already applied in tests; missed this example call site in the original commit. ``xo.read_parquet`` now falls through to ``ibis.load_backend("read_parquet")`` which raises AttributeError, breaking ``make examples``. --- examples/basic_flights_xorq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/basic_flights_xorq.py b/examples/basic_flights_xorq.py index 4aec6ead..bf1d5012 100644 --- a/examples/basic_flights_xorq.py +++ b/examples/basic_flights_xorq.py @@ -10,7 +10,7 @@ def main(): - flights_tbl = xo.read_parquet(f"{BASE_URL}/flights.parquet") + flights_tbl = xo.deferred_read_parquet(f"{BASE_URL}/flights.parquet") flights = ( to_semantic_table(flights_tbl, name="flights")